进程控制知识点

目录

1、进程终止

1.1、进程终止时,操作系统做了什么?

1.2、main函数返回值(比如return 0)

1.2.1、return 0这个0代表什么?

1.2.2、查看退出码的方式

1.2.3、退出码的意义是什么?

1.3、如何用代码终止一个进程

1.3.1、return+退出码

1.3.2、void exit(int status)函数

1.3.3、void _exit(int status)或者void _Exit(int status)函数

2、进程等待

2.1、为什么要进行进程等待?

2.2、进程等待的方法及其相关细节

2.2.1、pid_t wait(int* status)

2.2.2、pid_t waitpid(pid_t pid,int* status,int options)(包括对options的解释)

2.2.3、两个函数都有的参数:int*status(包含对退出码和终止信号的解释)

2.2.4、利用WIFEXITED(status)和WEXITSTATUS(status)两个宏也可以拿到退出状态,即退出码。

2.2.5、初始化status时明明等于0,为什么之后可以通过status拿到退出码和终止信息呢?

2.2.6、父进程可以通过wait或者waitpid获取子进程的退出信息,即退出码和终止信号,那可以使用全局变量获取子进程的退出信息吗?

2.2.7、进程具有独立性,那为什么父进程可以拿到子进程的退出码呢?两种wait函数做了什么?

3、进程替换

3.1、什么是进程替换?

3.2、如何实现?

3.3、程序替换时,由于要将程序加载进内存,那么此时会产生新的进程吗?

3.4、如何操作?

3.4.1、int execl(const char* path,const char* arg,。。。)函数及其细节

3.4.2、int execlp(const char* file,const char* arg,。。。)

3.4.3、int execv(const char* path,char* const argv[ ])函数

3.4.4、int execvp(const char* file,char* const argv[ ])

3.4.5、int execle(const char* path,const char* arg,。。。,char* const envp[ ] )

3.4.6、int execvpe(const char* file,char* const argv[ ],char* const envp[ ] )

3.4.7、int execve(const char* file,char* const argv[ ],char* const envp[ ] )

3.5、进程替换时,代码要不要写时拷贝?

3.6、进程替换会替换进程的环境变量吗?


1、进程终止

1.1、进程终止时,操作系统做了什么?

释放系统资源,即释放被进程的内核数据结构和代码以及数据占用的内存。

1.2、main函数返回值(比如return 0)

1.2.1、return 0这个0代表什么?

这个0代表退出码。

1.2.2、查看退出码的方式

在命令行可以输入echo $? 查看最近一次运行的进程的退出码,比如我在main函数最后写return 100,运行进程后再输入echo $?就会如下图打印100。但再次输入echo $?会打印0,为什么呢?因为echo $?本身也是一个进程,第一次echo $?后,它就是最近一次的进程,所以退出码为0。

1.2.3、退出码的意义是什么?

因为系统默认提供的退出码0就表示进程成功运行并且退出,非0就是异常退出,比如输入指令时,如果正常运行,echo$?时就会打印0,如果指令运行失败,则打印非0值,所以退出码的意义就是可以返回给上一级进程(父进程),用来评判该进程的执行结果。由于非0值有无数个,所以可以用不同的非0值表示不同的错误原因,可以让进程运行退出后,方便定位错误的原因。如下图1代码和下图2的运行结果,可以观察系统提供的退出码所对应的意义。(注意使用strerror函数需要头文件string.h)

 

下图中ls也是一个进程,111是main函数形参所需要的实参,名为111的目录文件不存在时打印的错误信息No such file or directory不就是上图的退出码2所对应的信息吗?如下图中此时echo $?也确实打印的退出码就是2。注意:ls 111这个程序是运行完毕了的,只是运行结果不符合预期,不要以为这是程序没跑完中途崩溃了。如果是越界等原因导致中途崩溃,会打印Segmentation fault在显示器上,表示段错误。程序崩溃的时候,退出码无意义,因为程序中途就崩溃了,return语句没有被执行。

 

