# 5 进程创建与管理 ## 5.1 完整概念讲解 ### 5.1.1 进程的基本概念 进程是程序的一次动态执行实例,是操作系统资源分配的基本单位。在Linux系统中,每个进程都有唯一的进程ID(PID),进程之间通过进程控制块(PCB)来管理。 Linux采用进程树的组织方式: - **init进程(PID=1)**:系统启动后的第一个用户态进程,是所有进程的祖先 - **systemd**:现代Linux发行版中init进程的实际实现 - **父进程与子进程**:通过fork创建的进程关系 ### 5.1.2 fork()详解 `fork()`是Linux/Unix系统中最核心的进程创建系统调用,它的特殊之处在于**调用一次,返回两次**。 **工作原理:** 1. 内核为新进程分配新的进程控制块(PCB)和唯一的PID 2. 复制父进程的地址空间、文件描述符表、信号处理等资源 3. 在父进程中返回子进程的PID(大于0) 4. 在子进程中返回0 **写时复制(Copy-On-Write, COW)机制:** 传统fork实现会立即复制整个父进程地址空间,效率极低。现代Linux采用写时复制技术: 1. **fork瞬间**:父子进程共享同一物理内存页,页表项标记为只读 2. **写入时触发缺页异常**:当任一进程尝试修改内存时,内核捕获异常 3. **按需复制**:仅复制被修改的页,其余继续共享 4. **优势**:大幅减少fork的开销,特别是对于大型进程 ```c // COW机制示意(伪代码) pid_t pid = fork(); if (pid == 0) { // 子进程修改变量时,内核才会真正复制该页 variable = 100; // 触发COW,复制该页 } ``` ### 5.1.3 fork后父子进程的区别 | 特性 | 父进程 | 子进程 | | ---------- | ----------------- | ----------------------- | | PID | 原PID不变 | 新分配的唯一PID | | PPID | 父进程的PID | 父进程的PID(两者相同) | | fork返回值 | 子进程的PID(>0) | 0 | | 文件描述符 | 保持原有FD | 复制父进程的FD表 | | 信号处理 | 原有设置 | 复制父进程的设置 | | 资源限制 | 原有限制 | 继承父进程的限制 | **文件描述符共享的关键点:** - fork后,父子进程共享文件偏移量(通过同一个文件表项) - 如果父进程打开文件,子进程读写同一文件,会影响彼此的偏移量 - 这一特性可用于父子进程间通信 ### 5.1.4 exec族函数 exec函数族用于**替换当前进程的映像**,加载并执行另一个程序。调用exec后,进程的代码段、数据段、堆栈都会被新程序替换,但PID保持不变。 **exec函数族分类:** | 函数 | 参数形式 | 搜索路径 | 使用场景 | | ------ | ------------------- | -------------- | ----------------- | | execl | 列表参数 | 需指定完整路径 | 固定路径执行 | | execv | 字符串数组 | 需指定完整路径 | 动态构建参数 | | execlp | 列表参数 | 自动搜索PATH | 执行常见命令 | | execvp | 字符串数组 | 自动搜索PATH | 动态构建+PATH搜索 | | execle | 列表参数+环境变量 | 需指定完整路径 | 自定义环境变量 | | execve | 字符串数组+环境变量 | 需指定完整路径 | 系统调用原语 | **exec返回值:** - 成功:不返回(直接跳转到新程序入口) - 失败:返回-1,设置errno ### 5.1.5 wait/waitpid `wait()`和`waitpid()`用于**回收子进程资源**,获取子进程退出状态。 **僵尸进程(Zombie Process):** - 子进程退出后,内核保留其PCB信息(PID、退出状态等) - 父进程通过wait调用读取这些信息后,子进程资源才会被完全释放 - 如果父进程不调用wait,子进程会变成僵尸进程,占用进程表项 **wait与waitpid的区别:** | 特性 | wait | waitpid | | -------- | ------------------------ | --------------------- | | 阻塞行为 | 一直阻塞直到有子进程退出 | 可设置WNOHANG非阻塞 | | 目标 | 任意子进程 | 可指定特定PID的子进程 | | 并发处理 | 一次只能处理一个 | 可处理多个子进程 | **退出状态宏:** - `WIFEXITED(status)`:子进程正常退出时返回非零 - `WEXITSTATUS(status)`:获取子进程的退出码(0-255) - `WIFSIGNALED(status)`:子进程被信号杀死时返回非零 - `WTERMSIG(status)`:获取导致子进程终止的信号 ### 5.1.6 进程状态 Linux系统中进程有以下主要状态: | 状态 | 符号 | 描述 | | ------------ | -------------------- | ----------------------------------- | | 运行/就绪 | R (Running/Runnable) | 正在运行或在运行队列等待CPU | | 可中断睡眠 | S (Sleeping) | 等待事件(如I/O完成),可被信号唤醒 | | 不可中断睡眠 | D (Disk Sleep) | 等待I/O完成,不可被信号唤醒 | | 停止 | T (Stopped) | 被信号(SIGSTOP/SIGTSTP)暂停 | | 僵尸 | Z (Zombie) | 已退出但资源未被父进程回收 | **查看进程状态:** ```bash # 使用ps命令查看 ps -eo pid,stat,cmd | head -20 # 使用top命令实时查看 top ``` ### 5.1.7 多进程编程模式 **典型模式:fork + exec + wait** 这是Unix/Linux系统中最经典的进程创建模式: 1. fork()创建子进程 2. 子进程中调用exec()执行新程序 3. 父进程中调用wait()等待子进程结束 ```c pid_t pid = fork(); if (pid == 0) { // 子进程 execl("/bin/ls", "ls", "-l", NULL); perror("execl failed"); exit(1); } else if (pid > 0) { // 父进程 int status; wait(&status); // 处理退出状态 } ``` ### 5.1.8 嵌入式应用场景 在嵌入式Linux应用开发中,多进程架构广泛应用于: 1. **命令执行**:Shell程序通过fork+exec执行外部命令 2. **守护进程**:后台服务进程,如Web服务器、数据库服务 3. **进程隔离**:不同功能模块运行在独立进程中,故障隔离 4. **并发处理**:处理多个客户端连接(如FTP服务器) 5. **任务调度**:定时执行特定任务(如crond) 6. **看门狗**:监控主程序运行状态,异常时重启 ## 5.2 核心API/语法 ### 5.2.1 fork()函数原型 ```c #include pid_t fork(void); ``` **参数说明:** - 无参数 **返回值:** - 成功:在父进程中返回子进程的PID(>0),在子进程中返回0 - 失败:返回-1,设置errno **errno常见值:** - `EAGAIN`:系统进程表已满或用户进程数达到上限 - `ENOMEM`:内核内存不足 ### 5.2.2 exec族函数原型 ```c #include // execl - 执行文件(列表参数) int execl(const char *path, const char *arg, ... /* (char *) NULL */); // execv - 执行文件(字符串数组) int execv(const char *path, char *const argv[]); // execlp - 按PATH搜索执行(列表参数) int execlp(const char *file, const char *arg, ... /* (char *) NULL */); // execvp - 按PATH搜索执行(字符串数组) int execvp(const char *file, char *const argv[]); // execle - 执行文件(带环境变量) int execle(const char *path, const char *arg, ... /* (char *) NULL, char *const envp[] */); // execve - 执行文件(系统调用原语) int execve(const char *path, char *const argv[], char *const envp[]); ``` **参数说明:** - `path`:要执行的程序的完整路径 - `file`:程序名,execvp/execle会自动搜索PATH环境变量 - `arg`:第一个参数通常是程序名 - `argv`:字符串数组,以NULL结尾 - `envp`:环境变量数组,以NULL结尾 **返回值:** - 成功:不返回 - 失败:返回-1,设置errno ### 5.2.3 wait/waitpid函数原型 ```c #include // wait - 等待任意子进程退出 pid_t wait(int *wstatus); // waitpid - 等待指定子进程退出 pid_t waitpid(pid_t pid, int *wstatus, int options); ``` **waitpid参数说明:** - `pid`:指定要等待的子进程PID - `pid > 0`:等待特定PID的子进程 - `pid = -1`:等待任意子进程(等同于wait) - `pid = 0`:等待与调用进程同组ID的子进程 - `pid < -1`:等待组ID等于|pid|的子进程 - `options`:选项标志 - `WNOHANG`:非阻塞模式,无子进程退出立即返回 - `WUNTRACED`:报告停止的子进程状态 - `WCONTINUED`:报告继续的子进程状态 **返回值:** - 成功:退出的子进程PID,或0(WNOHANG且无子进程退出) - 失败:返回-1,设置errno ### 5.2.4 退出状态宏定义 ```c #include // 判断子进程是否正常退出 int WIFEXITED(int status); // 获取子进程退出码(仅当WIFEXITED非零时有效) int WEXITSTATUS(int status); // 判断子进程是否被信号杀死 int WIFSIGNALED(int status); // 获取导致终止的信号编号 int WTERMSIG(int status); // 判断子进程是否被信号停止 int WIFSTOPPED(int status); // 获取导致停止的信号编号 int WSTOPSIG(int status); ``` ## 5.3 代码示例 ### 5.3.1 fork基础示例 ```c #include #include #include #include int main() { pid_t pid; int counter = 100; // 共享变量 printf("Before fork: pid=%d, counter=%d\n", getpid(), counter); pid = fork(); if (pid < 0) { // fork失败 perror("fork failed"); exit(1); } else if (pid == 0) { // 子进程 counter += 10; printf("[Child] pid=%d, ppid=%d, counter=%d\n", getpid(), getppid(), counter); exit(0); } else { // 父进程 counter += 20; printf("[Parent] pid=%d, child_pid=%d, counter=%d\n", getpid(), pid, counter); wait(NULL); // 等待子进程结束 } return 0; } ``` **编译运行:** ```bash gcc -o fork_example fork_example.c ./fork_example ``` **输出示例:** ``` Before fork: pid=12345, counter=100 [Child] pid=12346, ppid=12345, counter=110 [Parent] pid=12345, child_pid=12346, counter=120 ``` **说明:** fork后,父子进程各有一份counter的副本,互不影响。 ### 5.3.2 文件描述符共享示例 ```c #include #include #include #include #include #include #include #include #define FILENAME "shared_file.txt" int main() { pid_t pid; int fd; char buf[128]; // 创建并写入文件 fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644); write(fd, "Hello", 5); close(fd); // 以读写方式打开文件,fork后父子进程共享此fd fd = open(FILENAME, O_RDWR); pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } else if (pid == 0) { // 子进程:在文件末尾追加写入 lseek(fd, 0, SEEK_END); // 移动到文件末尾 write(fd, " World", 6); printf("[Child] wrote ' World'\n"); close(fd); exit(0); } else { // 父进程:等待子进程写入后读取 sleep(1); // 等待子进程写入 lseek(fd, 0, SEEK_SET); // 移动到文件开头 memset(buf, 0, sizeof(buf)); read(fd, buf, sizeof(buf) - 1); printf("[Parent] read: '%s'\n", buf); close(fd); wait(NULL); } return 0; } ``` **编译运行:** ```bash gcc -o fd_share fd_share.c ./fd_share ``` **输出:** ``` [Child] wrote ' World' [Parent] read: 'Hello World' ``` **说明:** fork后父子进程共享文件偏移量,因此子进程写入后父进程能读取到完整内容。 ### 5.3.3 exec族函数示例 ```c #include #include #include #include #include int main() { pid_t pid; // 示例1:execl执行ls命令 printf("=== Example 1: execl ===\n"); pid = fork(); if (pid == 0) { // execl: 需要指定完整路径,参数逐个传递,以NULL结尾 execl("/bin/ls", "ls", "-l", "-a", NULL); perror("execl failed"); // 只有execl失败才会执行到这里 exit(1); } wait(NULL); // 示例2:execv执行ls命令(参数通过数组传递) printf("\n=== Example 2: execv ===\n"); pid = fork(); if (pid == 0) { char *args[] = {"ls", "-l", "-h", NULL}; execv("/bin/ls", args); perror("execv failed"); exit(1); } wait(NULL); // 示例3:execlp按PATH搜索执行 printf("\n=== Example 3: execlp ===\n"); pid = fork(); if (pid == 0) { // execlp: 自动搜索PATH环境变量 execlp("ls", "ls", "-l", "/tmp", NULL); perror("execlp failed"); exit(1); } wait(NULL); // 示例4:execvp按PATH搜索执行 printf("\n=== Example 4: execvp ===\n"); pid = fork(); if (pid == 0) { char *args[] = {"ls", "-l", "-t", NULL}; // execvp: 自动搜索PATH,参数通过数组传递 execvp("ls", args); perror("execvp failed"); exit(1); } wait(NULL); // 示例5:execle设置环境变量 printf("\n=== Example 5: execle ===\n"); pid = fork(); if (pid == 0) { char *env[] = {"MY_VAR=hello_world", NULL}; execle("/bin/env", "env", NULL, env); perror("execle failed"); exit(1); } wait(NULL); printf("\nAll exec examples completed.\n"); return 0; } ``` **编译运行:** ```bash gcc -o exec_example exec_example.c ./exec_example ``` ### 5.3.4 waitpid与退出状态处理示例 ```c #include #include #include #include #include void print_exit_status(int status) { if (WIFEXITED(status)) { printf(" 正常退出,退出码: %d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf(" 被信号杀死,信号: %d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf(" 被信号停止,信号: %d\n", WSTOPSIG(status)); } } int main() { pid_t pid1, pid2; int status; // 创建第一个子进程:正常退出 pid1 = fork(); if (pid1 == 0) { printf("[Child1] PID=%d, PPID=%d\n", getpid(), getppid()); sleep(1); printf("[Child1] exiting with code 42\n"); exit(42); // 退出码42 } // 创建第二个子进程:被信号杀死 pid2 = fork(); if (pid2 == 0) { printf("[Child2] PID=%d, PPID=%d\n", getpid(), getppid()); sleep(2); printf("[Child2] being killed by SIGSEGV\n"); char *p = NULL; *p = 'x'; // 故意触发段错误 exit(1); } // 父进程使用waitpid等待特定子进程 printf("[Parent] Waiting for child1 (PID=%d)...\n", pid1); waitpid(pid1, &status, 0); // 阻塞等待pid1 printf("[Parent] Child1 status:"); print_exit_status(status); printf("[Parent] Waiting for child2 (PID=%d)...\n", pid2); waitpid(pid2, &status, 0); // 阻塞等待pid2 printf("[Parent] Child2 status:"); print_exit_status(status); // 非阻塞等待示例 printf("\n[Parent] Non-blocking wait example:\n"); pid_t pid3 = fork(); if (pid3 == 0) { printf("[Child3] sleeping 3 seconds\n"); sleep(3); exit(0); } // 轮询等待 while (1) { pid_t result = waitpid(pid3, &status, WNOHANG); if (result == 0) { printf(" [Parent] Child3 still running...\n"); sleep(1); } else if (result > 0) { printf(" [Parent] Child3 exited with code %d\n", WEXITSTATUS(status)); break; } else { perror("waitpid failed"); break; } } return 0; } ``` **编译运行:** ```bash gcc -o waitpid_example waitpid_example.c ./waitpid_example ``` ### 5.3.5 多进程服务器模型示例(嵌入式应用) ```c #include #include #include #include #include #include #include #define MAX_CLIENTS 5 // 信号处理函数:回收子进程 void sigchld_handler(int sig) { int saved_errno = errno; // 使用WNOHANG非阻塞回收,避免阻塞 while (waitpid(-1, NULL, WNOHANG) > 0) { continue; } errno = saved_errno; } void handle_client(int client_id) { printf("[Client Handler %d] PID=%d, handling client %d\n", client_id, getpid(), client_id); // 模拟处理客户端请求 for (int i = 0; i < 3; i++) { printf(" [Client %d] Processing request %d...\n", client_id, i + 1); sleep(1); } printf(" [Client %d] Done\n", client_id); exit(0); } int main() { pid_t pids[MAX_CLIENTS]; // 注册SIGCHLD信号处理函数 struct sigaction sa; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; sigaction(SIGCHLD, &sa, NULL); printf("[Server] PID=%d, starting multi-process server\n", getpid()); // 模拟接受5个客户端连接,创建子进程处理 for (int i = 0; i < MAX_CLIENTS; i++) { pids[i] = fork(); if (pids[i] < 0) { perror("fork failed"); exit(1); } else if (pids[i] == 0) { // 子进程:处理客户端 handle_client(i); } else { // 父进程:记录子进程PID printf("[Server] Forked child process PID=%d for client %d\n", pids[i], i); } } // 父进程:处理其他任务(如接受新连接) printf("[Server] Main server running, waiting for children...\n"); // 等待一段时间让子进程完成 sleep(8); printf("[Server] Server shutting down\n"); return 0; } ``` **编译运行:** ```bash gcc -o multiproc_server multiproc_server.c ./multiproc_server ``` ## 5.4 注意事项与易错点 ### 5.4.1 fork使用注意事项 | 问题 | 描述 | 解决方案 | | ---------------------------- | ------------------------ | --------------------------------------------------- | | fork后未检查返回值 | 无法区分父进程和子进程 | 必须检查返回值:pid<0失败,pid=0子进程,pid>0父进程 | | fork后未处理错误 | 可能导致程序行为异常 | 失败时及时exit或错误处理 | | fork后子进程未调用exec或exit | 子进程继续执行父进程代码 | 明确子进程的执行路径 | | fork后未wait子进程 | 产生僵尸进程 | 使用wait/waitpid或SIGCHLD处理 | | fork后全局变量不确定 | 输出顺序与调度顺序有关 | 不要依赖父子进程的执行顺序 | ### 5.4.2 exec使用注意事项 | 问题 | 描述 | 解决方案 | | -------------- | ----------------------------- | ------------------------------- | | exec失败未处理 | 后续代码会继续执行 | exec成功不返回,失败需检查errno | | 参数传递错误 | 最后一个参数必须是NULL | 列表形式:execl(..., NULL) | | 路径错误 | 找不到可执行文件 | 使用execlp/execvp自动搜索PATH | | 权限问题 | 可执行文件无执行权限 | 检查文件权限:chmod +x | | 环境变量问题 | execle/execve需要手动传递环境 | 使用environ或自定义env数组 | ### 5.4.3 wait/waitpid使用注意事项 | 问题 | 描述 | 解决方案 | | -------------- | ------------------------------ | --------------------------- | | 忘记调用wait | 僵尸进程堆积,占用进程表 | 及时回收子进程 | | wait阻塞问题 | 多个子进程时阻塞等待 | 使用waitpid+WNOHANG非阻塞 | | 状态宏使用错误 | WIFEXITED为假时使用WEXITSTATUS | 先判断WIFEXITED再获取退出码 | | 信号竞争 | SIGCHLD信号可能在wait前到达 | 使用SA_RESTART或sigwait | | 僵尸进程处理 | 未注册SIGCHLD处理函数 | 使用sigaction注册信号处理 | ### 5.4.4 常见错误代码示例 ```c // 错误示例1:未检查fork返回值 pid_t pid = fork(); // 危险!pid可能是-1 // 正确做法 pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(1); } // 错误示例2:exec失败后继续执行 if (pid == 0) { execl("/bin/ls", "ls", NULL); // 如果execl失败,这里会继续执行 printf("This will execute if execl fails\n"); exit(1); // 必须exit,否则子进程会继续执行父进程代码 } // 错误示例3:僵尸进程未回收 if (pid == 0) { exit(0); // 子进程退出 } // 父进程未调用wait,子进程变成僵尸 sleep(10); // 僵尸进程占用进程表项 ``` ### 5.4.5 嵌入式场景注意事项 1. **资源限制**:嵌入式系统资源有限,避免创建过多子进程 2. **内存管理**:注意COW机制可能的内存开销 3. **信号处理**:使用sigaction替代signal,避免竞态条件 4. **守护进程**:使用double fork创建守护进程 5. **进程监控**:实现看门狗机制,监控子进程健康状态 ## 5.5 面试要点 ### Q1: fork()的返回值是什么?为什么能返回两次? **答:** - fork()成功后,**父进程返回子进程的PID(>0)**,**子进程返回0** - 失败时返回-1 - 能返回两次的原因:fork通过复制父进程创建子进程,两个进程都从fork调用点继续执行,但内核在不同进程中设置不同的返回值 ### Q2: 什么是写时复制(COW)?有什么优势? **答:** - **定义**:fork时不立即复制整个地址空间,而是让父子进程共享物理内存页,页表项标记为只读 - **触发条件**:当任一进程尝试写入共享页时,触发缺页异常,内核才真正复制该页 - **优势**:大幅减少fork的开销,特别是对于大型进程(如占用GB内存的程序) - **实现**:Linux通过页表项的写保护位实现,是现代操作系统的标准优化 ### Q3: fork后父子进程有哪些区别? **答:** | 区别 | 父进程 | 子进程 | | ---------- | ---------- | ---------------------------------- | | PID | 原PID | 新分配的唯一PID | | fork返回值 | 子进程PID | 0 | | 文件描述符 | 独立的FD表 | 复制父进程的FD表(共享文件偏移量) | | 信号处理 | 原有设置 | 复制父进程的设置 | | 资源限制 | 原有限制 | 继承父进程的限制 | ### Q4: exec函数族的作用是什么?为什么成功不返回? **答:** - **作用**:替换当前进程的映像,加载并执行另一个程序 - **为什么不返回**:成功时,进程的代码段、数据段、堆栈都被新程序替换,控制权转移到新程序入口点,原程序的返回地址已不存在 - **失败情况**:如果找不到程序、权限不足等,exec返回-1,原程序继续执行 ### Q5: 僵尸进程是如何产生的?如何避免? **答:** - **产生原因**:子进程退出后,内核保留其PCB信息等待父进程读取;如果父进程不调用wait,子进程变成僵尸 - **危害**:占用进程表项,长期积累可能导致系统无法创建新进程 - **避免方法**: 1. 父进程及时调用wait/waitpid回收 2. 注册SIGCHLD信号处理函数,自动回收 3. 使用double fork:孙进程由init回收 4. 父进程忽略SIGCHLD信号:`signal(SIGCHLD, SIG_IGN)` ### Q6: wait和waitpid有什么区别? **答:** | 特性 | wait | waitpid | | -------- | -------------- | ------------------- | | 阻塞 | 一直阻塞 | 可设置WNOHANG非阻塞 | | 目标 | 任意子进程 | 可指定特定PID | | 并发 | 一次处理一个 | 可处理多个子进程 | | 使用场景 | 简单的单子进程 | 复杂的多子进程管理 |