【Linux】 进程控制

文章详细介绍了Linux中进程的创建,特别是fork函数的工作原理,包括写时拷贝的概念。接着讨论了进程的正常和异常终止,以及退出码在判断程序执行结果中的作用。最后,阐述了进程等待的重要性,wait和waitpid函数的使用,以及如何获取子进程的退出信息。文章强调了进程等待在避免僵尸进程和资源回收中的关键角色。
摘要由CSDN通过智能技术生成

📕 进程创建

fork 函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做如下的事情:

  • 分配新的内存块和内核数据结构(pcb)给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

进程 = 内核数据结构 + 代码和数据。 创建一个进程,必须要有内核数据结构(pcb) 、代码和数据。对于内核数据结构而言,子进程继承了父进程的绝大部分内容;而对于代码和数据,如果子进程没有作任何修改,那么它们共用一份代码和数据。但是,如果父进程 或者 子进程(假设该进程为 proc ),想要修改某个数据 x(假设其物理地址是 A),那么就会发生 “写时拷贝” ,将数据拷贝到物理地址 B 的地方,然后修改数据,并且更改 proc 的页表映射关系,让 x 的虚拟地址映射到 物理地址 B 处。(详情可见 【Linux】进程地址空间

写时拷贝本质上是一种按需申请资源的策略。由于操作系统不允许任何的资源浪费,所以,操作系统不会为子进程独立开辟一块物理内存来存放代码和数据,而是和父进程共用。当我们对数据不作修改的时候,就直接运行;要修改数据的时候,就进行写时拷贝。

但是,只有数据会发生写时拷贝吗?代码会不会发生写时拷贝?目前而言,确实只有数据会被修改,因为代码是不可变的,编译之后生成的可执行文件,无法去修改里面的代码。(但是,代码会发生替换,具体下一篇blog会涉及)
请添加图片描述

如下,执行下列代码。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>                                                                                                           
  4 
  5 int main( void )
  6 {
  7     pid_t pid;
  8     printf("Before: pid is %d\n", getpid());
  9     if ( (pid=fork()) == -1 )
 10       {
 11          perror("fork()");
 12          exit(1);
 13       }
 14     printf("After:pid is %d, fork return %d\n", getpid(), pid);
 15     sleep(1);
 16 return 0;
 17 }

其结果如下图所示。这里看到了三行输出,一行before,两行after。进程5497先打印before消息,然后它又打印after。另一个after消息有5498打印的。注意到进程5498没有打印before,这是因为,fork 之前的代码,由当前进程(父进程)独立执行。fork之后的代码,由父子进程(当前进程变成父进程)这两个执行流分别执行。
值得注意的是,fork之后,谁先执行是完全由调度器的调度算法决定的。
请添加图片描述
由上图我们也可以了解到 fork 函数的返回值。在子进程中,fork 函数返回0 ,在父进程中,fork 函数返回子进程的pid。

fork函数的常规用法

fork函数的用法主要用以下两种,本文主要讨论第一种情况,对其进行分析。

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

有时,fork 函数也会调用失败,其失败的原因一般分为两种:系统中有太多的进程;实际用户的进程数超过了限制。

📕 进程终止

情况分类

一个进程终止了,有两种情况:

  • 正常执行完。
    • 结果正确,不用管了。
    • 结果不正确,要知道为什么不正确。
  • 崩溃了(进程异常) – 崩溃的本质:进程收到了来自操作系统的信号(比如 kill -9)。

在我们写 C 语言代码的时候, main 函数总是要 return 0; 这个返回的 0 ,叫做退出码。退出码用来标识进程执行结果的正确与否。如果退出码为 0,结果正确;退出码为 非0,结果不正确。结果不正确的时候,退出码是 非0,那么就可以是 1、2、3、4、…… 等等,由于要知道结果不正确的原因,所以用不同的退出码标识不同的错误。
所以,退出码为0,进程执行结果正确;退出码 非0,进程执行结果不正确,可以根据退出码知道错误原因。

退出码

下图是下列代码的运行结果。对于这段程序,是想要实现 1 到 100 的加法,我们并没有打印任何信息来标识结果是否正确。但是,却可以根据退出码,来知道是不是正确的。如下, echo $? 的指令,是打印上一个进程的退出码,打印出来是 11 ,根据程序源代码来看,很明显 ret 不等于5050。
这就是,退出码的作用,我们可以用退出码来查看进程执行结果的正确与否。
请添加图片描述

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main( void )
  6 {
  7     int ret=0,i;
  8     for(i=1;i<100;i++){                                                                                                      
  9       ret += i;
 10     }
 11 
 12     if(ret == 5050) return 0;
 13     else return 11;
 14 }

当然,如果 退出码 不是0,也就是说 进程执行结果不正确,我们可以根据退出码来判断错误原因。这可以用 strerror 函数来演示,这是C语言中 <string.h> 头文件下的一个函数,可以根据错误号返回该一个指向错误字符串的指针。这里的错误号就类似于退出码,不同的错误号标识不同的错误信息。

请添加图片描述

  1 #include<stdio.h>
  2 #include<string.h>
  3 
  4 int main( void )
  5 {
  6   int i;
  7   for(i=0;i<=20;i++){
  8     printf("%d: %s\n",i,strerror(i));
  9   }
 10 
 11   return 0;                                                                                                                  
 12 }

但是,这上面的退出码和错误信息的对应关系,只不过是C语言定义的,其他环境下不一定支持这种对应关系。
如下图,在 Linux 环境下,我杀掉一个不存在的进程,它报错 “No such process”。然后查看上一个进程(kill -9 21345)的退出码,是 1 。在C语言的标准下,“No such process” 应该是3号退出码。
请添加图片描述

至此,我们可以了解到的信息是,进程退出有两种状态,一种是正常执行完退出,另一种是进程崩溃。对于前者, 结果正确的情况下,我们不用去关心;结果不正确的情况下,我们可以通过退出码来判断错误原因

理解

对于操作系统而言,一个进程退出了,就要释放该进程的 内核数据结构、代码和数据(如果有独立的)。

那么如何实现进程退出呢?

  • main 函数 return 。其他函数 return,只代表函数返回,这从平时运行程序就可以明显看出。
  • exit(int val) 。该函数的参数 val 就是进程的退出码。exit 无论是在main函数,还是在其他函数,只要执行,进程必然退出,并返回 val 当作退出码。
  • _exit(int status) 。这个函数和 exit 基本上差不多,但是 _exit 不会刷新缓冲区。

如下演示,用 C 语言的库函数 exit 会冲刷缓冲区,将缓冲区内的数据打印出来。但是系统调用 _exit 会直接结束进程,不会刷新缓冲区
请添加图片描述
通过这个例子,我们可以猜测, exit 内部是调用了 _exit 的。只是 exit 先进行了缓冲区的冲刷,然后再调用 _exit 这个系统调用。

📕 进程等待

进程等待的重要性

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼” 的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(退出码 / 信号)。

所以,进程等待就是通过系统调用,获取子进程的退出码或者退出信号的方式,顺便解决内存问题。

进程等待的方法

进程等待可以通过两个系统调用,wait 和 waitpid

wait

头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

通过下列代码来验证 wait 的作用。

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main( void )
  8 {
  9 
 10   pid_t id=fork();
 11   if(id == 0) // 子进程
 12   {
 13     int cnt=5;
 14     while(cnt){
 15       printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
 16       cnt--;
 17       sleep(1);
 18     }
 19       exit(0);                                                                                          
 20   }
 21   // 父进程
 22   sleep(10);
 23   pid_t ret=wait(NULL);
 24   printf("我是父进程,我醒了,我的pid是: %d ,ret: %d \n",getpid(),ret);
 25   sleep(5);
 26   return 0;
 27 }

如下,父子进程两个执行流同时执行,但是父进程先睡 10 s,在子进程执行 5s 后,父进程还在 sleep,此时子进程已经退出,但是父进程没有回收他的退出信息,于是子进程变成僵尸状态,该状态持续 5s 。父进程醒来后,回收了子进程的退出信息,于是子进程彻底退出。
这里我们并没有关心子进程的退出信息,于是在 wait 函数的参数填写的是 NULL。
请添加图片描述
如果想要获取退出结果,最好使用 waitpid 系统调用。

waitpid

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
pid=-1,等待任一个子进程。与wait等效。
pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

简单理解一下,这个系统调用的返回值 >0 ,等待成功返回值 =-1,等待失败
第一个参数 pid > 0 ,表示指定要等待的进程, pid = -1 ,表示等待任意一个子进程
至于第二个参数 status ,不能简单地看作一个整数,要把它看作位图,等待结束之后,status 存储的就是进程的退出信息(传的指针,所以可以改变实参)。

如下,status 的低八位表示终止信号,高八位表示退出状态(退出码)。status 是 int 类型的变量,有 32 位比特位,对于高 16 位,这里并不关心,只使用低16 位。
core dump 如果为0,表示没有收到信号,正常退出。那么此时就要关心其退出码,根据退出码判断结果是否正确,如果结果错误,错误原因是什么。

请添加图片描述


如下代码运行结果,确实可以看出 status 并不是简单地存储数据。它实际上就是一个位图结构。

请添加图片描述

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main( void )
  8 {
  9 
 10   pid_t id=fork();
 11   if(id == 0) // 子进程
 12   {
 13     int cnt=5;
 14     while(cnt){
 15       printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
 16       cnt--;
 17       sleep(1);
 18     }
 19       exit(122);
 20   }
 21   // 父进程
 22   int status=0;
 23   pid_t ret=waitpid(id,&status,0);
 24   printf("我是父进程,我的pid是: %d ,ret: %d ,status: %d \n",getpid(),ret,status);
 25   return 0;                                                                                             
 26 }   

需要像下面一样才可以。status 右移八位,然后按位与 0xff ,也就是二进制的 11111111 ,得到退出码。 status 按位与0x7f ,也就是 01111111,得到退出信号。结果和我们上面所讲的 status 位图结构是一样的。

请添加图片描述

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main( void )
  8 {
  9 
 10   pid_t id=fork();
 11   if(id == 0) // 子进程
 12   {
 13     int cnt=5;
 14     while(cnt){
 15       printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
 16       cnt--;
 17       sleep(1);
 18     }
 19       exit(122);
 20   }
 21   // 父进程
 22   int status=0;
 23   pid_t ret=waitpid(id,&status,0);
 24   printf("我是父进程,我的pid是: %d ,ret: %d ,退出码: %d ,信号: %d \n",getpid(),ret,(status>>8)&0xff,status&0x7f);                                                                                              
 25   return 0;
 26 }

如下图,修改一下子进程的代码,使其异常退出,其信号变成了 8 ,说明是异常退出,此时退出码为0。

请添加图片描述


但是,一般而言我们不会使用上面的方法(按位与)来得到退出码和退出信号,而是使用宏。
WIFEXITED( ); 如果子进程正常退出,返回值为真。
WEXITSTATUS( ); 得到子进程的退出码。

如下代码,在父进程等待成功之后,分情况输出。
如果子进程正常退出,那么输出其退出码。
如果子进程异常退出,那么输出信号。信号不可以用 WIFEXITED( ); 这个宏来得到,这个宏只是让我们知道子进程是否正常退出,所以得到信号要用按位与的方法。

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main( void )
  8 {
  9 
 10   pid_t id=fork();
 11   if(id == 0) // 子进程
 12   {
 13     int cnt=5;
 14     while(cnt){
 15       printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
 16       cnt--;
 17       sleep(1);
 18     }
 19       exit(122);
 20   }
 21   // 父进程
 22   while(1)                                                                                                                   
 23   {
 24     int status=0;
 25     pid_t ret=waitpid(id,&status,WNOHANG);            
 26     if(ret == -1){                                    
 27       printf("wait error\n");
 28       exit(-1);
 29     }
 30     else if(ret == 0){
 31       printf("子进程正常执行,还未结束,我先执行这里代码!\n");
 32       sleep(1);
 33       continue;
 34     }
 35     else{ // 等待成功
 36       if(WIFEXITED(status)){
 37         printf("wait sucess,child exit code: %d  \n",WEXITSTATUS(status));
 38       }
 39       else{
 40         printf("wait sucess,child exit signal: %d  \n",status & 0x7f);
 41       }
 42       break;
 43     }
 44   }
 45   return 0;
 46 }


父进程等待的时候做了什么

在子进程没有退出的时候,父进程只能一直在调用 waitpid 的地方等待,这叫做阻塞等待。

如下, 通过代码 和 运行情况可以看出,父进程确实在 waitpid 的地方等待,这就是阻塞等待,父进程没有干任何事情,只是在等待子进程退出。

请添加图片描述
如果不想进行阻塞等待,让父进程在等待子进程的同时,可以执行其他代码,那么可以将 waitpid 的第三个参数设置为 WNOHANG 。如下是 man 手册中,waitpid 系统调用的返回值说明。
如果使用 WNOHANG 参数,当子进程还没有结束的时候,如果等待出错,返回 -1;等待正常,返回0。

请添加图片描述
这种方式叫非阻塞轮询,父进程会隔一段时间观察一下子进程的状态,得到返回值,如果返回值是 -1,说明子进程异常执行;如果返回值是0,说明子进程正在正常执行,还未结束;如果返回值大于0,那么得到的返回值就是子进程的 pid,子进程结束

我们可以通过下列代码来观察非阻塞轮询。

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main( void )
  8 {
  9 
 10   pid_t id=fork();
 11   if(id == 0) // 子进程
 12   {
 13     int cnt=5;
 14     while(cnt){
 15       printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
 16       cnt--;
 17       sleep(1);
 18     }
 19       exit(122);
 20   }
 21   // 父进程
 22   while(1)                                                                                                                   
 23   {                                                                               
 24     int status=0;
 25     pid_t ret=waitpid(id,&status,WNOHANG);                                        
 26     if(ret == -1){ // 子进程异常执行
 27       printf("wait error\n");
  28       exit(-1);
 29     }
 30     else if(ret == 0){  // 子进程正常
 31       printf("子进程正常执行,还未结束,我先执行这里代码!\n");
 32       sleep(1);
 33       continue;
 34     }
 35     else{  // 子进程执行结束
 36       printf("我是父进程,我的pid是: %d ,ret: %d ,退出码: %d ,信号: %d \n",getpid(),ret,(status>>8)&0xff,status&0x7f);
 37       break;
 38     }
 39   }
 40   return 0;
 41 }


代码运行结果如下,确实父进程在执行其他的代码。

请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力努力再努力.xx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值