【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编写的程序,同样,也可以用这两个语言编写的程序去调用其他语言编写的可执行程序。

相关推荐
dntktop2 小时前
内嵌编辑器+AI助手,Wave Terminal打造终端新体验
运维
Peter_chq3 小时前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
太阳风暴3 小时前
Ubuntu-修改左Alt和Win键位置关系
linux·ubuntu·修改键盘·键盘映射
kaiyuanheshang4 小时前
docker 中的entrypoint和cmd指令
运维·docker·容器·cmd·entrypoint
wanhengwangluo4 小时前
裸金属服务器能够帮助企业解决哪些问题?
运维·服务器
Python私教5 小时前
除了 Docker,还有哪些类似的容器技术?
运维·docker·容器
titxixYY5 小时前
SElinux
linux·运维·服务器
聚名网6 小时前
手机无法连接服务器1302什么意思?
运维·服务器·智能手机
香吧香6 小时前
getent使用小结
linux
代码欢乐豆7 小时前
软件工程第13章小测
服务器·前端·数据库·软件工程