【Linux】进程创建、退出和等待(fork、exit和_exit、waitpid和wait、阻塞和非阻塞)

1、进程创建

进程的创建主要依靠系统接口fork函数

fork函数从已存在的一个进程中,创建一个子进程,原进程为父进程。

#include <unistd>
#include <sys/type.h>//用pid_t 需要包括这个文件
pid_t fork(void);

父进程返回子进程pid, 子进程返回0,出错返回-1。

1.1 理解fork函数

先从一个小程序看看fork函数的效果。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/type.h> //用pid_t 需要包括这个文件
  4 int main()
  5 {
  6     pid_t id = fork();                                                                                                                                      
  7     if(id < 0)        
  8     {         
  9         printf("fork error!\n");
 10     }                           
 11                                 
 12     if(id > 0)
 13     {         
 14         printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
 15     }                                                                                     
 16                                                                                           
 17     else
 18     {   
 19         printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
 20     }                                                                                     
 21     sleep(2);                                                                             
 22              
 23     return 0;
 24 }

在这里插入图片描述

pid_t 是什么?
首先在/usr/include/sys/types.h中,通过通过/pid_t查询
这里是引用
再到/usr/include/bits/types.h中,通过/__pid_t查询

在这里插入图片描述
再到/usr/include/bits/typesizes.h中,通过/__PID_T_TYPE查询
在这里插入图片描述
回到/usr/include/bits/types.h中,通过/__S32_TYPE查询,发现其实就是int。
在这里插入图片描述

为什么要弄这么麻烦? 其实为了代码在不同平台上跑,可能其它平台是long,而不是int。(为了可移植性)

可能你会有疑惑,为什么会有两次打印?打印为什么是这个结果?代码是怎么走的?

我们的程序代码执行前
首先,我们所写的程序,在运行后加载到内存就成了Linux系统中的一个进程。
当我们运行编译好的程序后,程序加载到内存称为了一个Linux进程。
该进程(对应pid:31200)由命令行解释器bash(bash是一个系统进程,这里对应31107)创建,作为其子进程执行代码。

代码执行过程
进入main函数,执行pid_t id = fork(); 此时转到操作系统内核fork定义处,执行fork函数代码。
(下图的子进程,其实不是在fork中马上创了一个空间,这里为了更好理解,下面会解释)
在这里插入图片描述

所以其实很简单,就是fork之后,有了两个执行流,通过返回值的不同走不同的代码路径。

1.2 fork函数的细节

有几个细节,能让我们更好理解fork。
在前面我们解释了fork函数为什么有两个返回值的问题,就是通过fork创建子进程,有了两个执行流。

首先

  • 如何理解fork之后,父进程返回子进程id,子进程返回0?
    我们都知道,一个父亲可以对应着多个孩子,而多个孩子只能对应一个父亲。
    进程也一样,我们可以通过getpid和getppid得到唯一的自己和父亲,但对于孩子,如果我们需要找其中一个就需要有一个确定值。

其次

  • 为什么会有一个变量id,储存两个不同的值?

pid id = fork(); 首先对于一个进程,我们并不确定父子进程哪个先执行完。
返回的本质,其实就是写入值到id,所以谁先返回谁就先写入id。
后写入的进程,因为进程独立性,为了不影响前面的一个进程就会发生写时拷贝
在这里插入图片描述

  • 我们看到fork失败会返回-1,那么什么情况会发生呢?
    1、当系统中有太多进程,通常意味着某方面出了问题(比如 死循环调用fork)。
    2、当该用户ID的进程数超过了系统的限制数。(CHILD_MAX)

  • fork的通常用法
    1、通过创建子进程,继承父进程的代码,运行和父进程运行的不同代码路径。
    2、创建子进程,运行其它的进程(比如后续进程程序替换中的exec系列的函数)。

2、进程退出

2.1 退出码

从main函数开始。
我们之前写的程序很多在最后都会有一个return 0;
这个0其实就是退出码,它标识着程序运行成功。

int main()
{
	return 0;
}


通过echo $? 可以查看记录最近一个进程在命令行执行完毕时对应的退出码。

