当前位置:   article > 正文

Linux操作系统~进程fork到wait到底怎么用?

Linux操作系统~进程fork到wait到底怎么用?

目录

1.fork()

(1).概念

(2).fork的写时拷贝

(3).fork常规用法

2.进程终止

(1).进程退出场景/退出码

(2).进程常见退出方法

1).exit函数

2)._exit和exit的区别

3).进程退出后,OS层面做了什么呢?

3.进程等待

(1).概念

(2).为什么要让父进程等待

(3).wait

(4).waitpid

(5).获取子进程status

1.子进程的退出有两种情况

2.位运算获取子进程的退出状态(退出码)和终止信号

3.宏函数获取子进程的退出状态(退出码)和判断是否收到终止信号

Q:为什么我们echo可以查到各个进程的退出码?

理解waitpid调用的流程和status

4.第三个参数options-设置等待方式

        1.阻塞等待和非阻塞等待

        2.如何设计一个非阻塞等待的轮询机制


1.fork()

(1).概念

        在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

  1. #include <unistd.h>
  2. pid_t fork(void);
  3. 返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  1. 分配新的内存块和内核数据结构(pcb+地址空间mm_struct+页表)给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度


(2).fork的写时拷贝

        通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

        父进程和子进程的虚拟地址空间以及页表都是各自有一份的,但是对应数据虚拟地址映射到的物理地址是同一个。

        默认所有的数据都是只读的,如果有子进程想要写某个数据(页表项100),会发生写时拷贝,就会发生缺页中断,先拷贝一份数据到新的物理地址空间,然后修改子进程页表的映射关系,让子进程对应数据的虚拟地址映射到新的物理地址,然后中断结束,让子进程来修改数据。

Q:如何保证进程独立性?(如何保证父子进程间的独立性)——写时拷贝


(3).fork常规用法

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

2.进程终止

(1).进程退出场景/退出码

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止(此时程序崩溃了,退出码也就变得没有意义了!我们echo出来也没有意义)

我们的main函数返回0就是表示代码运行完毕,结果正确。(这个返回值就是进程的退出码

我们可以通过echo $?查看最近一次进程退出时的退出码

  1. [zebra@VM-8-12-centos test6]$ echo $?
  2. 0

        第二次第三次echo的时候输出的是前一次echo这个进程的退出码。实际上每个命令执行都是一个进程,都会有一个退出码,这个退出码就是main函数的返回值,每个进程都有一个main函数

不同的错误码对应不同的错误信息,当然我们也可以自己设计一种错误码和错误信息的映射。 

(2).进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

1. 从main返回(代表进程退出,从函数返回也一样,叫做函数返回)

2. 调用exit

3. _exit

异常退出:

ctrl + c,信号终止

1).exit函数

  1. #include <unistd.h>
  2. void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值(status有很多宏值,比如EXIT_SUCCESS)

  • 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

e.g.:我们就算不加上\n,最后main return或者程序exit的时候也会把缓存区中的内容刷新到显示器上面。

  1. printf("hello,zebra");
  2. sleep(5);
  3. exit(EXIT_SUCCESS);
  4. return 0;

2)._exit和exit的区别

_exit则是强制终止进程,不会进行进程后续的收尾工作,比如刷新缓冲区。

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

1. 执行用户通过 atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

在这里也变相说明了我们所谓的缓冲区是用户层面的缓冲区,不再操作系统层面


3).进程退出后,OS层面做了什么呢?

        系统层面,少了一个进程: free PCB,free,mm_struct,free页表和各种映射关系,代码+数据申请的空间也要释放掉掉


3.进程等待

(1).概念

        父进程执行fork之后,会创建子进程,子进程一般都是要帮助父进程完成某种任务,此时父进程需要通过wait/waitpid等待子进程退出。

(2).为什么要让父进程等待

1.通过获取子进程退出的信息,可以得到子进程的执行结果

2.既然父进程要拿到子进程的执行结果,也就需要保证父进程在子进程之后退出,使用wait可以保证时序问题,即让子进程先退出,父进程后退出。

3.子进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放子进程占用的资源。

(3).wait

  1. #include<sys/types.h>
  2. #include<sys/wait.h>
  3. pid_t wait(int*status);

返回值:

成功返回被等待进程pid,失败返回-1。

参数:

输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

(4).waitpid

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

当正常返回的时候waitpid返回收集到的子进程的进程ID;

如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid:

