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

戴墨镜的恐龙 2024-10-12 12:37:01 阅读 95

文章目录

1. 进程创建2. 进程终止3. 进程等待4. 进程程序替换4.1 认识进程替换4.2 认识全部接口

在这里插入图片描述

1. 进程创建

如何创建进程我们已经在之前学习过了,无非就是使用fork(),它有两个返回值。创建成功,给父进程返回PID,给子进程返回0;创建失败,给父进程返回-1。

由于虚拟地址空间的存在,父子两进程各自独立,父子代码共享,父子在不修改时,数据也是共享的;当将fork的返回值进行写入时,便以写时拷贝的方式各自一份副本。具体见下图:

在这里插入图片描述

但是操作系统怎么知道要发生写时拷贝呢?

在调用fork时,父进程会先将页表中的执行权限全部改成只读。那么子进程继承下来的页表项,全部都是只读的。

当通过代码区对某些数据段就行写入时,会被页表识别到,并触发系统错误(缺页中断);只不过触发错误的时候,系统会判断是真的发生了错误,还是要发生写时拷贝!

fork常规用法

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

2. 进程终止

在我们以前写程序的时候,main函数总是会带一个返回值,但这个返回值有什么用处呢?它是给谁看的呢?- - 返回给父进程(bash)或者系统。

在下面的程序中,main函数返回了0

在这里插入图片描述

这个退出码有什么用呢? - - 表明错误的原因,一般用0表示成功,非0表示错误。

C/C++给我们提供了一些错误码,errno,perror,strerror等函数。

在这里插入图片描述

将退出码返回以后,可以供父进程或操作系统获得错误信息,所以这个错误码是给机器看的。

不同的系统,提供的错误码的种类和数量可能不同的;同时你也可以自己定义退出码。

进程终止的方式:

mian函数 return 返回exit(),会刷新缓冲区,底层调用_exit()_exit(),不会刷新缓冲区,是一个系统调用接口

return vs exit()

在这里插入图片描述

exit vs _exit

在这里插入图片描述

3. 进程等待

在进程状态那里讲过,子进程退出,如果父进程不读取它,它会变成僵尸进程。

在这里插入图片描述

为了避免子进程进入僵尸状态,父进程该如何读取子进程,回收它呢?- - -使用wait方法。

在这里插入图片描述

一般而言,父进程创建了子进程,就要对子进程负责,就要等待子进程,直到子进程结束。如果子进程不退出,父进程就要阻塞在wait函数内部。

在这里插入图片描述

除了回收子进程外,父进程还需要知道子进程把任务完成的怎么样呢? - -使用waitpid()

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

waitpid的第一个参数pid

pid > 0:等待指定的子进程

pid == -1:等待任意一个子进程

在这里插入图片描述

那如何获得子进程的退出信息呢?

waitpid的第二个参数 status

在这里插入图片描述

进程的退出码,是通过我们所传递的第二个参数带出来的,理想结果应该就是子进程的退出码1。

但是实际结果为什么是256呢?

在这里插入图片描述

这是因为status中不仅仅包含进程退出码,它还包括一些退出信息。

status不能简单的当作整形来看待,可以当作位图来看待,它有32个比特位,具体细节如下图(只研究status低16个比特位)

在这里插入图片描述

它的次低八位才是退出码

在这里插入图片描述

那低八位上的退出信号是干什么用的呢?

我们知道,进程退出有三次原因

代码跑完,结果对,return 0代码跑完,结果错误,return 非0进程异常

对于前两种原因,我们都可以通过退出码来识别错误;

对于第三种原因,进程异常直接会被操作系统使用信号终止,但是退出码是进程正常终止时由进程本身设置的,用于向父进程报告其结束状态或结果。然而,当进程因为接收到一个信号(如段错误、非法指令、用户中断等)而异常终止时,退出码就不再是一个可靠的指示器了。

所以,低七位上的会记录退出信号,第7位有其它作用,信号如下:

在这里插入图片描述

我们发现没有0号信号,因为0号信号标记着进程正常退出。

在这里插入图片描述

所以,当一个进程结束时,如果退出信号为0,则表示正常退出;但结果对不对,我们还需要再通过退出码来判断。