在这里插入图片描述
在这里插入图片描述

  • 进程退出的情况?
    1、代码跑完,结果正确 — return 0;
    2、代码跑完了,结果不正确 — return !0;
    3、代码没跑完,程序异常了,退出码无意义。

如果我们关心退出码,可以通过不同的数字,表述不同的错误。
如果我们并不知道退出码对应的退出信息是什么,可以通过strerror(errno)。

如果熟悉个别退出码对应的信息,可以通过strerror(num) 打印退出信息。

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

由于结果太长,只截了开头一段和结尾一段。(通过运行看结果 可以看到退出码只有0-133)
在这里插入图片描述
在这里插入图片描述

2.2 exit函数和_exit系统调用

  • exit()
    exit函数终止进程,返回对应退出码。

    #include <stdlib.h>
    void exit(int status);
    

    exit虽然并没有返回值,但是会将status传给父进程接收退出码。(这个后面进程等待会解释,先了解)
    通过man 3 exit
    在这里插入图片描述
    在C语言阶段,我们会在一些地方使用exit(num),里面的num其实就是退出码,退出码可以根据需要自己定义。

    #include <stdio.h>
    #include <stdlib.h>
    void fun()
    {
    	exit(10);//从这里程序退出。
    }
    int main()
    {
    	fun();
    	printf("hello world");
    }
    

  • _exit
    _exit作为一个系统接口,在操作系统层。以及exit其实就是调_exit实现的。

    #include <unistd.h>
    void _exit(int status);
    
  • exit和_exit的区别

    先通过一个小程序看exit

    	  1 #include <stdio.h>
    	  2 #include <unistd.h>
    	  3 #include <stdlib.h>                                                                                                                                         
    	  4 
    	  5 int main()
    	  6 {
    	  7     printf("process");
    	  8     sleep(2);
    	  9     exit(1);
    	 10 }
    

    通过运行
    在这里插入图片描述

    再经过小小修改

      1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <stdlib.h>
      4 
      5 int main()
      6 {
      7     printf("process");
      8     sleep(2);
      9     _exit(1);                                                                                                                                               
     10 }
    
    

    在这里插入图片描述

    其实exit和_exit的区别就是exit刷新缓冲区,但是_exit不刷新缓冲区。
    sleep后,进程放入等待队列,输出在缓冲区,等进程重新回到运行队列_exit直接退出了,exit会刷新缓冲区,所以有这两个结果。

    根据这个结果我们也可以推出:缓存区其实是一个用户级的缓冲区。
    在这里插入图片描述

3、进程等待

一个子进程在退出后,操作系统回收它的数据与代码,但是进程一定是为了什么目的才存在的,一个进程完成后可以不将结果汇报给创造它的父进程,但是不能没有结果。

其实,一个进程在退出后,操作系统依旧会保留其PCB,等待父进程或系统对该进程进行回收。
子进程在这个PCB被保留的状态就是一个僵尸进程,父进程通过进程等待的方式对子进程回收并且获得子进程退出信息

3.1 wait和waitpid

  • wait()
#include <sys/wait.h>
#include <sys/type.h>
pid_t wait(int* status);

当status值设为NULL时,只回收子进程,代表不在意回收的子进程的退出码。
当status不为NULL时,回收子进程,并且获得子进程的退出信息,存放在status中。

假设status不为NULL,status不是简单的存一个值,下面解释它如何保存信息。
在这里插入图片描述

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <assert.h>
  5 #include <sys/wait.h>
  6 #include <sys/type.h>
  7 
  8 int main()
  9 {
 10     pid_t id = fork();
 11     assert(id >= 0);
 12     if(id == 0)
 13     {
 14         //子进程
 15         printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
 16         exit(10); //随意设置
 17     }
 18 
 19     //父进程
 20     sleep(2); //让子进程先运行完
 21     int status = 0;
 22     pid_t ret = wait(&status);
 23     printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));                                                                                 
 24     if(id > 0)
 25     {
 26         printf("wait success: %d\n", ret);
 27 
 28     }
 29 }

在这里插入图片描述

  • waitpid()
#include <sys/wait.h>
#include <sys/type.h>
pid_t waitpid(pid_t pid, int* status, int options);

