C/C++语言进阶-fork详解《原理、示例、父子进程看门狗、守护进程》
目录
🧮 五、fork 的内存复制机制(Copy-on-Write)
🧠 一、fork() 是什么?
fork() 是 Linux/Unix 系统调用,用于 创建一个新的进程。
-
由
<unistd.h>提供(在 Linux 系统中使用) -
新创建的进程称为 子进程(child process)
-
调用
fork()的原进程称为 父进程(parent process)
简单说:
fork()会复制当前进程,从此系统中存在两个几乎完全相同的进程。
-
"一次调用,两次返回":这是
fork()最核心也最令人困惑的特性。调用一次fork(),它会在父进程和子进程中各返回一次。-
在父进程中,
fork()返回新创建的子进程的进程ID(PID)(一个大于0的整数)。 -
在子进程中,
fork()返回 0。 -
如果创建子进程失败(例如系统进程数已达上限或内存不足),
fork()返回 -1。通过判断返回值,程序可以确定当前是在父进程还是子进程中,从而执行不同的代码分支。
-
-
执行顺序:调用
fork()之后,父进程和子进程是并发执行的,它们的运行顺序是不确定的,取决于内核的调度策略。
⚙️ 二、函数原型
#include <unistd.h>
pid_t fork(void);
返回值说明:
| 返回值 | 含义 |
|---|---|
| > 0 | 父进程中返回子进程的 PID |
| = 0 | 子进程中返回 0 |
| < 0 | 创建失败,返回 -1(通常是资源不足) |
🧩 三、fork() 的执行流程
调用 fork() 后,系统会:
-
复制当前进程的 地址空间(包括代码段、堆、栈等)
-
为子进程分配一个新的 PID
-
子进程与父进程从
fork()处继续执行后续语句
📘 注意:
虽然两者“代码一样”,但它们有 独立的内存空间。互不影响。
🔍 四、简单示例代码
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// 创建失败
perror("fork failed");
return 1;
}
else if (pid == 0) {
// 子进程逻辑
printf("我是子进程,PID=%d,父PID=%d\n", getpid(), getppid());
}
else {
// 父进程逻辑
printf("我是父进程,PID=%d,子PID=%d\n", getpid(), pid);
}
return 0;
}
运行输出示例:
我是父进程,PID=1000,子PID=1001
我是子进程,PID=1001,父PID=1000
注意:执行顺序不确定,因为父子进程是并发运行的。
🧮 五、fork 的内存复制机制(Copy-on-Write)
现代 Linux 中并不会立即复制整个内存空间,而是使用 COW(Copy on Write) 技术:
-
父子进程 共享同一份物理内存页(只读)
-
当任意一方尝试写入该内存页时,才复制出一份独立副本
✅ 优点:显著提高性能,避免浪费内存。
💡 示例:vfork() 和 posix_spawn() 也是为性能优化的变体。
-
COW 的核心思想:在调用
fork()的瞬间,内核并不会立即复制父进程的整个数据空间、堆和栈。相反,它让父子进程共享所有的物理内存页,并将这些页标记为只读。 -
触发复制:当任何一个进程(父或子)尝试修改这些共享的内存页时,CPU会触发一个页错误异常。此时,内核才会为该进程复制一份需要修改的内存页的副本,然后在这个新副本上进行修改。这样,另一个进程仍然使用原来的未修改的页。
-
COW 的优势:
-
高效创建:
fork()调用本身非常快,因为避免了不必要的数据复制。 -
节省内存:如果子进程在创建后立即调用
exec系列函数来执行另一个程序,那么之前共享的、可能很大的内存空间就会被完全替换,COW机制避免了复制这些根本不会被用到的数据,极大地节省了内存开销。
-
|
特性 |
父子进程关系 |
说明 |
|---|---|---|
|
代码段 |
共享 |
代码是只读的,因此父子进程共享同一份代码。 |
|
数据段、堆、栈 |
写时复制(COW) |
初始共享,修改时分离,保证数据独立性。 |
|
文件描述符 |
共享文件表项 |
子进程会复制父进程打开的文件描述符,但它们指向内核中相同的文件表项。这意味着父子进程会共享文件的当前偏移量。如果一个进程移动了文件指针,另一个进程也会受到影响。 |
|
进程ID(PID) |
不同 |
子进程拥有全新的、唯一的PID。 |
|
父进程ID(PPID) |
不同 |
子进程的PPID是其父进程的PID。 |
|
运行空间 |
独立 |
拥有独立的虚拟地址空间,互不干扰。 |
下面是一个简单的示例,演示了 fork()的基本用法和返回值判断:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
int global_var = 10; // 全局变量,用于演示COW
printf("Before fork: Process ID = %d, global_var = %d\n", getpid(), global_var);
pid_t pid = fork(); // 核心调用
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 这里是子进程代码块
printf("Child process: PID = %d, Parent PID = %d\n", getpid(), getppid());
global_var = 20; // 修改变量,触发COW
printf("Child process: modified global_var = %d\n", global_var);
exit(EXIT_SUCCESS); // 子进程退出
} else {
// 这里是父进程代码块
printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
waitpid(pid, NULL, 0); // 等待子进程结束,避免僵尸进程
printf("Parent process: after child modified, global_var = %d\n", global_var); // 仍然是10
}
return 0;
}
关键点分析:
-
返回值判断:通过
if-else结构严格区分父子进程的逻辑。 -
COW验证:子进程对
global_var的修改不会影响父进程中该变量的值,证明了内存空间的独立性。 -
等待子进程:父进程使用
waitpid()等待子进程结束,这是一种良好的编程实践,可以回收子进程资源,防止其变成“僵尸进程”(Zombie Process)。
🧰 六、常见用法
示例1:基础用法与进程区分
下面的代码展示了最基本的使用方法,并验证了父子进程的独立内存空间。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
int global_var = 10; // 全局变量,用于演示COW
printf("【父进程】fork调用前:进程ID = %d, global_var = %d\n", getpid(), global_var);
pid_t pid = fork(); // 核心调用!
// 错误处理必须最先判断
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 这里是子进程代码块 (pid == 0)
printf("【子进程】我的PID是:%d,我父进程的PID是:%d\n", getpid(), getppid());
global_var = 20; // 修改变量,这里会触发写时复制!
printf("【子进程】我修改了 global_var = %d\n", global_var);
printf("【子进程】我的任务完成了,即将退出。\n");
exit(EXIT_SUCCESS); // 子进程退出
} else {
// 这里是父进程代码块 (pid > 0, pid就是子进程的ID)
printf("【父进程】我的PID是:%d,我创建的子进程PID是:%d\n", getpid(), pid);
// 父进程等待子进程结束,避免其成为"僵尸进程"
int status;
waitpid(pid, &status, 0);
printf("【父进程】等待子进程结束。在我这里,global_var 仍然是:%d\n", global_var); // 验证独立性
printf("【父进程】子进程的退出状态是:%d\n", WEXITSTATUS(status));
}
printf("【进程 %d】这一行会被父子进程都执行一次,但他们是独立的进程!\n", getpid());
return 0;
}
编译与运行:
gcc -o fork_basic fork_basic.c
./fork_basic
可能的输出:
【父进程】fork调用前:进程ID = 1234, global_var = 10
【父进程】我的PID是:1234,我创建的子进程PID是:1235
【子进程】我的PID是:1235,我父进程的PID是:1234
【子进程】我修改了 global_var = 20
【子进程】我的任务完成了,即将退出。
【父进程】等待子进程结束。在我这里,global_var 仍然是:10
【父进程】子进程的退出状态是:0
【进程 1234】这一行会被父子进程都执行一次,但他们是独立的进程!
关键点分析:
-
返回值判断:通过
if-else结构严格区分父子进程的逻辑 。 -
COW验证:子进程对
global_var的修改不会影响父进程中该变量的值,证明了内存空间的独立性 。 -
等待子进程:父进程使用
waitpid()等待子进程结束,这是一种良好的编程实践,可以回收子进程资源,防止其变成“僵尸进程”(Zombie Process)。
示例2:循环创建子进程及控制
如果想创建多个子进程,直接使用循环可能会产生意想不到的结果。
错误示范:
#include <stdio.h>
#include <unistd.h>
int main() {
int i;
pid_t pid;
printf("【父进程】开始循环创建子进程...\n");
for (i = 0; i < 3; i++) {
pid = fork();
if (pid == 0) {
printf("子进程 %d 被创建,它的 i = %d\n", getpid(), i);
// 注意:子进程没有退出,它会继续循环!
} else if (pid > 0) {
printf("父进程创建了第 %d 个子进程,PID=%d\n", i, pid);
}
}
sleep(1); // 让所有进程有时间输出
return 0;
}
运行这个程序,你会发现创建的进程数量远多于3个(实际上是2^3=8个),因为子进程也会继续执行循环,创建"孙进程" 。
正确做法: 在子进程中及时使用 break或 exit跳出循环。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
int i;
pid_t pid;
printf("【父进程 PID=%d】开始创建3个子进程...\n", getpid());
for (i = 0; i < 3; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程:执行自己的任务后立即跳出循环,不再参与fork
printf(" 【子进程%d】我的PID是%d,我负责第%d项任务\n", i+1, getpid(), i+1);
sleep(1); // 模拟任务执行时间
break; // 或者直接 exit(0); 关键是要阻止子进程继续循环
}
// 父进程继续循环,创建下一个子进程
}
// 等待所有子进程结束 (父进程需要做的事情)
if (pid > 0) { // 只有父进程的pid才大于0
for (i = 0; i < 3; i++) {
wait(NULL); // 等待任何一个子进程结束
}
printf("【父进程】所有子进程任务已完成。\n");
} else {
// 子进程可以在这里执行更复杂的操作,然后退出
printf(" 【子进程%d, PID=%d】任务完成,退出。\n", i+1, getpid());
}
return 0;
}
示例3:fork + exec 执行新程序
这是 fork()最经典的用法之一:Shell就是这样运行命令的。子进程通过 exec系列函数来加载并执行一个全新的程序,替换掉自己原有的代码段、数据段等 ,但pid没有变。子进程调用 exec()成功后,它并没有变成一个“新的进程”,而是同一个进程“脱胎换骨”,执行了全新的程序。父进程 wait等待的,正是这个“改头换面”后的子进程的结束。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程:我要去执行 ls -l 命令了
printf("【子进程】我要变身去执行 'ls -l' 命令了!\n");
// 使用 execvp 执行命令,第一个参数是命令名,第二个是参数数组
char *args[] = {"ls", "-l", NULL}; // 参数列表必须以NULL结束
execvp("ls", args);
// 如果 execvp 成功,下面的代码绝不会被执行!
perror("execvp failed"); // 只有 exec 失败时才会执行到这里
exit(1); // 如果exec失败,子进程需要退出
} else {
// 父进程
printf("【父进程】我正在等待子进程执行完 'ls' 命令...\n");
waitpid(pid, NULL, 0); // 等待子进程结束
printf("【父进程】子进程的任务完成了。\n");
}
return 0;
}
示例4:简易并发网络服务器模型
这是 fork()另一个极其重要的应用场景:实现并发服务器 。下面是一个高度简化的逻辑模型:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
// 模拟处理客户端请求的函数
void handle_client(int client_id, const char *message) {
printf(" 【工作进程%d】开始处理请求:%s\n", client_id, message);
sleep(2); // 模拟处理请求需要时间
printf(" 【工作进程%d】请求处理完毕。\n", client_id);
}
int main() {
printf("【服务器主进程 PID=%d】启动,正在监听端口...\n", getpid());
int client_id = 0;
// 服务器主循环
while (client_id < 3) { // 假设只处理3个客户端连接以示例如
printf("【服务器】接受一个新的客户端连接 (#%d)...\n", ++client_id);
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
} else if (pid == 0) {
// 子进程:专门处理这个客户端的请求
char msg[100];
sprintf(msg, "Hello from Client %d", client_id);
handle_client(client_id, msg);
exit(0); // 处理完成,子进程退出
} else {
// 父进程:继续回到循环开头,监听下一个连接
printf("【服务器】已创建子进程 %d 来处理客户端 #%d 的请求,父进程继续监听...\n", pid, client_id);
// 父进程不需要立即wait,否则就变成串行处理了。
// 可以通过信号或非阻塞wait来避免僵尸进程,这里为简化使用sleep(1)略微等待子进程结束
}
sleep(1); // 模拟连接到达的间隔
}
// 父进程最后等待所有子进程
while (wait(NULL) > 0) { // 回收所有子进程
;
}
printf("【服务器】所有客户端请求处理完毕,服务器关闭。\n");
return 0;
}
⚠️ 关键注意事项总结
-
僵尸进程与孤儿进程:
-
僵尸进程:子进程终止后,如果父进程没有调用
wait()或waitpid()来回收其退出状态信息,该子进程的进程描述符仍然保留在系统中,成为“僵尸进程”。过多的僵尸进程会消耗系统资源。 -
孤儿进程:如果父进程先于子进程终止,子进程就会成为“孤儿进程”。孤儿进程会被 init 进程(PID=1) 自动收养,由 init 负责其善后工作。
-
-
缓冲区问题:标准I/O库(如
printf)通常带有缓冲区。如果fork()之前有未刷新(如没有换行符\n)的缓冲区数据,子进程会复制这份缓冲区。这可能导致输出内容出现重复或错乱。可以在fork()前使用fflush(stdout)来清空缓冲区。 -
连续调用 fork:在循环或条件判断中连续调用
fork()需要特别小心,否则容易产生指数级增长的进程数量,造成“fork炸弹”。务必确保子进程在创建后及时break循环或退出。 -
waitpid 使用方式详解:
阻塞等待 (options = 0)
父进程暂停执行,直到指定子进程结束
最简单直接的方式,适用于需要等待子进程完成的场景
示例:
waitpid(child_pid, &status, 0)非阻塞检查 (options = WNOHANG)
立即返回,不阻塞父进程
返回值:
0:子进程已结束,返回值为子进程PID
0:子进程仍在运行
-1:出错
适用于需要定期检查子进程状态的场景
示例:
waitpid(child_pid, &status, WNOHANG)监控停止进程 (options = WUNTRACED)
报告已停止的子进程状态(通常由SIGSTOP等信号引起)
使用
WIFSTOPPED(status)检查是否停止使用
WSTOPSIG(status)获取导致停止的信号示例:
waitpid(child_pid, &status, WUNTRACED)监控继续进程 (options = WCONTINUED)
报告已恢复运行的子进程状态(通常由SIGCONT信号引起)
使用
WIFCONTINUED(status)检查是否恢复运行示例:
waitpid(child_pid, &status, WCONTINUED)
七、看门狗及守护进程示例(重点)
1、实现父子进程看门狗
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>#define HEARTBEAT_TIMEOUT 30 // 心跳超时时间(秒)
#define RESTART_DELAY 3 // 重启延迟时间(秒)// 日志函数
void write_log(const char* format, ...) {
FILE* log_file = fopen("/tmp/watchdog.log", "a");
if (!log_file) return;
time_t now = time(NULL);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
va_list args;
va_start(args, format);
fprintf(log_file, "[%s] PID:%d ", timestamp, getpid());
vfprintf(log_file, format, args);
fprintf(log_file, "\n");
fflush(log_file);
va_end(args);
fclose(log_file);
}// 业务函数 - 用户需要实现
void business_main() {
while (1) {
// 模拟业务工作
sleep(5);
write_log("业务进程运行中");
}
}int main() {
int pipe_fd[2];
pid_t child_pid;
int status;
time_t last_heartbeat;
char heartbeat_msg[100];
write_log("=== 看门狗守护进程启动 ===");
// 创建心跳管道
if (pipe(pipe_fd) < 0) {
write_log("创建管道失败");
return 1;
}
// 设置管道为非阻塞
fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);
// 忽略常见信号
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGPIPE, SIG_IGN);while (1) {
write_log("启动子进程...");
child_pid = fork();
if (child_pid < 0) {
write_log("fork失败,5秒后重试");
sleep(5);
continue;
}
if (child_pid == 0) {
// 子进程:业务逻辑
close(pipe_fd[0]); // 关闭读端
// 设置管道写端为非阻塞
fcntl(pipe_fd[1], F_SETFL, O_NONBLOCK);
write_log("业务进程启动");
business_main();
exit(0);
}
// 父进程:监控逻辑
close(pipe_fd[1]); // 关闭写端
last_heartbeat = time(NULL);
while (1) {
int wait_result;
// 非阻塞检查子进程状态
wait_result = waitpid(child_pid, &status, WNOHANG);
if (wait_result == child_pid) {
// 子进程状态变化
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
write_log("子进程正常退出,代码:%d", exit_code);
break;
} else if (WIFSIGNALED(status)) {
int signal_num = WTERMSIG(status);
write_log("子进程被信号终止:%d", signal_num);
break;
}
} else if (wait_result == -1) {
write_log("waitpid错误");
break;
}
// 检查心跳
if (read(pipe_fd[0], heartbeat_msg, sizeof(heartbeat_msg)) > 0) {
last_heartbeat = time(NULL);
}
// 心跳超时检查
if (time(NULL) - last_heartbeat > HEARTBEAT_TIMEOUT) {
write_log("心跳超时,终止子进程");
kill(child_pid, SIGKILL);
sleep(1);
break;
}
sleep(1); // 避免CPU占用过高
}
write_log("等待%d秒后重启子进程", RESTART_DELAY);
sleep(RESTART_DELAY);
}
close(pipe_fd[0]);
write_log("=== 看门狗退出 ===");
return 0;
}
关键特性说明
-
心跳监控机制:
-
使用管道实现父子进程通信
-
父进程定期检查心跳信号
-
可配置的超时时间(HEARTBEAT_TIMEOUT)
-
-
进程状态监控:
-
使用waitpid非阻塞检查子进程状态
-
自动检测正常退出和信号终止
-
支持自动重启子进程
-
-
日志系统:
-
带时间戳的详细日志记录
-
日志文件保存在/tmp/watchdog.log
-
包含进程ID和详细状态信息
-
-
错误处理:
-
完善的fork失败处理
-
信号忽略防止意外中断
-
管道创建错误处理
-
2、守护进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdarg.h> // 添加可变参数支持#define LOG_FILE "/tmp/watchdog.log"
#define HEARTBEAT_TIMEOUT 30
#define RESTART_DELAY 3// 日志函数 - 修复可变参数问题
void write_log(const char* format, ...) {
FILE* log_file = fopen(LOG_FILE, "a");
if (!log_file) return;
time_t now = time(NULL);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
fprintf(log_file, "[%s] [PID:%d] ", timestamp, getpid());
va_list args;
va_start(args, format);
vfprintf(log_file, format, args);
va_end(args);
fprintf(log_file, "\n");
fclose(log_file);
}// 信号处理
volatile sig_atomic_t g_shutdown = 0;
void signal_handler(int sig) {
if (sig == SIGTERM || sig == SIGINT) {
write_log("接收到终止信号 %d,开始关闭", sig);
g_shutdown = 1;
}
}// 守护进程初始化
int daemon_init() {
pid_t pid = fork();
if (pid < 0) return -1;
if (pid > 0) exit(0);
if (setsid() < 0) return -1;
pid = fork();
if (pid < 0) return -1;
if (pid > 0) exit(0);
umask(0);
chdir("/");
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
return 0;
}// 业务函数(用户实现)
void business_main() {
// 示例业务逻辑
while (!g_shutdown) {
sleep(5);
write_log("业务进程运行中");
}
}// 启动子进程
pid_t start_child_process() {
pid_t pid = fork();
if (pid == 0) {
write_log("子进程启动");
business_main();
exit(0);
}
return pid;
}// 监控子进程
void monitor_child_process(pid_t child_pid) {
int status;
while (!g_shutdown) {
pid_t wait_result = waitpid(child_pid, &status, WNOHANG);
if (wait_result == child_pid) {
if (WIFEXITED(status)) {
write_log("子进程正常退出,状态码: %d", WEXITSTATUS(status));
break;
} else if (WIFSIGNALED(status)) {
write_log("子进程被信号终止: %d", WTERMSIG(status));
break;
}
} else if (wait_result == -1) {
write_log("waitpid错误: %s", strerror(errno));
break;
}
sleep(1); // 避免CPU占用过高
}
}int main() {
if (daemon_init() < 0) {
return 1;
}
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
write_log("守护进程启动");
while (!g_shutdown) {
pid_t child_pid = start_child_process();
if (child_pid > 0) {
monitor_child_process(child_pid);
}
sleep(RESTART_DELAY);
}
write_log("守护进程退出");
return 0;
}
关键功能说明
-
守护进程初始化:
-
通过两次fork创建标准守护进程
-
脱离终端控制,在后台运行
-
关闭标准输入输出,重定向到/dev/null
-
-
子进程监控:
-
使用waitpid非阻塞检查子进程状态
-
自动检测正常退出和信号终止
-
支持心跳超时检测(需业务代码实现)
-
-
自动重启机制:
-
子进程退出后自动重启
-
可配置重启延迟时间(RESTART_DELAY)
-
支持优雅关闭(SIGTERM/SIGINT)
-
-
日志系统:
-
带时间戳的详细日志记录
-
日志文件保存在/tmp/watchdog.log
-
包含进程ID和详细状态信息
-
八、面试常见问题
1. fork() 的基本特性是什么?
答案: fork()的核心特性是 “一次调用,两次返回”。
-
调用一次
fork(),系统会创建一个与父进程几乎完全相同的子进程。 -
返回值区分父子进程:
-
在 父进程 中返回 子进程的 PID(大于0的整数)
-
在 子进程 中返回 0
-
返回 -1 表示创建失败(如系统资源不足)。
-
2. 什么是写时复制(Copy-On-Write, COW)?它有什么优势?
答案: 写时复制是现代操作系统优化 fork()性能的关键机制。
-
工作原理:
-
fork()初始时,父子进程共享所有物理内存页,并将这些页标记为只读。 -
当任一进程(父或子)尝试修改共享内存时,才会触发真正的内存页复制。
-
-
优势:
-
高效创建:避免启动时复制大量内存,
fork()速度极快。 -
节省内存:未修改的数据始终共享,特别适合子进程后续立即调用
exec()的场景。
-
3. 先 open()再 fork()与先 fork()再 open(),文件描述符有何区别?
答案: 这是面试常见陷阱,关键区别在于文件描述符的继承方式。
|
操作顺序 |
文件描述符关系 |
文件偏移量 |
|---|---|---|
|
先 |
父子进程共享同一个文件描述符表项 |
共享偏移量(一个进程读写会影响另一个) |
|
先 |
父子进程各自打开文件,有独立的描述符 |
不共享偏移量(相互独立) |
代码验证要点:先open再fork后,父子进程交替读取文件,会看到它们轮流读取后续内容,而非重复读取开头,证明偏移量共享。
4. 循环中调用 fork()会产生多少进程?
答案: 进程数量会呈指数级增长。公式为 2^n(n 是 fork()调用次数)。
-
示例:
for (i = 0; i < 3; i++) { fork(); }最终会创建 2^3 = 8 个进程(包括原始进程)。 -
关键点:每次
fork()后,新创建的子进程也会继续执行后续的循环和fork()调用。
5. 什么是僵尸进程?如何避免和解决?
答案:
-
僵尸进程:子进程终止后,其退出状态信息未被父进程通过
wait()或waitpid()回收,导致其进程描述符仍占用系统进程表资源的状态。 -
解决方案:
-
父进程主动回收:在父进程中调用
waitpid(pid, &status, 0)。 -
信号处理:为
SIGCHLD信号设置处理函数,在函数中非阻塞地循环调用waitpid(-1, NULL, WNOHANG)来回收所有已终止的子进程。 -
终极手段:若父进程已无法修改,可杀死父进程(
kill -9 PPID),让子进程被 init 进程(PID=1) 收养并自动回收。
-
6. 在多线程程序中调用 fork()安全吗?
答案: 极其危险,不推荐。
-
fork()只会复制调用它的那个线程,而其他线程在子进程中会瞬间“消失”。 -
这可能导致子进程持有父进程中其他线程已加锁但未释放的锁,从而引发死锁。
-
最佳实践:如果必须在多线程环境中创建新进程,应使用
fork()后立即调用exec()执行新程序。
7. vfork()和 fork()有什么区别?
答案: vfork()是一个历史遗留的、为高效执行 exec()而设计的函数。
-
关键区别:
vfork()创建的子进程与父进程共享地址空间,且保证子进程先运行,直到其调用exec()或_exit()后父进程才被调度。 -
使用场景:仅在子进程立即调用
exec()系列函数时,为避免不必要的内存复制开销才考虑使用vfork()。现代 Linux 中fork()的 COW 机制已非常高效,vfork()的使用场景已大大减少。
8. wait()和 waitpid()有什么区别?WNOHANG选项有什么用?
答案:
-
wait():阻塞调用,父进程暂停执行,直到任意一个子进程结束。 -
waitpid():更灵活,可以指定要等待的特定子进程(通过PID),并可通过选项控制行为。 -
WNOHANG选项:使waitpid()变为非阻塞调用。如果指定的子进程没有结束,waitpid()会立即返回0,而不会让父进程等待。这使得父进程可以在等待子进程的同时继续处理其他任务,是实现“看门狗”或监控进程的关键。
9. 预测以下代码的输出并解释原因。
#include <stdio.h>
#include <unistd.h>
int main() {
fork();
fork();
fork();
printf("Hello\n");
return 0;
}
答案: 输出 8 个 "Hello"。
-
解释:3次
fork()调用,进程总数变为 2^3 = 8 个(包括原始进程)。每个进程都会执行printf语句。
10. 如何解析 waitpid()得到的 status信息?
答案: 使用预定义的宏来解析 status这个“信息包”:
-
WIFEXITED(status):判断子进程是否正常退出。 -
WEXITSTATUS(status):如果正常退出,提取子进程的退出码(即exit(5)中的5)。 -
WIFSIGNALED(status):判断子进程是否因信号而异常终止。 -
WTERMSIG(status):如果被信号终止,提取导致终止的信号编号(如SIGKILL为9)。
更多推荐



所有评论(0)