【Linux】进程控制(1)
本文介绍了Linux中进程创建与终止的相关机制。进程创建方面,重点讲解了fork系统调用及其写时拷贝优化,分析了fork的常见用法和失败原因。进程终止部分,详细说明了正常退出、错误退出等场景,对比了_exit、exit和return三种退出方式的差异。此外,还阐述了进程等待的必要性,介绍了wait和waitpid两种方法,包括如何获取子进程状态以及阻塞与非阻塞等待的实现方式。这些内容为理解Linu
1. 进程创建
1.1 fork 函数初识
fork
是 Linux 中创建新进程的核心系统调用,原型为:
#include <unistd.h>
pid_t fork(void);
- 功能:从已存在的父进程中创建一个新的子进程,子进程是父进程的 “副本”。
- 返回值:
- 对父进程:返回子进程的 PID(大于 0 的整数)。
- 对子进程:返回 0。
- 失败:返回 -1(并设置
errno
)。
核心特性:
- 调用一次
fork
,会返回两次(父进程和子进程各一次)。 - 子进程会复制父进程的代码段、数据段、堆、栈等内存空间,以及打开的文件描述符、信号处理方式等。
- 父子进程共享 CPU 时间片,独立运行,执行顺序由操作系统调度决定(不确定)。
1.2 写时拷贝(Copy-On-Write, COW)
fork
最初设计为 “全量复制父进程内存”,但效率极低(多数子进程会立即执行 exec
替换代码)。现代操作系统通过写时拷贝优化:
- 原理:
fork
创建子进程时,不立即复制父进程的内存数据,而是让父子进程共享同一份内存页,并将这些内存页标记为 “只读”。 - 触发拷贝:当父子进程任一方向修改共享内存页时,会触发页错误:内核为修改方分配新物理页,复制原内容,更新其页表指向新页并标记为可写,实现双方修改隔离。
- 优势:
- 减少
fork
的时间开销(无需立即复制大量内存)。 - 节省内存资源(若子进程未修改数据,无需额外空间)。
- 减少
写时拷贝是对 “空间” 和 “时间” 的双重优化,使 fork
调用更高效。
1.3 fork 常规用法
fork
主要用于创建子进程执行与父进程不同的任务,常见场景:
-
父进程与子进程分工协作
父进程继续处理原有任务,子进程执行新任务(如服务器进程接收请求后,fork
子进程处理具体请求,父进程继续监听)。pid_t pid = fork(); if (pid == 0) { // 子进程:处理具体任务 handle_request(); exit(0); } else if (pid > 0) { // 父进程:继续监听新请求 continue_listen(); }
-
创建子进程执行新程序
子进程通过exec
系列函数(如execl
、execvp
)加载新程序,替换自身代码段(这是 Shell 执行命令的核心逻辑)。pid_t pid = fork(); if (pid == 0) { // 子进程:执行新程序(如 "/bin/ls") execl("/bin/ls", "ls", "-l", NULL); // 若 exec 失败才会执行以下代码 perror("execl failed"); exit(1); }
1.4 fork 调用失败的原因
fork
并非总能成功,常见失败原因:
-
系统进程数达到上限
操作系统对最大进程数有限制(可通过ulimit -u
查看),若已达上限,fork
会失败。 -
内存不足
尽管有写时拷贝优化,但fork
仍需为子进程分配进程描述符、页表等内核数据结构,若内存不足(或虚拟内存耗尽),会导致失败。 -
用户进程数超限
系统可能限制单个用户的最大进程数,若当前用户创建的进程数已达上限,fork
会失败。
失败时,fork
返回 -1 并设置 errno
(如 EAGAIN
表示暂时资源不足,ENOMEM
表示内存不足),可通过 perror
打印具体错误信息。
2. 进程终止
2.1 进程退出场景
进程终止主要有以下几种常见场景:
- 正常退出:进程完成预定任务后主动退出(如程序执行到
main
函数结尾)。 - 错误退出:因无效输入、资源不足等错误主动终止(如
exit(1)
)。 - 信号终止:被操作系统或其他进程发送的信号强制终止(如
kill -9 PID
发送SIGKILL
信号)。 - 异常终止:因内存访问违规、除零等致命错误触发信号退出(如段错误
SIGSEGV
)。
2.2 进程常见退出方法
2.2.1 退出码
进程退出时会返回一个整数退出码(0~255),用于表示退出状态。
- 退出码
0
表示命令成功执行; - 退出码
1
是通用错误代码; - 退出码
2
表示命令(或参数)使用不当; - 退出码
126
表示权限被拒绝(或)无法执行; - 退出码
127
表示未找到命令,或PATH
错误; - 退出码
128 + n
表示命令被信号从外部终止,或遇到致命错误; - 退出码
130
表示通过Ctrl + C
或SIGINT
终止(终止代码 2 或键盘中断); - 退出码
143
表示通过SIGTERM
终止(默认终止); - 退出码
255/*
表示退出码超过了 0 - 255 的范围,因此重新计算(超过 255 后,用退出码取模)。
2.2.2 _exit 函数
_exit
是系统调用,用于立即终止进程,原型:
#include <unistd.h>
void _exit(int status);
- 特点:直接终止进程,不执行清理工作(如不刷新缓冲区、不调用
atexit
注册的函数)。 - 参数
status
:退出码(低 8 位有效,超出部分会被截断)。 - 用途:通常在子进程或紧急退出场景使用,确保资源快速释放。
2.2.3 exit 函数
exit
是标准库函数,封装了 _exit
,原型:
#include <stdlib.h>
void exit(int status);
- 特点:终止进程前会执行清理操作:
- 刷新所有已打开的标准 I/O 缓冲区(如
printf
未手动刷新的内容)。 - 调用通过
atexit
注册的退出处理函数(按注册逆序执行)。 - 最终调用
_exit
完成进程终止。
- 刷新所有已打开的标准 I/O 缓冲区(如
- 用途:大多数场景下的正常退出,确保资源正确释放和数据完整性。
2.2.4 return 退出
在 main
函数中使用 return n
等价于 exit(n)
,即:
int main() {
return 0; // 等价于 exit(0)
}
- 仅在
main
函数中有效,其他函数的return
只是返回调用者,不会终止进程。 - 底层会将返回值作为退出码,触发与
exit
相同的清理流程。
总结:return
(仅 main
)和 exit
适用于正常退出(带清理),_exit
适用于快速退出(无清理);退出码用于标识退出状态,供父进程判断执行结果。
3. 进程等待
3.1 进程等待的必要性
3.2 进程等待的方法
3.2.1 wait 方法
wait
是 Linux 系统中用于父进程等待子进程退出的系统调用,原型为:
#include <sys/wait.h>
pid_t wait(int *status);
- 功能:阻塞等待任意一个子进程终止,回收子进程资源(避免僵尸进程)。
- 返回值:成功返回终止的子进程 PID;失败返回 -1(如无子进程)。
- 参数
status
:用于存储子进程的退出状态,若为NULL
则不关心退出状态。
特点:
- 只能等待任意一个子进程,无法指定特定子进程。
- 阻塞式等待,若没有子进程退出,父进程会一直暂停执行。
3.2.1 waitpid 方法
waitpid
是更灵活的进程等待函数,支持指定等待的子进程和等待方式,原型为:
pid_t waitpid(pid_t pid, int *status, int options);
-
参数
pid
:指定等待的子进程:pid > 0
:等待 PID 为pid
的子进程。pid = 0
:等待与父进程同组的任意子进程。pid = -1
:等待任意子进程(等效于wait
)。pid < -1
:等待进程组 ID 为|pid|
的任意子进程。
-
参数
options
:控制等待方式:0
:默认阻塞等待。WNOHANG
:非阻塞等待,若子进程未退出则立即返回 0。
-
返回值:
- 成功:返回退出的子进程 PID。
- 若
WNOHANG
生效且子进程未退出:返回 0。 - 失败:返回 -1(如无对应子进程)。
特点:可指定子进程、支持非阻塞等待,功能比 wait
更强大。
3.2.3 获取子进程 status
status
参数用于接收子进程的退出状态,是一个整数,需通过宏解析:
WIFEXITED(status)
:若为真,表明子进程正常退出。WEXITSTATUS(status)
:结合WIFEXITED
使用,获取子进程的退出码(exit(n)
中的n
)。WIFSIGNALED(status)
:若为真,表明子进程被信号终止。WTERMSIG(status)
:结合WIFSIGNALED
使用,获取终止子进程的信号编号。
示例:
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号 %d 终止\n", WTERMSIG(status));
}
3.2.4 阻塞与非阻塞等待
-
阻塞等待:父进程暂停执行,直到子进程退出后才继续运行(
wait
或waitpid(pid, &status, 0)
)。
优点:实现简单,无需轮询;缺点:父进程在等待期间无法处理其他任务。 -
非阻塞等待:父进程发起等待后立即返回,若子进程未退出,可继续执行其他逻辑,需通过循环轮询检查(
waitpid(pid, &status, WNOHANG)
)。
优点:父进程可并行处理其他任务;缺点:需手动实现轮询逻辑,可能消耗额外资源。
示例(非阻塞等待):
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) == 0) {
// 子进程未退出,执行其他任务
printf("等待子进程中...\n");
sleep(1);
}
if (pid > 0) {
printf("子进程 %d 已退出\n", pid);
}
两种方式的选择取决于业务需求:若父进程无其他任务,阻塞等待更高效;若需并发处理多任务,非阻塞等待更灵活。
更多推荐
所有评论(0)