第四讲 进程控制
1. 进程创建
1.1. fork初识
1.1.1. fork基本用法
fork函数是用来在程序中创建进程的函数, 他从一个已经存在的进程中创建新的进程. 新进程为子进程, 而原来的进程为父进程.
#include <unistd.h>
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1
当调用fork函数时, 操作系统开始创建进程. 那操作系统此时正在做什么呢?
我们知道, 一个进程 = 相关的数据结构(task_strcut, mem_strcut...) + 对应的代码和数据. 因此操作系统先在内核中创建一个task_strcut, 虚拟地址空间等等, 之后再把父进程的代码和数据映射到虚拟地址空间, 页表中去.
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
如何理解进程的独立性问题?
答: 所谓的进程独立性, 并不是指的是两个进程一点关系没有, 指的是一个进程出现问题, 其他进程不受干扰(影响). 而操作系统虽然让父子进程对代码和数据进行了共享, 但是也可以通过写时拷贝的方式对其修改数据进行分离, 实现进程独立.
1.1.2. fork返回值
对于fork函数, 如果成功的话有两个返回值,
- 对于父进程, 返回子进程的pid
- 对于子进程, 返回0
- 如果fork失败, 则只对父进程返回-1.
为什么fork函数对于父进程返回的是子进程的pid, 而给子进程返回的是0 ?
答: fork函数返回的是子进程的返回值, 告诉父进程fork函数的执行结果, 这样方便父进程对子进程进行管理和标识. 而对于子进程, 他没有子进程, 成功了因此返回0.
1.1.3. fork常规用法
通常, 我们新建一个进程通常有下面用法:
- 代码分流: 一个父进程希望复制自己, 使父子进程分流, 父进程执行一部分代码, 子进程执行一部分代码.
- 执行不同的功能: 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.1.4. fork函数调用失败的原因
虽然fork函数大多数时候可以调用成功, 但是也有一些情况调用失败.
- 系统中进程过多
- 实际用户的进程数超过了限制
2. 进程的终止
2.1. 进程的终止在做什么?
还是说一下, 我们的进程 = 相关数据结构 + 对应代码和数据. 因此, 对于进程的终止是:
- 释放对应代码和数据
- 释放相关数据结构
- 返回错误码(退出码)
释放相关数据结构
不过这里需要注意的是, 对于释放相关的数据结构, 操作系统往往会延迟处理, 当释放对应代码和数据之后进程会进入Z(僵尸)状态, 等待父进程对其进行回收, 接收其错误码.
错误码:
用来标识进程处理事情的结果. 在bash中用了?
对最近的进程退出错误码进行了保存.
echo $?
不过需要明白的是, 错误码可以使用自定义哈.
错误码的意义: 在于程序正常情况下来标识返回结果是否正确.
2.2. 进程终止的三种情况
- 代码跑完, 结果正确
- 代码跑完, 结果错误
- 代码异常
前面我们已经说过错误码来标识代码跑完情况下, 结果是否正确, 以及为啥不正确的.
代码异常理解: 而对于代码异常, 本质上是接收到了来自操作系统的"信号", 而这个信号, 用户可以让操作系统被动发送, 也可以是操作系统查到问题操作系统自己主动发送. 一旦出现异常, 错误码不再具有意义.
举例: 为了更好的理解, 我们可以创建一个死循环的进程, 之后kill -11 pid, 我们就可以看到相关的异常报错, 再比如我们写一个10/0在程序内, 操作系统也会主动终止程序. 不过本质上都是操作系统发给进程信号让其终止就是了.
下面是操作系统主动给进程发送终止信号的例子:
下面是用户让操作系统让其对某进程终止:
发送kill -11 pid
2.3. 如何终止进程?
return: 在main函数内, return
即可终止进程. 但是return如果是在调用的函数中, 则表示返回上一层函数.
exit: 这是一个C语言库函数, 内部调用系统调用_exit, 不但可以在程序的任意位置终止进程, 还可以在终止进程之前冲刷缓冲区.
_exit: 这是一个操作系统的系统调用接口, 也是可以在程序的任意位置终止进程, 但是不能冲刷缓冲区, 因为缓冲区在_exit的上层封装, _exit不能进行调用.
return: C语言关键字, 即使不包含任何头文件都可以使用.
exit: C库函数, 包含在头文件为<stdlib.h>的头文件中
_exit: 操作系统接口, 头文件是<unistd.h>
3. 进程等待
3.1. 进程等待的概念
任何子进程, 在退出时, 一般都必须要被父进程进行等待. (进程在退出时, 若父进程不管, 则该进程会进入Z状态, 进而引发内存泄漏问题).
3.2. 进程等待的原因
- 回收系统资源: 父进程通过等待子进程, 解决子进程僵尸问题, 回收系统资源.
- 获取子进程信息: 获取子进程的退出信息, 了解子进程退出原因.
3.3. 进程等待的实现
父进程等待通过两个函数完成对子进程的等待.
- wait
- waitpid
我们下面依次来介绍wait和waitpid两个函数, 介绍wait函数时候, 我们来说一下回收僵尸问题.
#include<sys/types.h> #include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程的pid, 如果失败则返回-1
参数:
输出型参数, 获取子进程的状态信息, 不关心则可以设置为NULL
监控脚本: while : ; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done
父进程在等待时候做其他事情了吗? 没有, 父进程一直处在阻塞状态. 在这个过程中, 父进程在等待某种软件条件就绪, 如何理解阻塞呢? 不局限于硬件设备的等待, 软件也可以进行等待.
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时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
获取子进程的状态信息:
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
问: 为什么不定义两个全局变量去接收status呢? 而是用这么一个参数?
答: 这是因为写时拷贝问题.
status的处理:
- 我们可以对(status>>7)&0xFF提取信号, (status) & 0X7F来提取退出码. 当然, 也可以通过使用宏的方式.
-
- WIFEXIED(status) //如果正常终止子进程, 返回真, 查的是信号
- WEXITSTATUS(status) //如果WIFEXITSED位0, 提取子进程退出码, 查的是退出码.
非阻塞等待: WNOHANG
为啥叫这个名字呢? 服务器卡住了->服务器"HANG"住了, 有点等待的含义在里面.
非阻塞等待的好处? 就是可以让父进程去做一些自己的事情.
pid_t > 0 等待成功, 子进程退出了, 父进程回收成功.
pid_t < 0, 等待失败
pid_t = 0, 检测是成功的, 子进程还没有退出, 需要重复对子进程进行访问. (非阻塞等待).
非阻塞轮询: 非阻塞等待 + 循环.
4. 进程的替换
4.1. 进程替换的原理
原理: 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2. 进程替换用到的函数
函数及其解释:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]); (注意: 这个属于系统调用接口, 并不属于C语言库函数)
exec函数族中函数带 p
(比如execlp
、execvp
)的表示会自动搜索环境变量PATH
.类似的函数名出现以下字符都表示特定的意义:
l
(list):表示参数采用列表。
v
(vector):表示参数用数组。
e
(env) : 表示自己维护环境变量
path
表示的是可执行文件的路径
arg
表示命令行参数的某个元素。第一个必须是程序名,其余每一个arg参数表示一条选项用逗号隔开。最后以NULL结尾(具体用法看上面例子)。
file
表示可执行文件名,在execvp、execlp中会在其环境变量中去找。
enp[]
表示环境变量数组,可提供给替换程序作为环境变量
argv[]
表示命令行参数数组,以命令名为首,以NULL结尾。给替换程序提供命令选项。
函数之间的关系:
4.3. 进程替换的例子
下面是一个进程替换的例子: