fork,wait等基本系统调用

这篇文章主要通过一些题目来巩固一些基础知识,熟悉一些底层原理

先来放一些基础,再来一些题目吧

PCB存放

每当我们创建一个新进程,有很多信息都需要记录,比如PID,权限,用户组,进程之间的关系,进程状态等等,他们都将被保存在一个数据结构当中,叫做task_struct,就是Process Control Block

我们所谓的PCB主要存在了内核中,内核通过控制PCB来达到控制进程。

写时复制技术copy-on-write

内核只为新生成的进程分配虚拟空间,而物理空间会与父进程共同享用,但某一个线程真正要修改时,才会复制对应的页面内容,并在子线程开启自己的物理页面,并修改虚拟空间与物理空间映射关系。

原因

当父进程创建子进程时,我们如果把父进程的物理空间完完整整复制过去,各种映射关系,页面内容,只会使得效率低下,所以我们如果把页面标记为可读,那么一旦父进程或者某个子进程修改了某一个内容,就可以copy到自己的物理空间,使数据分离。

而且,在可读范围的访问之内,父进程或者子进程的某个物理地址如果被缓存在了TLB,如果另一个进程访问了这个物理地址,那么也可以很快的得到自己想要的结果,包括L1,L2各种数据缓存,进程之间在可读范围内都可以共享,而如果直接复制并开辟自己物理内存,那么必须重新去内存获取对应页的内容,甚至可能要去交换文件获取自己的页内容,那就真的太浪费时间了吧,还浪费空间。

进程1修改页面C前后

fork函数

fork调用后,内核会为子进程分配对应的虚拟内存空间,同时它的正文段,数据段,堆栈端都是指向了父进程的物理空间,实现物理空间共享,并且内容可读,一旦某个进程修改这个共享的物理空间的内容,就会复制到子线程自己的物理空间。

我们知道虽然物理空间是共享的,但是PCB还是不能共享,内核还是会在内核区中分配给PCB一部分空间,这样内核就可以通过控制PCB来调度进程

如上图一样,返回的情况有三种情况,如果成功的话在父进程返回自己成的PID,子进程返回0

1:首先内核就需要在内核区中分配一定大小的空间,存储PCB还有新进程的内核栈

2:将父进程PCB完整copy给子进程PCB,两个进程PCB是完全一样的

3:需要将子进程的状态设置为不会马上运行,因为它还需要资源建立

4:需要将继承过来PCB中的各种域更新一下,比如亲属关系,不可能爸爸的爸爸还是自己的爸爸吧

5:将进程插入到进程链表和哈希表(这是进程存放方式),然后将子进程设置为可运行状态。

6:子进程和父进程平分剩余时间片

7:返回子进程PID,由父进程获取

 

wait函数

函数功能是:

父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止

僵尸进程和孤儿进程

僵尸进程:当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

孤儿进程:但是当父进程忘了用wait函数等待还没有终止子进程,子进程就会变成孤儿进程,交给init领养

返回值

一般wait()要与fork()配套出现:

         如果在使用fork()之前调用wait(),wait()的返回值则为-1

         正常情况下wait()的返回值为子进程的PID.

参数status

用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,
我们就可以设定这个参数为NULL,就像下面这样:
pid = wait(NULL);

如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,
 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,
以及正常结束时的返回值,或被哪一个信号结束的等信息。

由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个

WIFEXITED(status)

这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

WEXITSTATUS(status)

当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,

如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。

 

waitpid函数

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

返回值

返回值和wait函数一样。

参数说明

pid参数

  • pid > 0:表示等待进程ID为pid的子进程;
  • pid = 0:表示等待与调用进程同一进程组的任意子进程;
  • pid = -1:表示等待任意子进程;
  • pid < -1:表示等待所有子进程中,进程组ID与pid绝对值相等的所有子进程;

参数status

该参数存储的信息时按位存储的,我们没办法解析status的值,只能通过系统提供的宏去解析。这些宏安功能分可以分为两类:获取子进程状态和判断是非由相应信号产生;

  • 进程正常退出
    WIFEIXITED(status):正常退出,返回true;
    WEXITSTATUS(status):正常退出,获取进程退出状态;
  • 进程收到信号退出
  WIFSIGNALED(status): 被信号杀死,返回true;
  WTREMSIG(status):被信号杀死,返回杀死进程的pid;
  WCOREDUMOP(status):子进程产生core dump,返回true;
  • 进程收到信号停止
  WIFSTOPPED(status):收到相关信号,暂停执行,返回true;
  WSTOPSIG(status):如果子进程处于停止状态,该宏返回导致子进
  程停止的信号值;
  • 进程收到信号回复执行
WIFCONTINUED(status): 递送SIGCONT信号,子进程回复执行,返回true;

参数options

是一个位掩码,可以同时存在多个标志。当options的值为0时,行为和wait类似。

