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

目录

1.进程创建

2.进程终止(退出)

[2.1 什么是进程终止](#2.1 什么是进程终止)

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

[2.3 进程退出码](#2.3 进程退出码)

[2.4 错误码errno](#2.4 错误码errno)

[2.5 进程常见的退出方法](#2.5 进程常见的退出方法)

正常终止

从main函数返回

调用库函数exit

系统接口_exit

3.进程等待

[3.1 进程等待的概念](#3.1 进程等待的概念)

[3.2 僵尸进程为什么要被回收](#3.2 僵尸进程为什么要被回收)

[3.3 wait和waitpid](#3.3 wait和waitpid)

wait

waitpid

参数:status

宏WIFEXITED、WEXITSTATUS

[3.4 等待失败](#3.4 等待失败)

[3.4 进程等待的原理](#3.4 进程等待的原理)

[3.5 非阻塞轮询](#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,![](https://i-blog.csdnimg.cn/direct/e3063e2b50de41cdbe6d86e628e4a01a.png)我们执行该源文件生成的可执行文件,![](https://i-blog.csdnimg.cn/direct/1a8b469aced34ec08ed88ddf0bcef24e.png)可以用"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,就是阻塞等待, 这样父进程会在子进程运行完才会继续运行,

4.进程的程序替换

4.1 程序替换的概念

我们之前所创建的子进程,基本上都是继承了父进程的代码和数据。

当我们想要创建一个子进程去执行其他的任务,去完成独立于父进程的代码和数据的任务时,该怎么办呢?

我们只要替换子进程的代码和数据即可。

那么如何替换进程的代码和数据呢?

只要将新的程序和数据从磁盘(或其他存储介质)中读取到内存中,然后用它来替换当前正在运行的进程的代码和数据。

4.2 程序替换的原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

4.3 程序替换实例

我们可以看到,执行源文件生成的程序,就相当于执行指令"ls"。

4.4 程序替换接口

4.4.1 execl

l表示链表的意思,其中参数path表示要执行文件的绝对路径或相对路径,

4.4.2 execlp

p就表示环境变量PATH,execlp中第一个参数file表示要执行文件的文件名,这里不用写路径是因为execlp会自动到环境变量PATH中去寻找该文件所在路径,找到路径了,还要确定要执行的文件名,第二个"ls"就表示要执行的文件名。

实例演示:

4.4.3 execv

v表示vector,

path表示可执行文件的路径,argv是一个字符串指针数组,该数组第一个参数是要执行文件的名称,最后一个参数必须是NULL,中间是程序执行选项。

实例演示:

注意:

ls是一个可执行文件,文件中有main函数,main函数有参数,谁传给它的参数?

答案是execv函数,execv会将argv数组中除了第一个参数外都传递给ls的main函数。

在命令行中运行程序,所创建的进程都是bash的子进程,这些进程都是通过调用exec系列的函数执行的。

4.4.4 execvp

同理,execvp会在环境变量PATH中寻找参数file(可以写文件名,也可以写文件所在的绝对路径),

实例演示:

4.4.5 execle

在学习execle之前,我们先了解两个知识点,

1.用make生成多个文件:
2.在c程序中调用cpp程序

Linux系统中C++可执行文件的后缀可以写成" .cc "或" .cxx "。我们编写mycommand.c,在其main函数中通过execl调用otherExe.cpp生成的可执行程序otherExe,

我们用C语言编写的程序去调用CPP编写的程序,同样,也可以用这两个语言编写的程序去调用其他语言编写的可执行程序。

相关推荐
卑微的码蚁2 分钟前
服务器相关问题
运维·服务器
博洋科技4 分钟前
网站建设的服务器该如何选择?
运维·服务器·网站建设·保定响应式网站建设·保定h5网站建设·保定网站建设
人类群星闪耀时8 分钟前
服务器管理:从零开始的服务器安装与配置指南
运维·服务器
NiNg_1_2341 小时前
使用Docker Compose一键部署
运维·docker·容器
萠哥啥都行1 小时前
Linux安装Docker以及Docker入门操作
运维·docker·容器
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
小江湖19941 小时前
元数据保护者,Caesium压缩不丢重要信息
运维·学习·软件需求·改行学it
gopher95111 小时前
linux驱动开发-中断子系统
linux·运维·驱动开发
码哝小鱼1 小时前
firewalld封禁IP或IP段
linux·网络
鼠鼠龙年发大财1 小时前
【x**3专享】安装SSH、XFTP、XShell、ARM Linux
linux·arm开发·ssh