【Linux】进程控制

目录

进程创建

fork函数初识

fork函数的返回值

写实拷贝

fork常规用法 

fork调用失败的原因

进程终止 

进程终止的常见方式

进程常见的退出方法

1)return退出

2)exit

3)_exit

return、exit和_exit的区别

进程等待

进程等待的必要性

获取子进程的status

进程等待的方法

wait方法

waitpid方法


进程创建

fork函数初识

在linux中,程序执行到fork函数将创建一个新的进程:子进程,而原来的进程称为父进程。

函数原型:pid_t fork(void)

进程调用fork,当控制转移到内核中的fork代码后,内核做了什么呢?

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

面试题:请描述一下,fork创建子进程,系统都做了什么?

创建子进程也就是创建新的进程,那么做了一下内容:创建了对应的PCB、虚拟地址空间、页表并将自己进程中的代码和数据加载到物理内存,构建映射关系,然后将该进程的PCB放入到运行队列里等待OS或者调度器调度,一旦CPU开始调度这个进程,那么此时就可以通过虚拟地址空间和页表找到该进程的相关代码,然后从上至下开始执行。

当一个进程调动fork之后,就有了两个进程,且共用相同的代码,例如:

结果:

 可以看到fork之前代码被执行了一次,而fork之后的代码被执行了两次,也就说明在执行fork之前的代码,子进程是不会执行的,也就是fork之前只有父进程能够执行,而在fork之后,有了子进程,也就是有了两个执行流,父进程子进程且代码共享。注意:fork之后,父子进程谁先执行完全有调度器决定。

fork函数的返回值

fork函数在调用成功后有两个返回值,1)子进程返回0;2)父进程返回子进程的PID;

调用失败返回 -1;

为什么父进程返回子进程的pid,而子进程就要返回0呢?

因为对于子进程而言父进程永远只有一个,但是父进程可以有多个子进程,并且父进程需要进程标识子进程,因为父进程创建子进程无非就是让子进程完成一些事情,而对于子进程完成的怎么用父进程是要能够获得信息,这时就要用子进程的pid来查看子进程任务完成的怎么样了;

我们可以通过上面的代码观察到 id = fork();那么问题来了

 一条语句,一个变量,一个函数为什么有两个返回值呢?

我们上面说了fork会创建子进程,为了创建子进程,fork函数内部会执行一系列的操作,包括创建了对应的PCB、虚拟地址空间、页表等等,子进程创建完成之后,系统还有将子进程进程的PCB放入到运行队列里等待,此时子进程便创建完成,也就是说在fork函数返回之前子进程已经创建好了所以在fork内部父子进程会各自执行自己的return语句,所以就会有两个返回值。 

了解上面的内容之后,我们看一个问题:fork之后父子进程代码共享是fork函数后面的代码共享,还是所有的代码都共享?答案是所有。

原因:

1)我们的代码会汇编之后会有很多行代码,而且每行代码加载到内存之后都有对应的地址;

2)因为进程有随时被中断的可能(可能进程并没有执行完),下次回来,还必须从以前的位置继续运行(不是最开始),这就要求CPU必须随时记录下来当前进程执行的位置,所以CPU内有相应的寄存器数据记录当前进程的执行位置(寄存器在CPU内只有一份,但是寄存器数据可以有多份)也就是进程的上下文数据,创建的时候,要不要将这个上下文数据给子进程内呢?虽然父子进程各自调度,各自修改EIP(寄存器),但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码

写实拷贝

创建子进程,给子进程分配对应的内核结构,这个必须是子进程自己独有的,因为进程要具有独立性。理论上子进程也要有自己的代码和数据,可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据,所以子进程只能使用父进程的代码和数据,对于代码来讲都是不可被写的,也就是只能读不能写,所以在这个层面的来讲父子共享没有问题。但是对于数据而言,他可能被修改,所以必须进行分离。

那对于数据而言;子进程创建时,将数据进行直接拷贝吗?

如果直接拷贝,会出现这样的问题,拷贝了子进程根本不会用到的数据,或者即便是用到也只是读取,并不写入,这样一来,拷贝的这些数据就浪费了空间,那我们可以得出结论,创建子进程不需要将不会访问的或者只会读取的数据拷贝一份。但是,有些数据你还必须要拷贝,哪些数据呢?将来被父或子进程写入的数据。那么问题又来了?我们通过上面已经知道在调用fork函数完成之前子进程就已经创建完成,创建时它无法得知哪些数据是将来要进行写入的(即便是OS也无法提前得知);另外,就算你提前得知哪些数据要进行写入,在创建子进程时就将这些数据拷贝到自己内存中,但是这些数据你并不会立即使用,只有当进程执行到它时才会使用,那这样看的话,提前拷贝也是浪费当前的空间。所以,基于以上情况OS选择了写时拷贝技术来进行父子进程的数据分离

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

 在子进程写入之前,数据与父进程共享,当子进程准备写入时,再开辟空间,这样就做到了数据分离。

总结:

OS为什么要选用写时拷贝技术,对父子进程分离?

  • 用的时候再分配是高效使用内存的一种表现;
  • OS无法在代码执行前预知那哪些空间会被访问

fork常规用法 

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

fork调用失败的原因

  •  系统中有太多的进程

  • 实际用户的进程数超出了限制

进程终止 

进程终止的常见方式

1)代码跑完,结果正确;

2)代码跑完,结果不正确;

3)代码没跑完,程序崩溃;