我们可以使用系统提供的错误码,也可以自己设计一套退出方案,如下图中退出码1表示运行结果不正确,而不是表示系统提供的错误信息。当echo $?显示1的时候,我们就能立刻知道此时程序的运行结果不正确。

1.3、如何用代码终止一个进程

1.3.1、return+退出码

如标题所说,main函数当中的return+退出码就是用于终止当前进程的。注意只有main函数当中的return语句才可以终止进程,其他函数的return语句只是起返回返回值的作用。

1.3.2、void exit(int status)函数

使用时需要包头文件<stdlib.h>,用于终止当前进程,形参status表示退出码。和前面return的方法不同,exit函数在任何地方调用都直接结束当前进程,即不管出现在main函数里还是其他函数里,只要执行到exit函数就立刻退出当前进程。

1.3.3、void _exit(int status)或者void _Exit(int status)函数

函数声明在头文件<unistd.h>中,除开头文件不同,和exit函数的区别只有一个,那就是使用_exit函数退出进程时不会在退出进程之前刷新缓冲区,比如printf函数后面不加\n,也没有fflush函数,那么使用_exit函数退出进程时,printf函数会失效,而exit函数退出进程之前,会先刷新缓冲区,然后再退出进程。(注意_Exit函数和_exit函数是等价的)

为什么会这样呢?因为_exit函数是系统调用接口。从这里可以看出,操作系统中是不存在关于缓冲区的代码的,就是说不负责缓冲区的维护,缓冲区是语言的库为我们提供的,比如我们常说C语言为我们提供输出缓冲区,是C标准库在维护缓冲区。

2、进程等待

2.1、为什么要进行进程等待?

1.回收僵尸进程,避免一个已经终止但没有释放PCB的进程的PCB以及相关内核数据结构占用内存。

2.如果想要获取子进程退出信息,就必须进行进程等待。

2.2、进程等待的方法及其相关细节

2.2.1、pid_t wait(int* status)

wait函数用于等待(或者说回收)任意子进程。

头文件

1.pit_t就是size_t,函数在头文件<sys/types.h>和<sys/wait.h>中,注意两个头文件都要包。

返回值

1.当前进程里的wait函数调用成功时返回子进程的PID,失败时返回-1。

函数参数

1.如果不关注子进程的退出信息,即退出状态和终止信号,可以将函数参数设置为NULL。

其他细节

1.wait(&status)等价于waitpid(-1,&status,0),所以本质上wait函数是waitpid函数的子集。

2.cpu执行完当前进程里的fork函数后,创建了一个子进程。此时在属于父进程的代码块中调用wait函数,当cpu执行完wait函数的前一部分逻辑时(这部分逻辑用于暂停当前进程),注意这时只执行了wait函数的部分代码,此时当前进程(即父进程)会呈阻塞式等待,即当前进程的状态变为阻塞状态,正处在非cpu的运行队列中,这里子进程如果不死亡(终止),当前进程就一直等待,直到子进程死亡(终止)。当在子进程终止并退出后,父进程就立刻恢复运行状态,继续执行wait函数中剩下的代码,但在wait函数中剩下的代码执行完毕之前,由于此时子进程已经退出,父进程还在运行,所以子进程的状态就变成僵尸状态了,当wait函数的所有逻辑执行完毕后,操作系统就回收掉了处于僵尸状态的子进程的PCB,彻底释放了子进程占用的内存。由于cpu在一瞬间就可以执行很多代码,所以在观察进程状态时是看不见子进程的僵尸状态的,父进程恢复运行后在一瞬间wait函数中剩下的逻辑就调用完毕了,即在一瞬间就回收掉了子进程,此时子进程的内核数据结构所占用的内存全部被释放,也就不存在子进程了,所以不仅是看不到子进程的僵尸状态,回收后压根就看不到子进程的存在了。如果想观察到子进程的僵尸状态,比如属于子进程的代码块执行5s就退出(使用sleep函数控制),那么在属于父进程的代码块中的wait语句之前,即wait语句上面加上比如sleep(7)即可观察到子进程的僵尸状态,因为子进程5s就退出了,父进程暂停7s后才开始执行代码,所以父进程肯定是没有退出的,但子进程退出了,又没有人回收,所以处于僵尸状态,在5到7s的时间戳内是可以观察到子进程的僵尸状态的。

