文章目录
- 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自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量