对于1),2)两种情况代码都跑完了,那我该怎样分辨结果是否正确呢

之前我们编写的代码,在main函数中总是以return 0 结尾,那有没有思考过main函数返回值的意义,又为什么总是返回0呢

其实main函数的返回值是进程的退出码,而退出码表示着是否正确返回,也并不是总是0,也可能是其他值。但是 0代表着正确,而非0代表不正确。这样设置的原因在于非零值有无数个,不同的非零值就可以代表不同的错误原因,这在我们程序运行结束之后结果不正确也方便定位错误的原原因。到这里我们就该知道main函数返回值的意义在于返回给上一级进程,用来评判该进程执行结果用的。但是,当程序崩溃时退出码将不再有意义,一般而言退出码对应的return语句没有被执行。

一般情况下我们可以用echo $?查看最近一次执行完毕进程的退出码,以下面的代码为例:

 除了0之外还有哪些退出码呢,上面讲过除了0之外的退出码都代表着错误信息,在C语言中我们可以调用strerror函数来查看对应的错误信息:

注意:退出码都有对应的含义,帮助用户确定执行失败的原因,而这些退出码具体代表的含义是认为规定的,不同的环境下相同的退出码可能会不同。

进程常见的退出方法

1)return退出

在main函数中我们常用的退出方式就是return

例如:

结果:

2)exit

exit在代码的任何地方调用都表示直接终止进程。

3)_exit

与exit基本相同,不同的是exit会刷新缓冲区,_exit则不会刷新缓冲区 。

实例:

结果: 

 

return、exit和_exit的区别

执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。

而exit执行时会调用_exit,只是在调用_exit之前,还做了其他的工作:

  • 执行用户通过atexit或者on_exit定义的清理函数
  • 关闭所有打开的流,所有的缓冲数据均被写入
  • 调用_exit

进程等待

进程等待的必要性

  • 子进程退出,父进程不管不顾,就可能造成僵尸进程的问题,最终导致内存泄漏
  • 进程一旦变成僵尸进程,那就会无法被杀死,kill-9也不行,因为谁也没办法杀死一个已经死掉的进程
  • 另外,大多数情况下,创建子进程是要子进程完成工作的,那么父进程派给子进程的任务完成的如何,我们就需要知道,如:子进程运行完成,结果是对还是错或者是否正产退出。
  • 父进程通过进程等待的方式,回收子进程资源,获得子进程退出信息(这就是为什么要进行进程等待的原因)

获取子进程的status

下面要将的wait和waitpid函数都有一个参数status,这是一个输出型参数,有操作系统填充,标志子进程退出的状态信息。如果传递NULL,表示不关心子进程退出状态信息,否则的话,操作系统会根据该参数,将子进程的退出信息反馈给父进程。另外status不能简单的当做整体来看待,不同的位组合代表不同的信息,故应将他当做位图看待,基体细节如下图(只研究status的第16个比特位);

 

 我们可以通过位操作获取退出码(次八位)和终止信号

int exitCode = (status>>8) & 0xFF;
int stopSignal = (status & 0x7F)

 也可用宏来查看:

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

退出状态:WEXITSTATUS(status);若WIFEXITD(status)非零,提取子进程退出码(查看进程的退出码)

进程等待的方法

wait方法

头文件:

#include<sys/tpyes.h>

#include<sys/wait.h>

原型:

pid_t  wiat( int* statistics>

返回值:

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

参数:

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

waitpid方法

头文件:

#include<sys/tpyes.h>

#include<sys/wait.h>

原型:

pid_t  waitpid(pid_t pid, int *status, int options)

返回值:

  • 当正常返回时,waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WHOHANG,而调用waitpid发现没有已经退出的子进程可以收集,则返回0;
  • 如果调用中出现错误,则返回-1,这时error会被设置成相应的值以提示错误所在;

参数:

pid

  • pid =-1,等待任一个子进程,与wait等效。
  • pid > 0;等待其进程ID与pid相等的进子程。

status:

  • WIFEXITED(status)若为正常终止子进程返回的状态,则为真(查看进程是否正常退出);
  • WEXITSTATUS(status)若WIFEXITED(status)非零,提取子进程退出码(查看进程的退出码)

options:

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

0,表示阻塞等待

注意:

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

如何理解阻塞等待和非阻塞等待?

举一个例子来理解:

假如你给你的朋友打电话,你的朋友接通了,但是他现在有事情要忙,他给你说:你先别挂电话,我忙完这一会再说 ,这时你就只能在这等着不能做其他的事情,这时你就处于堵塞状态。但是接通电话时,他给你说他在忙,你说那我先挂电话,一会在给你打,这时你可以做一些自己的事情,这时你就处在非阻塞等待。

waitpid原理:

 通过以上的学习,可能会有一个疑问;

1.父进程为什么要通过wait/waitpid来拿到子进程的退出结果?直接用全局变量不应吗?

答案是不行的,我们知道父子进程有独立性,数据会发生写时拷贝,父进程无法拿到相关信息。

2.既然进程是独立的,退出码不也是子进程的数据吗?父进程又凭什么拿到呢?wait/waitpid究竟做什么?

子进程退出,父进程没有回收之前,子进程处于僵尸状态,僵尸状态下也保留着该进程的PCB信息,task_struct里面保留了任何进程退出时的退出结果,而wait/waitpid本质上就是读取了子进程的task_struct结构,获得信息。

阻塞等待方式:

 结果:

 非堵塞等待方式:

 结果:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值