pid:如果pid>0等待的就是一个子进程,如果pid=-1,那么就是等待所有的子进程。
status:记录回收进程的退出信息。
options:一般选择是阻塞还是非阻塞两个状态。(下面会说啥是阻塞)
返回值返回回收的子进程pid,如果子进程还没退出返回0,如果waitpid调用失败返回-1。


稍稍修改一下代码

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <assert.h>
  5 #include <sys/wait.h>
  6 #include <sys/types.h>                                                                                                                                      
  7 
  8 int main()
  9 {
 10     pid_t id = fork();
 11     assert(id >= 0);
 12     if(id == 0)
 13     {
 14         //子进程
 15         printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
 16         exit(10);
 17     }
 18 
 19     //父进程
 20     sleep(2); //让子进程先运行完
 21     int status = 0;
 22     pid_t ret = waitpid(id, &status, 0);// 0 代表阻塞式等待 WNOHANG代表非阻塞式等待
 23     printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));
 24     if(id > 0)
 25     {
 26         printf("wait success: %d\n", ret);
 27 
 28     }
 29 }
  • 子进程退出的退出信息存放在哪?
    在这里插入图片描述

  • 补充:宏函数
    WIFEXITED(status)。W-wait,wait是否退出,若正常退出子进程,返回真。
    WEXITSTATUS(status)。查看进程退出码,若WIFEXITED非零,提取子进程退出码。

    //是否正常退出
    if(WIFEXITED(status))
    {
    	// 判断子进程运行结果是否ok
    	printf("exit code: %d\n", WEXITSTATUS(status);
    }
    

3.2 阻塞和非阻塞

前面wait相关的测试都是在子进程已经退出的前提下进行的。

阻塞和非阻塞很简单,将waitpid设置为阻塞后如果子进程没有退出,那么父进程就会一直等待,直到子进程退出。

父进程查看子进程状态,子进程没有退出,父进程立即返回去执行其它任务,这一次的过程叫做非阻塞。(而父进程多次来回确认子进程有没有退出的过程称为轮询)

一个测试非阻塞的程序

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/wait.h>
  4 #include <sys/types.h>
  5 #include <stdlib.h>
  6 #include <string.h>
  7 #define NUM 10
  8 
  9 typedef void (*func_t)();
 10 func_t handlerTask[NUM];
 11 
 12 void task1()
 13 {
 14     printf("do task1!\n");
 15 }
 16 
 17 void task2()
 18 {
 19     printf("do task2!\n");
 20 }
 21 
 22 void loadTask()
 23 {
 24     memset(handlerTask, 0, sizeof(handlerTask));
 25     handlerTask[0] = task1;
 26     handlerTask[1] = task2;
 27 }
 28 
 29 int main()
 30 {
 31     pid_t id = fork();
 32     if(id == 0)
 33     {
 34          while(1)
 35          {
 36              //child
 37              int cnt = 3;
 38              while(cnt)
 39              {
 40                  printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
 41                  sleep(1);
 42              }
 43 
 44              exit(10);
 45         }
 46     }
 47     //parent
 48     loadTask();                                                                                                                                                                                                                                                           
 49     int status = 0;
 50     while(1)
 51     {
 52         pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞 -> 子进程没有退出, 父进程检测时候, 立即返回.
 53         if(ret == 0)
 54         {
 55             //waitpid调用成功 && 子进程没退出
 56             //子进程没有退出,我的waitpid没有等待失败,仅仅是检测到了子进程没退出
 57             printf("wait done, but child is runing , parent do :\n");
 58             int i = 0;
 59             for(i = 0; handlerTask[i]!=NULL; ++i)
 60             {
 61                 handlerTask[i]();
 62             }
 63         }
 64         else if(ret > 0)
 65         {
 66             //waitpid调用成功 && 子进程退出了
 67             printf("wait success, exit code: %d, sig: %d\n", (status >> 8), (status & 0x7F));
 68             break;
 69         }
 70         else
 71         {
 72             //waitpid调用失败
 73             printf("waitpid call failed\n");
 74             break;
 75         }
 76         sleep(1);
 77     }
 78 
 79     return 0;
 80 }

在这里插入图片描述
非阻塞不会占用父进程所有精力,可以在轮询期间干点别的!

本章完~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值