Pid=-1,等待任意一个子进程。与wait等效。(如果这里给的pid和子进程对不上,会等待失败

Pid>0.等待其进程ID与pid相等的子进程。

  • status:(输出型参数,父进程拿到什么status结果,一定和子进程如何退出强相关

为了确定是三种退出情况中的哪一种

  WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

  WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

说明:

  1. 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  2. 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  3. 如果不存在该子进程,则立即出错返回

(5).获取子进程status

  1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  2. 如果传递NULL,表示不关心子进程的退出状态信息。
  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  4. status不能简单的当作整形来看待,可以当作位图来看待(只看status低16比特位):

1.子进程的退出有两种情况

  • 一种是正常终止(结果正确或者不正确)
  • 一种是异常终止(进程因为异常问题,收到了某种信号)

        所以我们可以通过判断是否收到终止信号(0-6,低7位)来判断进程是否是正常终止,如果是正常终止(低7位全是0),我们再去看退出状态(8-15,8个比特位)

2.位运算获取子进程的退出状态(退出码)和终止信号

        如果我们要获取8-15这8位退出状态(退出码),可以让status右移8位,然后和000...000 1111 1111(也就是(status>>8)&0xFF)&一下(因为status是32位的,右移8位后,我们要把前面的24位置0)

        如果我们要获取0-6这7位终止信号,可以让status&0x7F(我们只需要获取低7位就行)

        如果正常退出,退出信号都是0;如果异常退出,退出信号就不是0,此时前面的退出状态也就没有意义

3.宏函数获取子进程的退出状态(退出码)和判断是否收到终止信号

  1. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。判断进程是否收到终止信号
  2. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

        首先我们需要判断进程是否收到退出信号,如果收到了,那就是异常退出的;如果没有收到退出信号,那就是正常退出,退出码也就有效了

Q:为什么我们echo可以查到各个进程的退出码?

        bash是命令行启动的所有进程的父进程。bash是通过wait方式得到子进程的退出结果,所以我们echo $?能够查到子进程的退出码

理解waitpid调用的流程和status

        waitpid是一个系统调用接口,肯定是用户来调的。用户调用waitpid让父进程等待子进程退出并变成僵尸状态,然后从进程的pcb中获取exit_code和signal,组合起来放到status中,父进程中的status和用户层的status也就都发生了变化,用户就可以看到进程的退出码和终止信号了。(实际上这个status是一个指针,作为一个输出型参数,所以在操作系统内部修改status会直接影响用户层的status)

 

4.第三个参数options-设置等待方式

WNOHANG: 设置等待方式为非阻塞

若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

1.阻塞等待和非阻塞等待

首先都是等待的一种方式,都是父进程在等子进程退出(子进程退出是一种条件)

阻塞的本质:其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态

返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度

  • 阻塞等待:父进程调waitpid这个接口等待子进程完成时,把父进程放在等待队列里面,等到子进程退出后,再把它从等待队列里面拿出来。
  • 非阻塞等待:父进程不会被放入等待队列,而是时不时地多次调用waitpid,直到子进程结束。

2.如何设计一个非阻塞等待的轮询机制

0:阻塞等待

WNOHANG: 设置等待方式为非阻塞

全称是which no hang,表示不会被hang住

我们进行非阻塞等待时,需要考虑三种情况,并进行基于非阻塞等待的轮询方案

每次循环调用waitpid,通过返回值判断子进程是否退出

情况一:这次查询子进程还没退出,父进程做自己的事情

情况二:这次查询子进程退出了

情况三:调用waitpid出错,返回错误信息

  1. while(1){
  2.     pid_t ret = waitpid(id, &status, WNOHANG);
  3.     if(ret == 0){
  4.         //子进程没有退出,但是waitpid等待是成功的,需要父进程重复进行等待
  5.         printf("Do father things!\n");
  6.     }
  7.     else if(ret > 0){
  8.         //子进程退出了,waitpid也成功了,获取到了对应的结果
  9.         printf("fahter wait: %d, success, status exit code: %d, status exit signal: %d\n", ret, (status>>8)&0xFF, status&0x7F);
  10.         break;
  11.     }
  12.     else{ //ret < 0
  13.         //等待失败
  14.         perror("waitpid");
  15.         break;
  16.     }
  17.     sleep(1); //每隔一秒进行一次轮询
  18. }

阻塞就是调用waitpid,如果子进程没退出,就干等

非阻塞就是调用一次waitpid,然后继续执行后面的代码,需要自己设计一个基于非阻塞等待的轮询方案

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/210694
推荐阅读
相关标签
  

闽ICP备14008679号