除了使用位运算来获取进程的退出信息,系统还提供了两个宏供我们使用:

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

在这里插入图片描述

waitpid的第三个参数option

options = 0 : 阻塞等待

options = WNOHANG:非阻塞等待。需要由你自己循环调用非阻塞接口,完成轮循检测,可以让调用方在轮循检测期间做更多自己的事情。

在这里插入图片描述

当option被设置为WNOHANG时,则函数有三个返回值:

返回值 > 0 : 非阻塞等待成功,返回子进程的PID返回值 < 0 :非阻塞等待失败返回值 = 0 :等待的进程尚未退出

所以,当函数的返回值 = 0时,可以让父进程执行一些其它的任务,然后轮循检测子进程是否退出。

在这里插入图片描述

4. 进程程序替换

4.1 认识进程替换

以往我们所创建出来的子进程,都是在执行和父进程相关的程序,那我能不能让子进程去执行一个全新的程序呢?下面展示的接口就是来完成程序替换的·

在这里插入图片描述

见一见进程替换

在这里插入图片描述

运行我们自己的程序发现,它去调用了系统的ls命令。也就是你main函数中没有代码,但是你去调用了人家ls的代码,这就是进程替换。

进程替换不是创建新的进程,仅仅是将代码和数据替换、页表映射修改一下,进程相关的PCB信息根本没有变化。

参数

那这个execl函数的参数都是什么意思呢?

在这里插入图片描述

简而言之,path就是你要执行谁,后面的可变参数列表就是你想怎么执行它。

返回值

这个execl的返回值是干什么用的呢?

在这里插入图片描述

只有调用execl时发生错误,该函数才会后返回-1;成功时没有返回值,因为调用成功后代码全被覆盖了,那它返回什么,所以只要它返回,它必定是失败的。

有了上面的理解后,我们做个简单的总结:

进程替换的使用中,一般不会让当前进程去替换,而是创建一个子进程去替换。父进程只需看子进程表演,如果想获得子进程任务的执行情况,通过返回码判断。如果返回了,则替换失败;没有返回,替换成功。

在这里插入图片描述

如果我不再将execl的参数写死,而是让用户输入,并且我给这段代码在套一个死循环,这不就是一个简单的命令行解释器吗?在linux中,所有的进程都是由父进程创建的,那 系统是怎么把我们的程序跑起来的呢?也无非就是先fork,然后进行程序替换。所以进程创建时要先有内核数据结构,即:使用fork继承父进程的;然后再execl加载自己的代码和数据,这不就是一个新的程序了吗?因此,用户所执行的所有程序,在操作系统看来全部都是进程。无非就是fork以后再执行程序替换

4.2 认识全部接口

进程替换一共有7个接口

在这里插入图片描述

第七个execve是系统调用,前6个都是C帮我们封装的函数,底层是调用的第七个。

execv vs execl

二者的区别就是execv将后面的参数,全部放在了一个vector里;execl是放在了一个list中

在这里插入图片描述

此时的vector数组,是不是就像main函数的命令行参数呢? - -就是这样,系统就是通过这个传递给main函数的。

execl函数内部也会将所传递的参数转化为上图所示的数组!

execlp vs execvp

在这里插入图片描述

带p(path)意味着:调用时无需指定路径,只需指定调用谁。v和l的区别还是vector与list

在这里插入图片描述

为什么不需要指定路径了呢?因为它会自己去path路径下找。

execvpe

在这里插入图片描述

除了告诉系统我要执行谁,怎么执行,我还可以给它传递新的环境变量,此时第三个参数就是环境变量。

当我们不传递时,替换的程序可以获得环境变量吗?- - 可以,通过全局指针变量envrion。

当我们传了,此时就会用我们所传递的,替换环境变量,程序使用全新的环境变量。

在这里插入图片描述

在这里插入图片描述

但是我不想改变整个环境变量表,仅仅想增加环境变量呢?

putenv()谁调用该函数,就将新的环境变量添加到它环境变量表当中!

如子进程添加环境变量,父进程的不变。

在这里插入图片描述

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表v(vector) : 参数用数组p(path) : 有p自动搜索环境变量PATHe(env) : 表示自己维护环境变量



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。