【Linux】:进程控制1(创建、终止、等待)

目录

1.进程创建

2.进程终止(退出)

2.1 什么是进程终止

2.2 进程退出的场景(原因)

2.3 进程退出码

2.4 错误码errno

2.5 进程常见的退出方法

正常终止

从main函数返回

调用库函数exit

系统接口_exit

3.进程等待

3.1  进程等待的概念

3.2 僵尸进程为什么要被回收

3.3 wait和waitpid

wait

waitpid 

参数:status

宏WIFEXITED、WEXITSTATUS

3.4 等待失败

3.4 进程等待的原理 

3.5 非阻塞轮询

1.进程创建

在Linux系统中,我们一般用系统调用接口fork创建进程。

当前进程调用fork()创建一个子进程,当前进程则是该子进程的父进程。一个父进程可以有多个子进程,但一个子进程在同一时间段只能有一个父进程(父进程可以改变)。

更详细的信息请查看:浅析fork()和底层实现 - tp_16b - 博客园 (cnblogs.com)

注意:当系统中有太多的进程或实际用户的进程数超过了限制 ,fork调用可能会失败,这时需要释放进程资源。

2.进程终止(退出)

2.1 什么是进程终止

结束一个进程的生命可以分为两个步骤:进程退出和进程销毁。进程退出主要是释放进程的资源,使进程成为僵死(TASK_ZOMBIE)状态;进程销毁主要是通过父进程,释放僵死进程的各种信息。

当前进程被设为TASK_ZOMBIE僵死状态后会向其父进程发生SIGCHLD信号,父进程收到SIGCHLD信号后,才会销毁僵死状态的子进程,。

父进程通过调用wait()waitpid()函数等待SIGCHLD处理僵死进程

 进程退出和销毁过程分析 - 66Ring's Blog

终止进程的结果如下:

  • 进程中的任何剩余线程都标记为终止。
  • 将释放进程分配的任何资源。
  • 所有内核对象都已关闭。
  • 进程代码将从内存中删除。
  • 设置进程退出代码。
  • 进程对象已发出信号。

终止进程 - Win32 apps | Microsoft Learn

2.2 进程退出的场景(原因)

根据程序是否运行完毕,我们可以把进程划分为运行完毕正常退出运行异常非正常退出

对于正常退出的进程,我们又可以根据程序运行的结果,划分为结果正确和结果不正确。

那么我们是如何判断退出的结果正不正确呢?以及不正确有哪些原因呢?

2.3 进程退出码

任何进程退出时,都会留下退出码,操作系统根据退出码可以知道进程是否正常运行。退出码是0到N的整数(不同的操作系统N可能不同),通常0表示正常退出,其他数字表示不同的错误。

在Linux系统中,我们可以用“$?”获取上一个进程的退出码。我们编写一个程序,main函数的返回值设置为3,我们执行该源文件生成的可执行文件,可以用“echo”将“$?”的内容打印到终端。退出码是一个整数,不同的整数对应着不同的退出信息,我们可以用strerror打印退出码对于的退出信息。

CentOS7系统中一共有134个退出码。 

我们用ls列出一个当前目录下不存在的文件yls610,发现终端显示报错,报错信息刚好是退出码2对应的信息,我们用“echo $?”输出上一个进程返回的退出码也是2,

 再“echo $?”输出,结果是0,这是为什么呢?因为上一个“echo $?”是success退出,虽然echo是内建命令,不创建进程,但它还是会调用接口。

2.4 错误码errno

C语言标准库errno.h中有个宏errno,该宏定义为一个int类型的左值, 存储最近一次函数调用产生的错误码,也就是最近一次返回的错误码。

在纯C语言的环境下,我们可以利用这个errno和strerror,用strerror读取错误码对应的错误信息,并将错误码作为进程的退出码,让父进程也知道错误信息。我们编写一个程序,验证一下,

我们讨论了程序运行完毕的情况。但是对于一个进程,我们一般先判断程序是否异常,再判断程序运行的结果是否正确,因为程序出现了异常,程序就无法正常运行,就难以运行完毕(不会产生错误码和退出码),这时我们又该怎么办呢?(两点:为什么发生异常,以及是什么样的异常)

我们知道,C语言中,我们不能对野指针解引用,如果访问野指针,可能抛异常,这是因为访问野指针,本质上是对虚拟地址的访问,但这个虚拟地址在页表中并没有建立映射关系,或者建立了映射关系,但权限为只读;C语言中我们不能对0整除,整除时会触发浮点异常,状态寄存器会发出对应的信号。

而这些异常问题,都是通过发出信号告诉操作系统使进程提前退出。 

状态寄存器_百度百科 (baidu.com)

浮点异常 - 活着的虫子 - 博客园 (cnblogs.com)

 如何证明异常是发送信号使进程退出的呢?

我们之前学习了用“kill -9”指令杀死一个进程,“kill -9”就相当于一个信号,信号8SIGFPE是“Floating point exception”,浮点数异常;信号11SIGSEGV是“Segmentation fault”,段错误,我们可以用信号8和信号11使一个进程退出。执行该文件生成的可执行文件,在root权限下用kill -8杀死该进程。

用kill -11杀死进程, 

2.5 进程常见的退出方法

