进程控制是 Linux 系统编程的核心能力,涵盖进程的创建、终止、等待和程序替换四大核心操作。掌握这些操作不仅能理解程序的运行机制,更能构建如 Shell 这样的复杂工具。本文将从基础概念入手,逐步拆解每个进程控制接口的原理与使用场景,最终通过实战实现一个简易 Shell,帮助开发者彻底掌握 Linux 进程控制的精髓。
一、进程创建:fork 与写时拷贝
创建进程是进程生命周期的起点,Linux 中最核心的接口是 fork,其设计巧妙地平衡了资源复用与进程独立性。
1.1 fork 函数的核心特性
fork 函数从已存在的进程(父进程)中创建一个新进程(子进程),核心定义如下:
#include <unistd.h>
pid_t fork(void);
返回值:
父进程:返回子进程的 PID(进程唯一标识);
子进程:返回 0;
出错:返回 -1(如系统进程数超限)。
内核操作:
为子进程分配新的内核数据结构(task_struct、页表等);
拷贝父进程的部分数据(如 PCB 中的进程属性、虚拟地址空间映射);
将子进程加入系统进程列表;
调度器调度父子进程执行(谁先执行由调度器决定)。
1.2 fork 的执行逻辑
fork 调用后,父子进程共享代码段(二进制指令),但数据段(全局变量、局部变量)采用写时拷贝(Copy-On-Write, COW) 机制:
未写入时:父子进程共享同一物理内存页,页表标记为 “只读”;
写入时:触发 CPU 缺页异常,内核为写入方复制新的物理内存页,修改页表映射,实现 “数据独立”。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("Before fork: PID = %d\n", getpid());
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
return 1;
} else if (pid == 0) {
// 子进程:修改数据,触发写时拷贝
printf("Child: PID = %d, Parent PID = %d\n", getpid(), getppid());
} else {
// 父进程:读取数据,不触发拷贝
printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);
sleep(1); // 避免父进程先退出,子进程成为孤儿
}
return 0;
}
运行结果:
Before fork: PID = 12345
Parent: PID = 12345, Child PID = 12346
Child: PID = 12346, Parent PID = 12345
1.3 fork 的常见用法
- 父子分工:父进程等待客户端请求,子进程处理请求(如 Web 服务器);
- 程序替换:子进程通过
exec系列函数执行全新程序(如 Shell 执行ls命令)。
二、进程终止:释放资源与退出码
进程终止的本质是 “释放内核分配的资源”(PCB、页表、物理内存等),需明确退出场景与退出状态,以便父进程获取执行结果。
2.1 进程退出的三种场景
- 正常退出:代码运行完毕,结果正确(如
main函数返回 0); - 异常退出:代码运行完毕,结果错误(如
main返回 1); - 强制终止:代码未运行完毕,被信号中断(如
Ctrl+C发送SIGINT信号)。
2.2 常用退出接口
Linux 提供三种核心退出接口,差异主要在于是否处理用户清理逻辑和 I/O 缓冲:
2.2.1 _exit:直接终止进程(内核级)
#include <unistd.h>
void _exit(int status);
功能:立即终止进程,释放内核资源,不处理用户空间资源;
参数 status:退出状态码,仅低 8 位有效(如 _exit(-1) 对应退出码 255);
特性:不刷新 I/O 缓冲(如 printf 未换行的内容会丢失)。
2.2.2 exit:处理清理后终止(用户级)
#include <stdlib.h>
void exit(int status);
功能:
在调用 _exit 前,额外执行三项操作:
执行 atexit/on_exit 注册的清理函数(如释放自定义资源);
刷新所有打开的 I/O 流(如 printf 缓冲内容写入终端);
关闭所有打开的文件描述符;
示例:
#include <stdio.h>
#include <stdlib.h>
void clean() {
printf("Custom clean function\n");
}
int main() {
atexit(clean); // 注册清理函数
printf("Hello "); // 未换行,缓冲未刷新
exit(0); // 刷新缓冲并执行清理函数
}
运行结果:
Hello Custom clean function
2.2.3 return:函数级退出
main 函数中 return n 等价于 exit(n)(其他函数的 return 仅退出函数,不终止进程),是最常用的退出方式。
2.3 退出码与状态解析
进程退出状态通过 退出码(0~255) 表示,规则如下:
0:执行成功;
1~255:执行失败(自定义含义,如 1 表示通用错误,2 表示参数错误);
查看退出码:Shell 中通过 echo $? 查看上一个进程的退出码。
常见退出码含义:
| 退出码 | 含义 | 示例场景 |
|---|---|---|
| 0 | 成功 | ls 命令正常执行 |
| 1 | 通用错误 | 除以 0、权限不足 |
| 2 | 命令使用不当 | ls --invalid-option |
| 126 | 权限拒绝(无法执行) | ./no-executable-file |
| 127 | 命令未找到 | lss(拼写错误) |
| 128+n | 被信号终止(n 为信号值) | Ctrl+C 对应 130(128+2) |
三、进程等待:回收僵尸进程与获取状态
子进程退出后若父进程不处理,会成为僵尸进程(Z 状态),导致内存泄漏。进程等待通过 wait/waitpid 接口回收子进程资源,并获取其退出状态。
3.1 进程等待的必要性
- 避免僵尸进程:僵尸进程的 PCB 仍占用内存,长期积累会导致内存泄漏;
- 获取执行结果:父进程需知道子进程任务是否完成、结果是否正确;
- 同步父子进程:父进程可等待子进程完成后再继续执行。
3.2 核心等待接口
3.2.1 wait:阻塞等待任意子进程
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:阻塞等待任意一个子进程退出,回收其资源;
参数 status:输出型参数,存储子进程退出状态(不关心可传 NULL);
返回值:成功返回被回收子进程的 PID,失败返回 -1(如无待回收子进程)。
3.2.2 waitpid:灵活等待指定子进程
pid_t waitpid(pid_t pid, int *status, int options);
参数 pid:指定等待的子进程:
-1:等待任意子进程(与 wait 等价);
>0:等待 PID 等于该值的子进程;
参数 options:等待方式:
0:阻塞等待(子进程未退出时,父进程暂停执行);
WNOHANG:非阻塞等待(子进程未退出时,立即返回 0,不阻塞);
返回值:
成功:返回被回收子进程的 PID;
WNOHANG 时子进程未退出:返回 0;
失败:返回 -1。
3.3 解析子进程退出状态
status 参数是一个 32 位整数,仅低 16 位有效,按位图解析:
低 7 位:子进程是否被信号终止(非 0 表示信号终止,值为信号编号);
第 8 位:正常退出时的退出码(仅当低 7 位为 0 时有效);
辅助宏:
WIFEXITED(status):判断是否正常退出(低 7 位为 0);
WEXITSTATUS(status):提取正常退出的退出码(需先通过 WIFEXITED 判断);
WIFSIGNALED(status):判断是否被信号终止;
WTERMSIG(status):提取终止信号编号。
示例代码:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid < 0) return 1;
else if (pid == 0) {
sleep(2);
exit(10); // 子进程正常退出,退出码 10
}
int status;
pid_t ret = waitpid(pid, &status, 0); // 阻塞等待
if (ret > 0) {
if (WIFEXITED(status)) {
printf("Child exit code: %d\n", WEXITSTATUS(status)); // 输出 10
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal: %d\n", WTERMSIG(status));
}
}
return 0;
}
3.4 阻塞与非阻塞等待
3.4.1 阻塞等待
父进程暂停执行,直到子进程退出,适用于 “必须等待子进程完成” 的场景(如批处理任务):
// 阻塞等待子进程退出
waitpid(-1, &status, 0);
3.4.2 非阻塞等待
父进程不暂停,定期检查子进程状态,适用于 “需同时处理其他任务” 的场景(如服务器同时处理多个客户端):
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) return 1;
else if (pid == 0) {
sleep(5); // 子进程睡眠 5 秒
exit(1);
}
int status;
pid_t ret;
// 非阻塞等待:每隔 1 秒检查一次
while ((ret = waitpid(pid, &status, WNOHANG)) == 0) {
printf("Child is running...\n");
sleep(1);
}
if (WIFEXITED(status)) {
printf("Child exit code: %d\n", WEXITSTATUS(status));
}
return 0;
}
四、进程程序替换:exec 系列函数
fork 创建的子进程默认执行与父进程相同的代码,若需执行全新程序(如 ls、cat),需通过 exec 系列函数 替换进程的用户空间代码和数据。
4.1 程序替换的原理
exec 函数不创建新进程(PID 不变),而是:
- 清空当前进程的用户空间代码段和数据段;
- 从磁盘加载新程序的代码和数据到用户空间;
- 重置程序计数器(
eip),从新程序的入口(main函数前的启动代码)开始执行。
4.2 exec 函数簇解析
Linux 提供 6 个 exec 函数,核心差异在于参数格式、是否自动搜索 PATH、是否自定义环境变量,具体如下表:
| 函数名 | 参数格式 | 是否自动搜 PATH | 是否自定义环境变量 | 核心特点 |
|---|---|---|---|---|
execl | 列表(arg1,...NULL) | 否 | 否 | 适合参数固定的场景 |
execlp | 列表(arg1,...NULL) | 是 | 否 | 无需写全路径(如 execlp("ls", "ls", "-l", NULL)) |
execle | 列表(arg1,...NULL, envp) | 否 | 是 | 需手动传入环境变量数组 |
execv | 数组(argv []) | 否 | 否 | 适合参数数量不确定的场景 |
execvp | 数组(argv []) | 是 | 否 | 结合数组和 PATH 搜索,最常用 |
execve | 数组(argv [], envp []) | 否 | 是 | 唯一系统调用,其他函数基于它实现 |
函数名记忆规律:
l(list):参数为列表(需以 NULL 结尾);
v(vector):参数为数组(需以 NULL 结尾);
p(path):自动从 PATH 环境变量搜索程序;
e(env):自定义环境变量(默认使用父进程环境变量)。
4.3 常用 exec 函数示例
4.3.1 execlp:执行 ls -l(自动搜 PATH)
#include <unistd.h>
int main() {
// 格式:execlp(程序名, 程序名, 参数1, 参数2, ..., NULL)
execlp("ls", "ls", "-l", "-a", NULL);
// 若执行成功,以下代码不会执行
perror("execlp error");
return 1;
}
4.3.2 execvp:执行数组参数(适合动态参数)
#include <unistd.h>
int main() {
char *argv[] = {"ls", "-l", "-a", NULL}; // 数组需以 NULL 结尾
execvp("ls", argv); // 自动搜 PATH
perror("execvp error");
return 1;
}
4.3.3 execve:自定义环境变量
#include <unistd.h>
int main() {
char *argv[] = {"echo", "Hello", NULL};
// 自定义环境变量(仅包含 PATH 和 TERM)
char *envp[] = {"PATH=/bin", "TERM=console", NULL};
execve("/bin/echo", argv, envp); // 需写全路径
perror("execve error");
return 1;
}
五、实战:实现简易 Shell 命令行解释器
Shell 的核心是 “解析用户命令 → 创建子进程 → 程序替换 → 等待子进程退出”,结合前面的进程控制接口,我们可以实现一个支持基础命令的 Shell。
5.1 简易 Shell 的核心流程
- 打印提示符:如
[user@host dir]$; - 读取命令行:获取用户输入的命令(如
ls -l); - 解析命令:拆分命令名和参数(如
ls -l拆分为argv = {"ls", "-l", NULL}); - 执行命令:
- 内建命令(如
cd、export):Shell 直接执行(子进程执行会失效); - 外部命令(如
ls、cat):创建子进程,通过execvp替换程序;
- 内建命令(如
- 等待子进程退出:回收资源并记录退出码。
5.2 关键技术点
5.2.1 内建命令的处理
部分命令需修改 Shell 自身状态(如 cd 切换目录、export 设置环境变量),若由子进程执行会失效,需 Shell 直接处理:
cd:调用 chdir 系统调用切换当前进程的工作目录;
export:将环境变量加入 Shell 的环境变量数组;
echo $?:打印上一个进程的退出码。
5.2.2 命令解析
将用户输入的字符串(如 ls -a -l)拆分为参数数组,需处理空格分隔:
void parse_command(char *cmd, char **argv) {
int argc = 0;
argv[argc++] = strtok(cmd, " "); // 第一次调用传入命令字符串
while ((argv[argc++] = strtok(NULL, " "))); // 后续调用传入 NULL
argc--; // 去掉最后的 NULL
}
5.3 完整实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#define MAX_CMD_LEN 1024
#define MAX_ARGC 64
#define MAX_ENV 64
// 全局变量:命令参数、环境变量、上一个进程退出码
char *g_argv[MAX_ARGC];
char *g_env[MAX_ENV];
int g_last_code = 0;
// 初始化环境变量(从父 Shell 继承)
void init_env() {
extern char **environ;
int i = 0;
while (environ[i] && i < MAX_ENV - 1) {
g_env[i] = strdup(environ[i]); // 复制环境变量
i++;
}
g_env[i] = NULL;
}
// 打印命令提示符(如 [user@host dir]$)
void print_prompt() {
char user[32] = "unknown";
char host[64] = "unknown";
char pwd[1024] = "unknown";
getlogin_r(user, sizeof(user)); // 获取当前用户
gethostname(host, sizeof(host)); // 获取主机名
getcwd(pwd, sizeof(pwd)); // 获取当前工作目录
// 提取当前目录(如 /home/user/test → test)
char *dir = strrchr(pwd, '/');
dir = dir ? dir + 1 : pwd;
printf("[%s@%s %s]$ ", user, host, dir);
fflush(stdout); // 刷新缓冲,确保提示符立即显示
}
// 读取用户命令
int read_command(char *cmd) {
char *ret = fgets(cmd, MAX_CMD_LEN, stdin);
if (!ret) return 0; // EOF(如 Ctrl+D)
cmd[strlen(cmd) - 1] = '\0'; // 去掉换行符
return strlen(cmd) > 0; // 空命令返回 0
}
// 解析命令为参数数组
void parse_command(char *cmd) {
memset(g_argv, 0, sizeof(g_argv));
int argc = 0;
// 跳过开头空格
while (isspace(*cmd)) cmd++;
if (*cmd == '\0') return;
g_argv[argc++] = cmd;
// 拆分后续参数
while (*cmd && argc < MAX_ARGC - 1) {
if (isspace(*cmd)) {
*cmd = '\0';
cmd++;
while (isspace(*cmd)) cmd++;
if (*cmd) g_argv[argc++] = cmd;
} else {
cmd++;
}
}
g_argv[argc] = NULL;
}
// 执行内建命令(返回 1 表示内建命令,0 表示外部命令)
int exec_builtin() {
if (strcmp(g_argv[0], "cd") == 0) {
// cd 命令:切换工作目录
char *dir = g_argv[1] ? g_argv[1] : getenv("HOME");
if (chdir(dir) < 0) perror("cd error");
g_last_code = 0;
return 1;
} else if (strcmp(g_argv[0], "export") == 0) {
// export 命令:设置环境变量
if (g_argv[1]) {
int i = 0;
while (g_env[i]) i++;
if (i < MAX_ENV - 1) g_env[i] = strdup(g_argv[1]);
g_env[i + 1] = NULL;
}
g_last_code = 0;
return 1;
} else if (strcmp(g_argv[0], "echo") == 0) {
// echo 命令:打印参数或退出码
if (g_argv[1] && g_argv[1][0] == '$' && g_argv[1][1] == '?') {
printf("%d\n", g_last_code); // 打印上一个退出码
} else if (g_argv[1]) {
printf("%s\n", g_argv[1]); // 打印普通参数
}
g_last_code = 0;
return 1;
} else if (strcmp(g_argv[0], "exit") == 0) {
// exit 命令:退出 Shell
exit(g_last_code);
}
return 0; // 非内建命令
}
// 执行外部命令
void exec_external() {
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
g_last_code = 1;
return;
} else if (pid == 0) {
// 子进程:程序替换
execvp(g_argv[0], g_argv);
perror("execvp error"); // 替换失败才执行
exit(127); // 命令未找到
} else {
// 父进程:等待子进程退出
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
g_last_code = WEXITSTATUS(status);
} else {
g_last_code = 128 + WTERMSIG(status);
}
}
}
int main() {
char cmd[MAX_CMD_LEN];
init_env();
while (1) {
print_prompt(); // 1. 打印提示符
if (!read_command(cmd)) // 2. 读取命令
continue;
parse_command(cmd); // 3. 解析命令
if (!g_argv[0]) // 空命令跳过
continue;
if (exec_builtin()) // 4. 执行内建命令
continue;
exec_external(); // 5. 执行外部命令
}
return 0;
}
5.4 编译与测试
# 编译
gcc myshell.c -o myshell
# 运行
./myshell
# 测试命令
[user@host myshell]$ ls -l
[user@host myshell]$ cd ..
[user@host ~]$ echo $?
0
[user@host ~]$ export MYENV=hello
[user@host ~]$ exit
六、总结
进程控制是 Linux 系统编程的基石,核心要点如下:
- 进程创建:
fork通过写时拷贝实现资源复用,父子进程共享代码、独立数据; - 进程终止:
exit处理清理逻辑,_exit直接终止,退出码反映执行结果; - 进程等待:
wait/waitpid回收僵尸进程,通过status解析退出状态; - 程序替换:
exec函数簇替换用户空间代码,execvp因自动搜 PATH 最常用; - Shell 实现:核心是 “解析命令 → 内建命令直接执行 → 外部命令创建子进程替换”。

被折叠的 条评论
为什么被折叠?