标志位:

  • WUNTRACE:关心终止子进程和因信号中断的子进程的信息(阻塞);
  • WCONTINUED:关系终止子进程和信号终止后由恢复执行的子进程(阻塞);
  • WNOHANG:指定的子进程没有发生变化,waitpid立即返回,返回值为0;出错返回时,返回值为-1,通过error=ECHILD和返回值来区分这两种情况。注意:error的值不会为EINTR,因为信号中断由WUTRACE来控制。

Linux内核不同导致的问题

http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L50:解释了Linux内核在2.6.32版本之后,父进程在fork之后,默认父进程先执行

其实之前的版本考虑到的是让子进程运行,如果先让父进程运行了,那么在父进程修改某个数据段的时候,写时复制技术就会使子进程要分配独立的物理页面了,而在子进程执行exec时,就会做无用功,而且还降低了效率。但是,从宏观来讲,这仅仅是一个原因而已。

在2.6.32版本后,可能Linux设计人员认为如果为了写时复制不造成效率降低的话而优先子进程,那么我为了TLB 和 cache可以加快页面的访问也可以优先运行父进程了,如果优先父进程,那么在父进程运行短短时间内,一定的物理页面会被缓存到TLB或者cache,而子进程是和父进程共享了一部分物理页面,这样子进程在访问时,可以加快速度,当然,这都是在没有写时复制的前提下。

以上优先父进程还是优先子进程都只是为了性能考虑,一旦某一优先进程执行完了,还是得乖乖听内核的调度。

我想大部分机器还是会在fork之后优先父进程执行一小段时间吧,除非你让父进程阻塞。


下面就来看看几道练习题

fork0

运行结果

这个题目很简单,但是也有可能会有一些疑问,先来看看进程流程图

我们执行多次了之后,可能有些机器总是是父进程先输出“Hello from parent”,而再输出子进程“Hello form Child”,有些机器总是先子进程先输出。这在之前说了,就是Linux内核版本不同的关系,在我的机器上,会为了TLB和Cache,优先父进程先执行,而有的机器可能为了优先不修改页,导致做无用功,效率低下,而先优先子进程。

fork1

这里要知道,当fork之后,就有了两个分支,一个是子进程,一个是父进程,对应的printf里面,子进程会执行++x,而父进程执行--x。

这个答案说明,父进程执行后,原本子进程和父进程共享同一块的物理内存,但是由于父进程通过--x修改了使x编程0,这时写时复制就起作用了,内核会在父进程修改之前为子进程开辟新的物理内存,将x的初始值存到子进程的物理内存中。这样在子进程执行的时候,看到的还是1,在++x之后就变成了2。

fork2

进程流程图这个样子,这个页不详细解释,对应的输出结果可能有好几种,但要知道一条线上的肯定是顺序执行,其他的不一定,一共输出一个L0,两个L1,四个Bye

fork3

这个无非就是比之前的那个稍微复杂点,但是看一下就应该知道,对应的输出一个L0,两个L1,四个L2,八个Bye,流程图就不多画了,就是上面那个图每个结尾多了两个分支

fork4

由进程图可以看出,一个L0,一个L1,一个L2,三个Bye,我的机器优先父进程,所以L0,L1总是在前面,其他的不确定

fork5

这个和fork4差不多,只是fork返回的条件判断不同,上面判断条件是父进程,这里就是子进程而已,结果也是一个L0,一个L1,一个L2,三个Bye。

fork6

 

当atexit登记了后,再执行fork,有了一个父进程和子进程,两个进程执行exit(0)后,先执行atexit登记的函数,所以有两个输出

fork7

父进程fork之后,执行了一个printf就一直陷在了while中,当调度给子进程执行时,就会执行printf(),并且直接退出

而父进程一致卡在那

fork8

看看进程,并没有子进程,而且父进程exit后,就会退出,它的所有子进程就没有了父亲,托孤给了init进程,这个进程并没有变成僵尸进程,而是一直在运行着,变成了孤儿进程,但是会一直在那里运行,所以需要使用kill -9杀死它。

这个就是和上面情况相反了,这个就是子进程僵死了

fork9

这个wait会等待子进程执行完毕,也就是说它进入了等待队列,当子进程执行完了后,就会进入就绪状态

fork10

这个呢,每个子进程都以非0的方式退出了,也就是不正常的退出,那么在父进程循环wait的时候,接受的都是不正常退出的子进程,那么使用WIFEXITED时就返回了子进程的退出是的状态码:100,101,102,103,104

fork11

这个其实和fork10类似,只是waitpid函数,等待的是pid[i],也就是指定了wait这个pid[i],而且这个循环是从4到0,那么输出顺序也是反着的

fork12

发布了66 篇原创文章 · 获赞 31 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43272605/article/details/102985935