正常终止

  • 从main函数返回
  • 调用库函数exit

在main函数中,exit中的参数status表示退出码;在进程中调用exit,表示退出该进程。

  • 系统接口_exit

参数:status 定义了进程的终止状态,父进程通过wait来获取该值。

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

 exit和_exit都可以终止进程,他俩有什么区别呢?

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

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

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

3. 调用_exit 

由于_exit不刷新缓冲区,我们可以得出一个结论,缓冲区不在内核区,在用户区,因为操作系统不做浪费资源的事。

3.进程等待

3.1  进程等待的概念

狭义的进程等待是指,当子进程退出,进入僵尸状态,父进程会对子进程的状态进程检测并回收,如果子进程没有进入僵尸状态,父进程会等待子进程进入僵尸状态再回收。

3.2 僵尸进程为什么要被回收

基本概念: 

 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。僵尸进程无法被“kill -9”。

背景: 

父进程创建子进程,目的就是让子进程执行自己创建的任务,任务的执行情况最终需要父进程知道。 

unix提供了一种机制保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。

冲突:

但这样就导致了问题,如果父进程不调用wait / waitpid的话, 那么保留子进程的那段信息就不会释放,一直不释放,就会造成内存泄漏。 进程ID - 维基百科,自由的百科全书 (wikipedia.org)

所以,为什么要有进程等待?

一是因为父进程要识别子进程执行任务的最终情况;

二是因为不进行“进程等待”(调用系统接口wait或waitpid回收子进程)会造成”内存泄漏“。

3.3 wait和waitpid

wait

 pid_t wait(int*status);

返回值:

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

参数:

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

我们编写一个程序,

执行,

可以看到,子进程15580打印五次后退出,变为僵尸进程,最终被父进程调用wait成功回收。

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

那么wait和waitpid是如何获取子进程的退出信息呢?

 我们是通过输出型参数status获得进程退出的结果。

输入型参数是指这个参数的值已知,由函数外传给函数里使用.输入型参数是指这个参数的值未知,要通过函数传出来。

我们编写一个可执行文件,来检测一下,

 对于这个问题,我们必须认识一下status,

参数:status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。非空,操作系统会将子进程的退出信息用该参数反馈给父进程。

由于status返回的是进程退出信息,根据进程退出的场景,我们可以将退出信息分类两类,一类是异常退出,一类是运行完毕后退出。

由于进程中异常在先,一般程序执行中出现了异常,就不会运行完毕,而status是一个整形,有32位,采用位图的算法思想,不同的bit位代表不同的退出原因,其中后低7位表示进程异常的信息,低7位全为0,表示进程没有异常,第低8位是core dump标志;次低8位(15-8)表示程序正常执行完毕返回的错误码,如果没有出现异常(低8位全为0),且次低8位全为0,表示程序运行结果正确,上面子进程的错误码(退出码)为1,status的后16位为0000 0001 0000 0000,转换为十进制就是256。具体细节如下图(只研究status低16比特位)

核心转储 - 维基百科,自由的百科全书 (wikipedia.org)

关于上面的程序,有些同学可能有疑惑,为什么要用wait的参数status呢?我们直接将status定义成全局变量不行吗?exit返回全局变量不可行吗?

不可行,因为进程具有独立性,进程之间的数据、代码、PCB实例、内存空间等都是相对独立的,进程数据之间的传递一般都要通过调用系统接口实现。 

我们将上面代码中第二个if语句改为:

重新生成可执行文件并执行, 

退出码就是“1”了。

我们编写一个“对空指针”解引用,看看打印结果,

在子进程中整除0,

宏WIFEXITED、WEXITSTATUS

如果用"status&0x7F"表示是否异常,“(status>>8)&0x7F”表示错误码,一些用户可能看不懂,所以标准库中定义了两个宏WIFEXITED、WEXITSTATUS,分别代表这两个运算,

我们用这两个宏代替移位运算,

 执行,

结果无误。

3.4 等待失败

什么时候父进程会等待失败呢?(waitpid返回值为-1)

父进程等待到的进程不是它想等待的进程,父进程的儿子是张三等来的却是李四,父进程就会等待失败。我们看下执行结果,

3.4 进程等待的原理 

父进程通过且必须通过操作系统管理的调用接口wait或waitpid获取子进程的PCB实例中的成员exit_code和exit_signal。

上面我们演示的“进程等待”,全是阻塞式等待,父进程等等待期间,只能单一地等待子进程退出变为僵尸进程,不能进行或参与其他任何操作。这种等待效率低,“我等的时候啥事都不能干,手机都不能刷”,但实现简单。还有一种等待方式,就是隔一段时间询问一次子进程是否退出了,没退出父进程就去干自己的事,不会一直等待子进程退出。这种等待方式叫做非阻塞轮询。

3.5 非阻塞轮询

waitpid函数的第三个参数option,设置为0就表示“阻塞等待”,设置为“WNOHANG”就表示非阻塞等待。

夯住(Hang)是指程序仍在运行,卡在某个方法调用上,没有返回也没有异常抛出;卡住时间从几秒到几小时不等。

 

子进程和父进程在交叉打印。如果我们把WNOHANG改为0,就是阻塞等待, 这样父进程会在子进程运行完才会继续运行,

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值