【Linux】深入理解进程控制:从创建到终止和进程等待
文章目录
- 进程创建
- fork函数
- 如何用fork函数创建子进程
- 写实拷贝
- 进程终止
- 错误信息
- exit
- _exit
- 进程等待
- wait
- waitpid
- 总结
进程创建
fork函数
fork
函数是 Unix/Linux 系统中用于创建新进程的系统调用。调用 fork
后,当前进程(父进程)会被复制,创建出一个新的进程(子进程)。
fork函数特点:
-
返回值:
- 在父进程中,
fork
返回子进程的 PID(进程ID)。 - 在子进程中,
fork
返回 0。 - 如果发生错误,
fork
返回 -1,并且不会创建新进程。
- 在父进程中,
-
进程资源:
- 父进程和子进程拥有相同的代码和数据段,但各自的进程空间是独立的。
- 子进程继承父进程的文件描述符等资源,但文件描述符的状态(如文件指针位置)是共享的。
-
执行顺序:
- 父进程和子进程可以并发执行,执行顺序不固定,取决于操作系统的调度策略。
如何用fork函数创建子进程
我们写一个简单的代码观察子进程的创建:
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t id=fork();
while(1);
return 0;
}
可以看见子进程已经创建。
写实拷贝
“写实拷贝”是一种优化技术,常用于内存管理,特别是在进程创建和资源共享的场景中。其基本原理是:在创建新进程时,父进程和子进程共享相同的内存页,直到其中一个进程尝试修改这些内存页时,系统才会为该进程创建一个独立的副本,从而避免不必要的内存复制,提高系统性能。
当子进程创建之后,子进程以父进程的PCB为模版,创建自己的PCB,然后指向同一块资源,但是当父进程或者子进程对对应资源进行修改的时候,会发生写实拷贝。
原本数据块指向同一块区域的父子进程会指向不同区域。
进程终止
进程终止的常用方法:
- 通过main函数return
- exit
- _exit
异常退出:
Ctrl+c 信号终止
每个进程退出的时候都是有退出码的,我们来验证一下:
我们写一个简单的代码:
#include<stdio.h>
int main()
{
return 10;
}
用命令查看上一次退出码:
可以看见退出码是10,我们再次查看一遍:
可以看见再次查看一遍退出信息就变成了0了,这是为什么呢?----原因就是因为我们使用的上一条命令也是一个进程,因为Linux的命令都是用C语言写的,通常运行成功都是会返回0的,所以这里查看最近一个程序的退出信息时就变成0了。
但是为什么返回0就是成功非零就是失败呢?----因为不同的数字代表不同的错误信息,系统提供了一批错误码来控制。
错误信息
在C语言中我们通常用一个全局变量来代表最近一个进程的错误码:
当我们创建子进程的时候也有创建失败的时候,所以当创建失败时,我们可以利用errno将错误信息打出,然后返回错误码给父进程。
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
int main()
{
pid_t id=fork();
if(id<0)
{
printf("error code:%d.errstring:%s\n",errno,strerror(errno));
return errno;
}
return 0;
}
我们来看看错误码的范围:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
int i=0;
for(i=0;i<200;i++)
{
printf("errno:%d,errstring:%s\n",i,strerror(i));
}
return 0;
}
通过上面这个简单的程序我们可以知道错误码的最大值是133。
前面一些都是比较熟悉的错误码,操作不被允许啊,文件找不到啊,还有没有这个进程啊之类的。
比如:
这个的错误码就是2。
还有:
这个的错误码并不是3,是因为kill杀死这个进程失败了,返回1,错误码输出的就是1了。
exit
exit在程序的任何地方表示进程结束。
_exit
_exit和exit相同,唯一不同的是exit不是系统调用,而_exit是系统调用,exit内部是用系统调用封装的。
进程等待
关于进程等待的三个函数,我们先从第一个函数说起:
wait
wait
函数在 Unix 和 Linux 系统中用于让父进程等待其子进程结束,并收集子进程的退出状态。这个函数在进程控制中尤为重要,因为它允许父进程在子进程完成之前暂停执行,避免“僵尸进程”的出现。
一般而言父进程创建子进程就需要等待子进程,子进程结束之后将子进程的僵尸状态回收掉。
用一段代码来展示一下这个函数的用法:
#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我是pid是%d\n",getpid());
sleep(1);
}
}
else
{
sleep(3);
pid_t rid=wait(NULL);
if(rid>0)
printf("sub process wait success,sub process pid is:%d\n",rid);
while(1)
{
printf("我是父进程,我的pid是%d\n",getpid());
sleep(1);
}
}
return 0;
}
可以看见这里子进程一直运行,父进程一直是阻塞状态,一直在等待子进程结束,回收子进程。
我们手动杀死一下子进程。
可以看见回收成功并且返回了子进程的pid。
这里子进程结束也可能是异常结束,结束分为几种情况:
- 代码跑完了,结果是对的,return 0
- 代码跑完了,结果是错的,return !0
- 进程异常了
前两种很容易理解,第三种异常情况,假如我们有一个指向nullptr的指针,我们对这个指针进行操作,虚拟地址转化到物理地址的过程中就会出现异常访问,所以这里系统可能会直接将进程杀死保护物理内存。(这里系统其实是释放信号,将进程杀死的)
我们来看看有哪些信号吧:
这里有很多信号,几种常见的就是下面几种:
当我们异常访问的时候会出现段错误,当我们1/0的时候会出现浮点数错误。
我们用代码来掩饰两个错误的信号:
首先在写代码之前,我们要知道退出信息
要知道退出信息我们就要知道一个接口,这个接口就是:
waitpid
第一个参数pid表示等待某一个进程,当第一个参数大于零的时候是等待指定进程,当这个参数为-1的时候表示等待任意进程。
第二个参数表示退出信息,这个退出信息就是上面那个图,高八位是退出码,低七位是退出信号。
#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
using namespace std;
int main()
{
pid_t id=fork();
if(id < 0)//create fail
{
printf("errno code:%d,errstring:%s\n",errno,strerror(errno));
}
else if(id==0)//child
{
double result = 1/0;
}
else//parent
{
int signal=0;
pid_t rid=waitpid(-1,&signal,0);
if(rid>0)
{
printf("等待成功,子进程的pid是%d,退出信号%d\n",rid,signal&0x7F);
}
else//wait fail
{
//printf fail information
printf("wait sub process fail,exit Signal:%d\n",signal&0x7F);
}
}
return 0;
}
可以看到这里退出信号对比上面的信号表应该是8。
这里可以看见对应的情况也是8.
除了通过退出信息来获取退出码和退出信号还可以通过C语言库中的宏来取出退出信号和退出码。
我们只需要通过取出退出信息,用宏来计算退出信号和退出码即可。
我们来说说waitpid的第三个参数options,第三个参数options表示的是等待时父进程的状态,是阻塞等待还是非阻塞等待,意思就是是一直等待,等子进程结束之后再完成父进程的任务,还是边等待边完成自己的任务。
options的参数:
- 0表示阻塞等待
- WNOHANG表示非阻塞等待
非阻塞等待状态样例代码:
#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)//child
{
int count=5;
while(count--)
{
cout<<"我是子进程"<<",我的pid是"<<getpid()<<endl;
sleep(1);
}
exit(1);
}
while(true)
{
int status = 0;
pid_t rid =waitpid(-1,&status,WNOHANG);
if(rid>0)
{
cout<<"wait success"<<endl;
break;
}
else if(rid<0)
{
cout<<"wait fail"<<endl;
cout<<"exit code:"<<((status>>8)&0xFF)<<" exit Signal:"<<(status&0x7F);
break;
}
else//parent do
{
cout<<"我是父进程,我的pid是"<<getpid()<<endl;
sleep(1);
}
}
return 0;
}
可以看见当等待状态为分阻塞时,我们的父子进程都是正常运行的。
总结
在本篇博客中,我们深入探讨了Linux进程控制的核心概念,从进程的创建、状态管理到终止及等待机制。通过了解 fork、exec 和 wait 等系统调用,我们掌握了如何有效地管理进程的生命周期。此外,我们还分析了父子进程之间的关系以及信号处理在进程控制中的重要性。
掌握进程控制不仅有助于提升对Linux操作系统的理解,更是编写高效和可靠程序的基础。随着对多进程编程的深入掌握,开发者可以更好地利用系统资源,提高应用的性能和响应能力。
希望这篇博客能为您在Linux进程控制的学习旅程中提供有价值的参考和启示。继续探索这一领域,您将发现更多精彩的技术与实践!