马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
进程这块是 Linux 学习分水岭:明白了进程怎么来、怎么走、父进程怎么收、代码怎么换,后面学多线程、守护进程、容器原理都顺。这篇把进程创建的 fork 内核动作、进程退出的三种情况与退出码、等待子进程的 wait/waitpid、以及程序替换 exec 系列,按"为什么—怎么做—参数怎么传—返回值怎么看"四段讲清楚。
一、进程创建
进程调用 fork,控制流转入内核的 fork 代码后,内核做四件事:
- 分配新的内存块和内核数据结构给子进程;
- 把父进程的部分数据结构内容拷贝给子进程;
- 把子进程加入系统进程列表;
- fork 返回,调度器开始调度。
对应用户侧一句话:子进程拿到的是父进程的副本,之后父子各自独立运行。
二、进程退出
1. 为什么要退出
释放代码和数据占用的空间,释放内核数据结构。
2. 退出的三种情况
- 代码跑完,结果正确;
- 代码跑完,结果不正确;
- 代码执行时出现异常,提前退出。
父进程 bash 通过 echo $? 拿到最近一个子进程的退出码,据此判断子进程情况:退出码 0 表示成功,非 0 表示失败,非 0 有很多种,每个退出码对应不同的失败原因。可以用 strerror 把退出码翻成文字,原型是:
char *strerror(int errnum);
要点:衡量一次进程退出只看两个数字——退出码和退出信号。所谓的"异常退出",本质是进程收到了 OS 发来的进程信号。
3. 怎么退出
- main 函数里直接 return(注意 return 在非 main 函数里只表示函数结束);
- 代码里调用 exit 函数,在任意位置调用都代表进程退出;
- 调用
_exit(),这是系统调用。
exit 和 _exit 的区别只有一条:exit 退出时会冲刷缓冲区,_exit 不会。写日志 类程序时这个差别会造成"日志 少几行"的诡异现象。
三、进程等待
1. 为什么等
- 父进程通过等待回收子进程、解决僵尸问题,回收系统资源(必须做);
- 获取子进程退出信息,知道它为什么退出的(非必须)。
2. 怎么等
两个函数:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
子进程退出之前,父进程会一直阻塞等待,不会执行自己的代码。
3. 参数怎么看
pid:
- pid 为 -1:等待任意一个子进程,等价于 wait;
- pid 大于 0:只等这个进程 ID 的子进程。
status:输出型参数,用来取子进程的退出信息(退出码 + 退出信号)。父进程不关心的话可以传 NULL。int 类型的 status 由 32 位构成,高 16 位不用;低 16 位里,高 8 位是退出码,低 7 位是退出信号。判断宏两个最常用:
WIFEXITED(status) // 进程是否正常退出
WEXITSTATUS(status) // 取退出码,需先确认上面为真
options:默认 0,表示阻塞等待。改成 WNOHANG 就变成非阻塞等待:父进程只确认一次状态,不等,可以干别的;此时需要循环调用 waitpid,也就是"非阻塞等待 + 循环 = 非阻塞轮询"。
4. 返回值
阻塞等待:正常返回时 waitpid 返回回收到的子进程 PID;出错返回 -1,errno 置成对应值。
非阻塞等待三种返回值:
- 大于 0:等待成功,子进程已退出并被回收;
- 小于 0:等待失败;
- 等于 0:检测到了,但子进程还没完全退出,需要下一轮再来。
while ((ret = waitpid(-1, &status, WNOHANG)) > 0) {
// 回收到一个子进程
}
这段轮询写法是 shell 脚本里"后台任务收尸"和 C 程序里优雅退出的通用骨架。
四、进程替换
用 exec 系列函数,在原本的程序中执行另一个新程序,叫程序替换。本质是 exec 系列相当于 Linux 上的加载函数,把新程序加载进内存。三个关键结论:
- exec 调用之后,后面的代码不会再执行,因为进程已经被替换;
- 返回值不用管——替换成功就不会继续往下跑,还能往下跑就说明失败了;
- 进程 = 内核数据结构 + 代码和数据,程序替换只换掉代码和数据,不创建新进程,复用原来的地址空间和页表,重新建映射。
父进程创建子进程后让子进程去执行新程序时,父子代码数据会发生写时拷贝,子进程拿到新代码新数据,与父进程完全独立。
1. execl
int execl(const char *path, const char *arg, ...);
后缀 l 代表 list,按列表方式传参。path 是程序所在路径,必须给全;arg 是程序名(命令名);省略号是变参,可继续追加参数,参数必须以 NULL 结尾。
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
2. execv
int execv(const char *path, char *const argv[]);
后缀 v 代表 vector 数组,命令从数组里取。argv 就是存放命令的数组:
char *const argv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls", argv);
3. execlp 与 execvp
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
p 代表环境变量 PATH。这两个功能与上面两种完全相同,区别是第一个参数不用传路径,只传程序名,函数会去 PATH 里找。日常写脚本优先用带 p 的。
4. execle 与 execvpe
int execle(const char *file, const char *arg, ..., char *const envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
e 代表环境变量,用这两种函数可以替换程序原本的环境变量。
五、把四件事连起来
fork 造子进程 → 子进程调 exec 换成目标程序 → 干完 exit/异常退出 → 父进程 waitpid 收尸并拿退出码。这条链路就是 shell 执行一条命令的完整过程:bash fork 一个子进程,子进程 exec 成 ls,ls 跑完退出,bash 用 $? 拿到退出码。理解这条链路,后面看 && 与 || 的短路逻辑、看容器的 PID 1、看进程树的僵尸,就都通了。
qidao123.com企服评测-应用市场 编辑提示
本文按 fork 创建的四步内核动作、进程退出三种情况与退出码加退出信号两个数字、exit 与 _exit 的缓冲区差异、wait 与 waitpid 的 pid/status/options 三参数与 WNOHANG 轮询写法、exec 系列 l/v/p/e 四类后缀的含义与原型,重排成一条"创建—退出—等待—替换"的完整链路。函数原型与宏取自 Linux 通用程序设计手册,示例代码为教学示意,请在自有编译环境中验证;程序替换与信号相关操作仅用于合法学习与自有服务排障。 |