第三章:进程
第三章:进程
理解 POSIX 进程模型:进程生命周期、fork/exec/wait、进程组、会话管理。
3.1 POSIX 进程模型
3.1.1 什么是进程
在 POSIX 中,进程(Process) 是程序执行的实例。每个进程拥有:
| 资源 | 说明 |
|---|---|
| 地址空间 | 独立的虚拟内存布局(代码段、数据段、堆、栈) |
| 进程 ID (PID) | 唯一标识符,pid_t 类型 |
| 父进程 ID (PPID) | 创建该进程的父进程 PID |
| 文件描述符表 | 打开的文件/管道/套接字 |
| 信号处理表 | 各信号的处理方式 |
| 用户/组 ID | UID、GID(实际/有效/保存的) |
| 工作目录 | 当前工作目录 |
| 环境变量 | KEY=VALUE 键值对列表 |
3.1.2 进程生命周期
fork()
│
▼
子进程诞生 ──→ exec() 加载新程序 ──→ 运行中
│
exit() / 信号终止
│
▼
僵尸状态 (Zombie)
│
父进程 wait()
│
▼
进程消亡
3.1.3 进程终止状态
| 退出方式 | 说明 | 获取方法 |
|---|---|---|
exit(n) / return n | 正常退出,状态码 n(0-255) | WEXITSTATUS(status) |
| 信号终止 | 被信号杀死(如 SIGKILL) | WTERMSIG(status) |
| core dump | 信号终止 + 核心转储 | WCOREDUMP(status)(非 POSIX 标准但广泛可用) |
3.2 fork():创建子进程
fork() 是 POSIX 中创建新进程的唯一标准方式。它创建当前进程的副本——子进程。
3.2.1 fork() 特性
| 特性 | 说明 |
|---|---|
| 返回值(父进程) | 子进程的 PID(> 0) |
| 返回值(子进程) | 0 |
| 返回值(失败) | -1 |
| 内存 | 子进程获得父进程地址空间的副本(写时复制 COW) |
| 文件描述符 | 子进程共享父进程的文件描述符表 |
| 信号处理 | 子进程继承父进程的信号处理设置 |
3.2.2 基本 fork 示例
/*
* fork_basic.c - fork() 基础示例
* 编译: gcc -Wall -o fork_basic fork_basic.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int parent_var = 100;
printf("[父进程] PID=%d, PPID=%d\n", getpid(), getppid());
pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
/* 子进程代码 */
parent_var += 50; /* 修改副本,不影响父进程 */
printf("[子进程] PID=%d, PPID=%d, parent_var=%d\n",
getpid(), getppid(), parent_var);
return 42; /* 子进程退出码 */
} else {
/* 父进程代码 */
printf("[父进程] 创建了子进程 PID=%d, parent_var=%d\n",
pid, parent_var);
/* 等待子进程结束 */
int status;
pid_t child_pid = waitpid(pid, &status, 0);
if (child_pid == -1) {
perror("waitpid");
return EXIT_FAILURE;
}
if (WIFEXITED(status)) {
printf("[父进程] 子进程 %d 退出,状态码=%d\n",
child_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("[父进程] 子进程被信号 %d 终止\n",
WTERMSIG(status));
}
}
return EXIT_SUCCESS;
}
$ ./fork_basic
[父进程] PID=1234, PPID=1000
[父进程] 创建了子进程 PID=1235, parent_var=100
[子进程] PID=1235, PPID=1234, parent_var=150
[父进程] 子进程 1235 退出,状态码=42
注意:父进程和子进程的输出顺序是不确定的,取决于调度器。生产代码中需要使用同步机制(信号量、管道等)来协调顺序。
3.3 exec() 家族:替换进程映像
exec() 系列函数用新程序替换当前进程的代码段、数据段和栈,但保留 PID 和文件描述符(除非设置了 FD_CLOEXEC)。
3.3.1 exec 族函数对照
| 函数 | 路径 | 参数形式 | 环境 |
|---|---|---|---|
execl() | 完整路径 | 逐个参数,NULL 结尾 | 继承当前 |
execv() | 完整路径 | 数组 | 继承当前 |
execlp() | 使用 PATH 搜索 | 逐个参数,NULL 结尾 | 继承当前 |
execvp() | 使用 PATH 搜索 | 数组 | 继承当前 |
execle() | 完整路径 | 逐个参数,NULL 结尾 | 自定义 |
execvpe() | 使用 PATH 搜索 | 数组 | 自定义 |
命名记忆法:l=list(参数列表),v=vector(参数数组),p=PATH(搜索 PATH),e=environment(自定义环境)。
3.3.2 fork + exec 经典模式
/*
* fork_exec.c - fork() + execvp() 执行外部命令
* 编译: gcc -Wall -o fork_exec fork_exec.c
* 用法: ./fork_exec ls -la /tmp
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s <命令> [参数...]\n", argv[0]);
return EXIT_FAILURE;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
/* 子进程:执行命令 */
/* argv[1..] 作为命令和参数 */
execvp(argv[1], &argv[1]);
/* 如果 execvp 返回,说明出错 */
perror(argv[1]);
_exit(127); /* 命令未找到 */
} else {
/* 父进程:等待子进程 */
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return EXIT_FAILURE;
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
return 1;
}
}
$ ./fork_exec ls -la /tmp
total 48
drwxrwxrwt 15 root root 4096 May 10 10:00 .
drwxr-xr-x 20 root root 4096 Jan 1 00:00 ..
...
3.4 wait() 与 waitpid():回收子进程
3.4.1 状态检查宏
| 宏 | 说明 |
|---|---|
WIFEXITED(status) | 子进程正常退出? |
WEXITSTATUS(status) | 获取正常退出的状态码(0-255) |
WIFSIGNALED(status) | 子进程被信号终止? |
WTERMSIG(status) | 获取终止信号的编号 |
WIFSTOPPED(status) | 子进程被停止(如 SIGSTOP)? |
WSTOPSIG(status) | 获取停止信号的编号 |
WIFCONTINUED(status) | 子进程被 SIGCONT 继续? |
3.4.2 多子进程管理
/*
* multi_children.c - 创建多个子进程并等待
* 编译: gcc -Wall -o multi_children multi_children.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM_CHILDREN 5
int main(void)
{
pid_t children[NUM_CHILDREN];
/* 创建 5 个子进程 */
for (int i = 0; i < NUM_CHILDREN; i++) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
/* 子进程:模拟工作 */
printf(" 子进程 %d (PID=%d) 开始工作\n", i, getpid());
sleep(1 + i); /* 模拟不同耗时 */
printf(" 子进程 %d (PID=%d) 完成\n", i, getpid());
_exit(i); /* 以索引作为退出码 */
}
children[i] = pid;
printf("创建子进程 %d, PID=%d\n", i, pid);
}
/* 等待所有子进程 */
printf("\n等待所有子进程完成...\n");
for (int i = 0; i < NUM_CHILDREN; i++) {
int status;
pid_t pid = waitpid(children[i], &status, 0);
if (pid == -1) {
perror("waitpid");
continue;
}
if (WIFEXITED(status)) {
printf("子进程 PID=%d 退出码=%d\n",
pid, WEXITSTATUS(status));
}
}
printf("所有子进程已回收\n");
return EXIT_SUCCESS;
}
3.5 进程组与会话
3.5.1 概念层次
会话 (Session)
├── 前台进程组 (Foreground Process Group)
│ ├── 进程 A
│ └── 进程 B
└── 后台进程组 (Background Process Group)
├── 进程 C
└── 进程 D
| 概念 | 说明 | 获取函数 |
|---|---|---|
| 进程组 (Process Group) | 一组相关进程的集合 | getpgid(), getpgrp() |
| 会话 (Session) | 一个登录会话中的所有进程 | getsid() |
| 控制终端 (Controlling Terminal) | 会话关联的终端 | /dev/tty |
3.5.2 守护进程 (Daemon)
守护进程是在后台运行、不与终端关联的进程。POSIX 标准创建守护进程的步骤:
/*
* daemon.c - 创建 POSIX 守护进程
* 编译: gcc -Wall -o daemon daemon.c
* 运行: ./daemon; sleep 2; cat /tmp/posix_daemon.log; kill $(cat /tmp/posix_daemon.pid)
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
static volatile sig_atomic_t running = 1;
static void handle_signal(int sig)
{
(void)sig;
running = 0;
}
static int create_daemon(void)
{
/* 第一次 fork:脱离终端 */
pid_t pid = fork();
if (pid == -1) return -1;
if (pid > 0) _exit(EXIT_SUCCESS); /* 父进程退出 */
/* 创建新会话 */
if (setsid() == -1) return -1;
/* 第二次 fork:防止重新获取控制终端 */
pid = fork();
if (pid == -1) return -1;
if (pid > 0) _exit(EXIT_SUCCESS);
/* 设置文件权限掩码 */
umask(0);
/* 切换到根目录 */
if (chdir("/") == -1) return -1;
/* 关闭标准文件描述符,重定向到 /dev/null */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); /* stdin */
open("/dev/null", O_WRONLY); /* stdout */
open("/dev/null", O_WRONLY); /* stderr */
return 0;
}
int main(void)
{
if (create_daemon() == -1) {
perror("create_daemon");
return EXIT_FAILURE;
}
/* 设置信号处理 */
signal(SIGTERM, handle_signal);
signal(SIGINT, handle_signal);
/* 写入 PID 文件 */
FILE *f = fopen("/tmp/posix_daemon.pid", "w");
if (f) { fprintf(f, "%d\n", getpid()); fclose(f); }
/* 守护进程主循环 */
f = fopen("/tmp/posix_daemon.log", "a");
while (running) {
if (f) {
time_t now = time(NULL);
fprintf(f, "[%ld] daemon running, PID=%d\n",
(long)now, getpid());
fflush(f);
}
sleep(5);
}
if (f) {
fprintf(f, "daemon shutting down\n");
fclose(f);
}
unlink("/tmp/posix_daemon.pid");
return EXIT_SUCCESS;
}
3.6 _exit() 与 exit() 的区别
| 函数 | 头文件 | 刷新 stdio 缓冲区 | 调用 atexit 处理器 |
|---|---|---|---|
exit(status) | <stdlib.h> | ✅ 是 | ✅ 是 |
_exit(status) | <unistd.h> | ❌ 否 | ❌ 否 |
关键规则:在
fork()之后的子进程中,应使用_exit()而非exit(),以避免刷新父进程的 stdio 缓冲区(导致重复输出)。
3.7 环境变量操作
/*
* env_ops.c - POSIX 环境变量操作
* 编译: gcc -Wall -o env_ops env_ops.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ; /* POSIX 定义的全局环境变量数组 */
int main(void)
{
/* 1. 获取环境变量 */
const char *path = getenv("PATH");
printf("PATH = %s\n\n", path ? path : "(未设置)");
/* 2. 设置环境变量 */
setenv("MY_VAR", "hello_posix", 1); /* 1 = 覆盖已有值 */
printf("MY_VAR = %s\n", getenv("MY_VAR"));
/* 3. 修改环境变量 */
setenv("MY_VAR", "modified", 1);
printf("MY_VAR (修改后) = %s\n", getenv("MY_VAR"));
/* 4. 删除环境变量 */
unsetenv("MY_VAR");
printf("MY_VAR (删除后) = %s\n", getenv("MY_VAR"));
/* 5. 遍历所有环境变量(前 5 个) */
printf("\n前 5 个环境变量:\n");
for (int i = 0; environ[i] && i < 5; i++)
printf(" [%d] %s\n", i, environ[i]);
return 0;
}
3.8 业务场景:并行任务执行器
/*
* parallel_exec.c - 并行执行多个命令
* 编译: gcc -Wall -o parallel_exec parallel_exec.c
* 用法: ./parallel_exec "sleep 2" "sleep 1" "echo done"
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
static pid_t run_command(const char *cmd)
{
pid_t pid = fork();
if (pid == -1) { perror("fork"); return -1; }
if (pid == 0) {
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
perror("execl");
_exit(127);
}
return pid;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s \"cmd1\" \"cmd2\" ...\n", argv[0]);
return 1;
}
pid_t *pids = malloc(sizeof(pid_t) * (argc - 1));
if (!pids) { perror("malloc"); return 1; }
/* 并行启动所有命令 */
for (int i = 1; i < argc; i++) {
pids[i - 1] = run_command(argv[i]);
if (pids[i - 1] > 0)
printf("启动: \"%s\" (PID=%d)\n", argv[i], pids[i - 1]);
}
/* 等待所有命令完成 */
int failed = 0;
for (int i = 0; i < argc - 1; i++) {
if (pids[i] <= 0) continue;
int status;
waitpid(pids[i], &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
fprintf(stderr, "PID=%d 退出码=%d\n",
pids[i], WEXITSTATUS(status));
failed++;
}
}
printf("\n完成: %d/%d 成功\n", argc - 1 - failed, argc - 1);
free(pids);
return failed ? 1 : 0;
}
3.9 注意事项
⚠️ fork() 后的线程状态:
fork()只复制调用线程,其他线程不会在子进程中存在。在多线程程序中fork()需要特别小心,推荐在fork()后立即exec()。
⚠️ 僵尸进程:父进程不调用
wait()的子进程会变成僵尸进程(Zombie),占用进程表条目。使用SIGCHLD信号处理或waitpid(-1, &status, WNOHANG)来避免。
⚠️ fork() 与文件描述符:
fork()后父子进程共享文件描述符偏移量。一个进程的写入会影响另一个进程看到的偏移位置。
⚠️ exec() 后的 FD_CLOEXEC:默认情况下
exec()后文件描述符保持打开。使用fcntl(fd, F_SETFD, FD_CLOEXEC)标记不继承的文件描述符。
3.10 扩展阅读
man 2 fork、man 2 execve、man 2 waitpid- APUE 第 8 章:Process Control
- TLPI 第 24-26 章:Process Creation, Process Termination, Monitoring Child Processes
clone()系统调用:Linux 特有的进程/线程创建接口,是fork()的超集posix_spawn():POSIX.1-2001 引入的fork()+exec()替代方案,某些嵌入式环境更高效
3.11 本章小结
| 要点 | 说明 |
|---|---|
| fork() | 创建子进程,父子进程地址空间独立(COW) |
| exec() | 替换进程映像,PID 不变 |
| wait()/waitpid() | 回收子进程,获取退出状态 |
| exit() vs _exit() | 子进程中应使用 _exit() 避免刷新父进程缓冲 |
| 进程组 | 相关进程的集合,用于信号分发和作业控制 |
| 会话 | 登录会话,由 setsid() 创建 |
| 守护进程 | 双 fork + setsid + 重定向文件描述符 |