1. 进程创建基础 (fork 函数)
1.1 函数接口与头文件
- 函数名称:
fork - 所需头文件:
#include <sys/types.h>#include <unistd.h>
- 函数原型:
pid_t fork(void); - 参数: 无参数。
- 返回值:
pid_t(进程 ID 类型,通常为整型)。- 父进程 (Parent Process): 返回子进程的 PID (大于 0)。
- 子进程 (Child Process): 返回 0。
- 错误: 返回 -1。
1.2 基本执行逻辑
- 调用
fork()后,系统会创建一个新的进程(子进程)。 - 代码执行点:
fork()之后的代码会被两个进程(父进程和子进程)同时执行。 - 示例代码:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf("Hello\n"); fork(); printf("Hello\n"); return 0; } - 现象:
fork()之前的printf执行一次。fork()之后的printf执行两次(因为有两个进程)。- 输出顺序不确定: 由于进程调度,父进程、子进程和终端 Shell (Bash) 可能交错输出。例如,可能先打印父进程内容,然后 Bash 打印提示符,最后子进程打印内容。
1.3 进程 ID (PID) 与返回值判断
- 通过判断
fork()的返回值可以区分当前代码是在父进程还是子进程中运行。 - 典型用法:
pid_t pid = fork(); if (pid < 0) { // 错误处理 perror("fork error"); return -1; } else if (pid > 0) { // 父进程逻辑 (pid 为子进程的 PID) printf("Parent PID: %d\n", pid); } else if (pid == 0) { // 子进程逻辑 printf("Child PID: 0\n"); } - 注意: 在多进程环境下,共享终端输出时,内容可能会 interleaved (交错)。
2. 多次调用 fork 与进程结构控制
2.1 循环中多次 fork 的问题
- 如果在循环中直接调用
fork(),进程数量会呈指数级增长。 - 示例:
for (int i = 0; i < 4; i++) { fork(); } - 结果: 并非只创建 4 个进程。因为子进程创建后也会继续执行循环,导致进程数量爆炸(理论上接近 2^n 级别,具体取决于循环逻辑)。
- 验证方法:
- 使用
ps -ef | grep <进程名>查看进程树。 - 使用
pstree -p <PID>查看进程层级关系。 - 使用
kill -9 <PID>清理残留进程。
- 使用
2.2 构建进程链 (Process Chain)
- 需求: 父进程生子进程,子进程生孙进程,形成线性链条 (Parent -> Child -> Grandchild)。
- 实现逻辑: 父进程创建子进程后,父进程退出循环,只有子进程继续循环。
- 代码关键:
for (int i = 0; i < 4; i++) { pid_t pid = fork(); if (pid > 0) { // 父进程:跳出循环,不再创建新进程 break; } // 子进程:继续循环,创建下一代 } - 效果: 每次循环只有当前最新的子进程会继续执行
fork,形成单链结构。
2.3 父进程创建多个子进程 (One-to-Many)
- 需求: 一个父进程直接创建多个子进程,子进程不再创建后代 (星型结构)。
- 实现逻辑: 子进程创建后,子进程退出循环,只有父进程继续循环。
- 代码关键:
for (int i = 0; i < 4; i++) { pid_t pid = fork(); if (pid == 0) { // 子进程:跳出循环,不再创建新进程 break; } // 父进程:继续循环,创建下一个子进程 } - 效果: 父进程循环 4 次,创建 4 个独立的子进程,所有子进程的父进程 ID (PPID) 相同。
3. 进程终止方式 (return, exit, _exit)
3.1 main 函数中的 return
- 在
main函数中,return 0;等同于调用exit(0);。 - 底层逻辑是调用
exit函数来结束进程。 - 返回值检查: 在 Shell 中可通过
echo $?查看上一个进程的退出状态码。
3.2 exit 函数 (标准库函数)
- 头文件:
#include <stdlib.h> - 性质: C 标准库函数 (POSIX 标准)。
- 功能:
- 刷新缓冲区: 会刷新
stdio流的缓冲区 (Buffer Flushing)。 - 终止进程: 调用底层系统调用结束进程。
- 刷新缓冲区: 会刷新
- 示例现象:
- 若代码中有
printf("Hello")(无\n),数据留在缓冲区。 - 调用
fork()后,父子进程各自拥有一份缓冲区副本。 - 若父子进程都调用
exit(),缓冲区会被刷新两次,导致 "Hello" 打印两次。
- 若代码中有
3.3 _exit 函数 (系统调用)
- 头文件:
#include <unistd.h> - 性质: 系统调用 (System Call)。
- 功能:
- 不刷新缓冲区: 直接终止进程,遗留缓冲区数据。
- 终止进程: 由内核直接结束进程。
- 示例现象:
- 若子进程中调用
_exit(0),子进程缓冲区不刷新。 - 只有父进程正常结束时刷新缓冲区,"Hello" 可能只打印一次。
- 若子进程中调用
- 区别总结:
exit: 库函数,刷新缓冲区,底层调用_exit。_exit: 系统调用,不刷新缓冲区,直接终止。
3.4 退出状态码的限制
- 有效位数: 进程退出状态码只有低 8 位 有效 (0-255)。
- 现象:
- 若
exit(1000),Shell 获取到的值可能是1000 % 256的结果。 - 例如
exit(256)可能返回 0,exit(257)可能返回 1。
- 若
- 建议: 返回值应在 0-255 范围内,通常 0 表示成功,非 0 表示错误。
4. 异常终止与信号
4.1 正常终止 vs 异常终止
- 正常终止: 通过
return,exit,_exit主动结束。 - 异常终止: 通过信号 (Signal) 强制结束。
- 例如:在终端按
Ctrl + C发送SIGINT信号。
- 例如:在终端按
- 返回值差异:
- 正常退出返回设定的状态码 (如 0)。
- 信号终止返回的状态码通常大于 128 (例如
128 + 信号值),表示非正常退出。
4.2 父进程管理子进程
- 父进程需要知道子进程是如何结束的 (状态码、是否异常)。
- 这涉及到进程等待与状态收集 (Wait/Waitpid),将在后续课程中探讨。
5. 实验与调试技巧
5.1 进程查看命令
ps -ef | grep <进程名>: 查找特定进程。pstree -p <PID>: 以树状图显示进程层级及 PID。ps -o pid,ppid,cmd: 查看进程 ID、父进程 ID 和命令。
5.2 后台运行与清理
- 后台运行: 编译运行后按
Ctrl + Z暂停,再输入bg让进程在后台运行,避免阻塞终端。 - 清理进程:
- 使用
kill -9 <PID>强制杀死进程。 - 注意清理父进程及其所有子进程,避免僵尸进程或残留进程干扰后续实验。
- 使用
5.3 缓冲区实验细节
- 代码示例:
printf("Hello"); // 无换行符,数据在缓冲区 fork(); exit(0); // 父子进程都会刷新缓冲区,输出两次 Hello - 修改为
_exit:printf("Hello"); fork(); if (pid == 0) { _exit(0); // 子进程不刷新缓冲区 } // 只有父进程最后退出时刷新,输出一次 Hello - 睡眠测试: 若子进程
sleep(10)后再_exit,可观察到父进程先结束输出,10 秒后子进程结束但无额外输出(因为_exit不刷新)。
6. 总结
fork返回值判断: 必须严格判断pid > 0,pid == 0,pid < 0三种情况。- 循环
fork控制: 务必使用break控制父子进程的循环行为,否则会导致进程数量失控。 - 终止函数选择:
- 一般情况使用
exit或return,确保缓冲区数据写入。 - 特殊场景(如守护进程、避免重复输出)可使用
_exit。
- 一般情况使用
- 资源清理: 实验结束后务必使用
kill清理残留进程,保持环境干净。 - 状态码规范: 遵循 0-255 的返回值规范,便于 Shell 脚本判断执行结果。