2.2.2、pid_t waitpid(pid_t pid,int* status,int options)(包括对options的解释)

waitpid函数用于等待(或者说回收)指定的子进程。

头文件

pit_t就是size_t,函数在头文件<sys/types.h>和<sys/wait.h>中,注意两个头文件都要包。

函数返回值

waitpid函数调用成功时返回子进程的PID,失败时返回-1,如果返回了0,那么表示参数options为WNOHANG,即为1,此时表示非阻塞等待,执行waitpid函数时,检测到此时子进程没有退出,waitpid函数直接返回0并继续执行父进程代码。

函数参数

1.实参pid=-1时,表示等待任意进程。当pid>0时,表示只等待指定的进程,比如pid为123,那么就只等待pid为123的进程,pid不为123的进程都不会进行等待。

2.如果不关注子进程的退出信息,即退出状态和终止信号,可以将int* status设置为NULL,剩余细节在下文细说。

3.int options

实参如果为0,表示阻塞等待,即waitpid函数内部有一块代码,逻辑为如果子进程没有退出,则暂停父进程,让父进程变成阻塞状态,将父进程的PCB从cpu的运行队列中移除到其他设备的运行队列中,只有子进程退出后,父进程才恢复运行状态,父进程的PCB重新回到cpu的运行队列,cpu继续执行wait函数内部剩余的代码。

实参如果为WNOHANG(这是一个宏,实际为1),表示非阻塞等待,即waitpid函数内部有一块代码,逻辑为如果子进程没有退出,则直接return 0,wait函数直接结束,继续执行父进程后序的代码。有人肯定会有疑惑,waitpid函数岂不是没起到任何作用?事实上不必担心,函数和循环组合使用即可。如下图。(下图是fork后属于父进程的代码块)

waitpid函数大概的伪代码可以看下图

2.2.3、两个函数都有的参数:int*status(包含对退出码和终止信号的解释)

status为输出型参数,由操作系统进行填充,可以获取子进程的退出信息,不关注时可以设置为NULL。如何使用这个参数呢?首先得在父进程中的wait语句之前定义一个变量int status=0,status是一个整型变量,之后在wait函数中填入实参&status即可。注意status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下。(只研究status低16比特位)

退出状态(即退出码)

在status的低16比特位当中,高8位,即8到15位表示进程的退出状态,即退出码,退出码可以通过定义变量int x=(status >> 8) & 0xFF拿到,0xFF表示二进制1111 1111,status>>8表示右移8位,即向低位移动8位,此时低8位就变成了移动前的次低8位,即用于表示退出码的8个位,然后按位和1111 1111与即可得到8个二进制位表示的10进制的退出码,这是因为:与操作只有1和1与才为1,只要出现0,相与后就是0,即使status实际上不止只有16位并且0xFF只有8位,由于相与时0xFF可以自动补0,所以status向低位移动8位后和0xFF相与时,status中高于(表示退出码的)8位的位都是和0相与,所以相与之后的结果也都是0,这样就可以拿到status中只表示退出码的8个二进制位,8个二进制位转换成10进制后就是子进程的退出码。

core dump标志

可以通过(status>>7)&1获取该比特位,这一个比特位表示是否发生了核心转储,当云服务器打开了核心转储的功能时,如果进程正常退出、或者使用信号终止进程,但信号的Action不为Core,则该比特位为0。如果该进程异常退出,或者使用信号终止进程,并且信号的Action为Core,则该比特位为1。详情可见《进程的信号》。

终止信号

