进程控制
1. 进程创建 (fork/vfork)
1.1 fork 函数初识
fork 函数用于从一个已存在的进程(父进程)中创建一个新进程(子进程)。
- 函数原型 :
pid_t fork(void); - 返回值 :
- 子进程 : 返回
0。 - 父进程: 返回子进程的 PID (大于0)。
- 出错 : 返回
-1。
- 子进程 : 返回
- 为什么有两个返回值? : 调用
fork后,内核会为子进程分配新的内存和内核数据结构,并将父进程的部分数据拷贝给子进程。之后,父子进程从fork返回处开始各自独立执行,因此会产生两个返回值。
1.2 写时拷贝 (Copy On Write)
为了提高效率,fork 创建的父子进程在初始时共享代码和数据。
- 机制: 只有当父子进程中任意一方试图修改数据时,操作系统才会为修改方复制一份数据副本。
- 优点: 这是一种延迟申请技术,避免了不必要的数据拷贝,提高了内存使用率。
1.3 fork 的常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段(如网络服务器中,父进程等待请求,子进程处理请求)。
- 一个进程要执行一个不同的程序 ,子进程在
fork返回后,立即调用exec函数族。
1.4 fork 调用失败的原因
- 系统中已经有太多的进程。
- 实际用户的进程数超过了系统限制。
2. 进程终止
进程终止的本质是释放系统资源,包括进程申请的内核数据结构和对应的代码数据。
2.1 进程退出场景
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止(如
Ctrl+C信号终止)。
2.2 进程常见退出方法
正常终止
- 从
main函数返回 (return)。 - 调用
exit函数。 - 调用
_exit函数。
异常终止
- 通过信号终止,如
Ctrl+C(SIGINT)。
2.3 退出码 (Exit Code)
退出码用于告知父进程或 Shell 命令的执行状态。可以通过 echo $ ? 查看。
| 退出码 | 解释 |
|---|---|
| 0 | 命令成功执行 |
| 1 | 通用错误 |
| 126 | 权限被拒绝或无法执行 |
| 127 | 未找到命令 |
| 128+n | 命令被信号 n 终止 |
| 130 | 被 Ctrl+C (SIGINT, 信号2) 终止 |
| 143 | 被 SIGTERM (信号15) 终止 |
2.4 exit 与 _exit 的区别
_exit(int status): 系统调用。直接终止进程,status的低8位作为退出码返回给父进程。exit(int status): 库函数。在调用_exit之前,会先执行一些清理工作:- 执行用户通过
atexit或on_exit定义的清理函数。 - 关闭所有打开的流,并刷新缓冲区(将缓存数据写入文件)。
- 最后调用
_exit。
- 执行用户通过
示例 :
printf("hello"); exit(0);会打印 "hello",因为exit会刷新缓冲区。而printf("hello"); _exit(0);则不会打印。
2.5 return 退出
在 main 函数中,return n 等同于 exit(n),因为调用 main 的运行时函数会将 main 的返回值作为 exit 的参数。
3. 进程等待 (wait/waitpid)
3.1 进程等待的必要性
- 回收资源 : 子进程退出后,如果父进程不处理,会变成僵尸进程 (Zombie),其 PCB 仍占用内存,造成内存泄漏。
- 获取状态: 父进程需要知道子进程的任务完成得如何(正常退出还是被信号杀死,退出码是多少)。
- 僵尸进程无法被杀死 :
kill -9对已经死去的僵尸进程无效。
3.2 进程等待的方法
wait 函数
- 函数原型 :
pid_t wait(int* status); - 功能: 阻塞等待任意一个子进程退出。
- 返回值: 成功返回被等待的子进程 PID,失败返回 -1。
- 参数 :
status是输出型参数,用于获取子进程的退出状态。若不关心可设为NULL。
waitpid 函数
- 函数原型 :
pid_t waitpid(pid_t pid, int *status, int options); - 功能: 更灵活的等待方式。
- 参数 :
pid:pid = -1: 等待任一子进程,等效于wait。pid > 0: 等待指定 PID 的子进程。
status: 输出型参数,获取退出状态。options:0: 阻塞等待。WNOHANG: 非阻塞等待。若子进程未退出,立即返回 0。
3.3 获取子进程退出状态 (status)
status 是一个位图,不能简单看作整型。
- 正常终止 :
WIFEXITED(status): 判断子进程是否正常终止。WEXITSTATUS(status): 若正常终止,提取其退出码。
- 被信号终止 :
status的低7位存储了终止该进程的信号编号。
3.4 阻塞与非阻塞等待
- 阻塞等待 : 父进程调用
wait或waitpid(..., 0)后会暂停执行,直到子进程退出。 - 非阻塞等待 : 父进程调用
waitpid(..., WNOHANG)后,如果子进程未退出,函数立即返回 0,父进程可以继续做其他事情(如处理其他任务),实现并发。
4. 进程程序替换 (exec 函数族)
4.1 替换原理
fork 创建的子进程默认执行与父进程相同的程序。若想让子进程执行一个全新的程序,需要调用 exec 函数族。
- 核心: 用磁盘上的一个新程序,完全替换掉当前进程的代码和数据段。
- 特点 : 不创建新进程,进程的 PID 保持不变。
4.2 exec 函数族
共有六个以 exec 开头的函数,它们最终都调用系统调用 execve。
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
|---|---|---|---|
execl |
列表 (list) | 否 (需全路径) | 是 |
execlp |
列表 (list) | 是 (搜索 PATH) | 是 |
execle |
列表 (list) | 否 (需全路径) | 否 (需自己提供) |
execv |
数组 (vector) | 否 (需全路径) | 是 |
execvp |
数组 (vector) | 是 (搜索 PATH) | 是 |
execve |
数组 (vector) | 否 (需全路径) | 否 (需自己提供) |
- l (list) : 参数以列表形式逐个传递,最后以
NULL结尾。 - v (vector) : 参数以指针数组
char *const argv[]的形式传递。 - p (path) : 可以只提供文件名,函数会自动在
PATH环境变量中搜索。 - e (env) : 需要自己组装并传递环境变量数组
char *const envp[]。
注意 :
exec函数调用成功则不返回,直接从新程序的main函数开始执行。只有调用失败时才返回 -1。
5 内建命令 vs 外部命令
- 内建命令 (Built-in Command) : 如
cd,export。它们必须由 Shell 进程自己执行,因为它们的目的是改变 Shell 自身的状态(如工作目录、环境变量)。如果在子进程中执行,子进程的更改不会影响到父 Shell。 - 外部命令 : 如
ls,ps。它们是独立的可执行程序,通过fork+exec的方式在子进程中运行。