在status的低16比特位当中,低7位,即0到6位表示终止信号,通过status & 0x7F即可拿到终止信号,0x7F表示二进制0111 1111,方法的原理和拿到退出码的原理一样,去看上文。如果进程是正常终止,那么低7位即终止信号为0,如果进程中途崩溃,则是异常退出,终止信号则是对应的异常信号,比如因为越界导致进程退出,终止信号的值就是下图蓝框中的11,如果进程是被信号所杀,则低7位表示对应的终止信号,比如kill -9杀死了子进程,那么父进程中可以通过用户定义的status变量观察到终止信号为9。第8位比特位是core dump标志,有兴趣自行了解。

2.2.4、利用WIFEXITED(status)和WEXITSTATUS(status)两个宏也可以拿到退出状态,即退出码。

WIFEXITED(status):若子进程是正常退出,即终止信号为0,则WIFEXITED(status)这个宏的返回值为真,即非0值。若子进程是异常退出,即进程中途崩溃了,则终止信号为非0,则WIFEXITED(status)这个宏的返回值为假,即为0。

WEXITSTATUS(status):若上面宏的结果为非0值,则提取子进程的退出码。

如下图1,可以通过手动位操作拿到退出码和退出信号,但也可以通过下图2里的宏拿到退出码。(对于拿到退出信号的宏,有兴趣自己搜索)

2.2.5、初始化status时明明等于0,为什么之后可以通过status拿到退出码和终止信息呢?

因为两种wait函数中都有int*status参数,初始化一个整形变量int status=0后调用函数wait(&status)将整形变量status传入函数中,函数内部有改变这个整形变量(即status)的逻辑,改变后这个整形变量就包含了表示退出码和终止信息的信息,所以可以通过status拿到退出码和终止信息。

2.2.6、父进程可以通过wait或者waitpid获取子进程的退出信息,即退出码和终止信号,那可以使用全局变量获取子进程的退出信息吗?

首先定义一个全局变量int code=0,用于表示退出码,在子进程结束前修改code,比如code=15,当子进程通过return 15,或者exit(15)结束进程后,那在属于父进程的代码块中(即父进程的执行流中)可以获取到这个15吗?答案是不可以,因为子进程修改code,即code=15时,由于进程具有独立性,此时会发生写时拷贝,父进程视角下的code变量还是0,所以不可以使用全局变量代替wait函数,即使通过这样的方式拿到了退出码,也拿不到终止信号。

2.2.7、进程具有独立性,那为什么父进程可以拿到子进程的退出码呢?两种wait函数做了什么?

1.父进程没有办法直接拿到子进程的退出码,但可以通过调用系统接口如wait函数,让操作系统帮父进程拿到。也正是因为两种wait函数都是系统调用接口,wait函数才有权限访问PCB,因为task_struct即PCB是内核数据结构,只有操作系统可以访问它。

2.一个进程退出时,main函数里return退出码是return给了操作系统,然后操作系统会将进程的退出码和终止信号写入到进程对应的PCB结构体变量中。当子进程退出后,父进程没有退出,此时子进程处于僵尸状态,此时会保留PCB结构体的变量,结构体变量中有表示各种进程退出信息的成员,源代码中包含int exit_code(退出码)和int exit_signal(终止信号)。wait函数的作用就是将子进程PCB里的退出码和终止信号填入父进程代码块中用户定义的变量中,如定义了int x=0,然后父进程代码块里调用函数wait(&x),此时wait函数会将PCB中表示退出码和终止信号的两个字段通过位操作填入变量x中。

3、进程替换

3.1、什么是进程替换?

fork之后子进程会执行属于子进程模块的代码,但本质上子进程和父进程的代码都是属于同一个程序,进程替换就是让子进程执行另一个程序的代码。

3.2、如何实现?

首先将程序加载进物理内存,之后改变子进程的页表的映射关系即可。注意子进程的task_struct和mm_struct完全不发生改变。

3.3、程序替换时,由于要将程序加载进内存,那么此时会产生新的进程吗?

不会,因为只是单纯的把程序的代码和数据加载进内存,然后将程序替换之前就已经存在的页表的映射关系修改了,操作系统不会为这个程序生成新的内核数据结构,所以不会产生新的进程。从这里也可以发现,之前常说的:【将程序加载进内存,此时就产生了一个进程】这句话是不完全准确的。

3.4、如何操作?

3.4.1、int execl(const char* path,const char* arg,。。。)函数及其细节

注意当execl函数调用失败时,即进程替换失败,则会继续执行当前进程的代码。当进程替换成功时,当前进程所有的代码和数据都被替换了,所以不会执行替换前的进程的代码了,会从新进程的main函数开始重新执行代码。

函数execl,最后的字符 l 表示list的意思。

头文件

<unistd.h>

参数

1.const char* path表示路径,实参为字符串。比如想让子进程替换成ls这个进程,填入ls程序所在的路径,比如execl(”usr/bin/ls“,”ls“,NULL)。

2.const char* arg表示若干个命令行参数,实参是若干个字符串,因为形参中的【 。。。】表示可变参数列表,最后一个参数必须以NULL结尾。示例:execl(”usr/bin/ls“,”ls“,”-l“,NULL)。

返回值

1.若execl函数调用失败,则进程替换失败,函数返回 -1。

2.若execl函数调用成功,则没有返回值的说法,因为进程已经完全被替换了,cpu会从新进程的main函数开始执行代码。

3.4.2、int execlp(const char* file,const char* arg,。。。)

和execl函数的区别是第一个参数变成了const char* file,即不用传路径,只需要可执行文件的文件名。如何做到的呢?函数execlp最后的字符p表示PATH,即环境变量PATH,函数可以通过PATH变量里表示路径的字符串找到对应的文件。

其他性质和execl函数一样,参考上文。

3.4.3、int execv(const char* path,char* const argv[ ])函数

函数execv,最后的字符v表示vector的意思。

参数char* const argv[]是个指针数组,数组中每个指针指向一个字符串。除了这个参数和execl函数不一样,其他性质都可以参考execl函数。实参传给execv函数时如下图红框。

3.4.4、int execvp(const char* file,char* const argv[ ])

结合execv函数和execlp函数就可以理解execvp函数的性质。和execv函数的区别是第一个参数变成了const char* file,即不用传路径,只需要可执行文件的文件名。

3.4.5、int execle(const char* path,const char* arg,。。。,char* const envp[ ] )

函数名execle最后的字符e表示自己维护环境变量,即自己给想要替换的进程传入环境变量。

和execl的区别是多了一个参数char* const envp[ ] ,这是一个指针数组,数组中每个指针指向一个字符串,字符串的值是各种环境变量的名称。用于将当前进程中的环境变量交给想要替换的进程,这里暂且把想要替换的进程叫做目标进程,那么在目标进程中,可以使用getenv函数或者其他方式获取替换前的进程的环境变量。

如下两图 ,图1在进程1中定义了环境变量MY_105 VAL=88877xxx,并通过进程替换的函数,既完成了将进程1替换成图2中的进程2,也完成了将进程1中的环境变量传给进程2的任务。进程2的getenv函数是从main函数的参数中获取到环境变量的。

3.4.6、int execvpe(const char* file,char* const argv[ ],char* const envp[ ] )

函数名execvpe后三个字符,和上文一样,v表示vector,p表示可以不带路径,e表示可以把环境变量传入想要替换的进程中。

3.4.7、int execve(const char* file,char* const argv[ ],char* const envp[ ] )

前6个函数之间已经存在互相封装的关系了,但它们6个函数都是通过execve函数实现的,即execve是它们实现的基础,是最底层的实现。v表示vector,e表示可以给想要替换的进程传入环境变量。

3.5、进程替换时,代码要不要写时拷贝?

是需要的,子进程在进程替换之前,即子进程变成另一个进程之前,父子进程的代码在物理内存中是共享的,数据写时拷贝,完成数据的分离。但当子进程完成进程替换后,由于进程要具有独立性,所以父子进程的代码也必须分离,此时父子进程的代码和数据在物理内存中就彻底分开了。

3.6、进程替换会替换进程的环境变量吗?

不会,进程替换只会替换进程的代码和数据,与系统有关的如环境变量是不会替换的,也能更加体现出环境变量具有全局属性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值