本文档配合前面的教程使用。
- 题目按难度递进,覆盖 fork、exec、wait/waitpid、僵尸进程、文件描述符共享、GDB 调试等。
- 每题给出题目、考察点、参考答案、易错提醒。
- 建议先自己做,再看答案。
目录
- 第一部分:基础概念题
- 第二部分:fork 基础编程题
- 第三部分:fork 进阶与易错题
- 第四部分:exec 编程题
- 第五部分:wait / waitpid 回收题
- 第六部分:僵尸进程与孤儿进程
- 第七部分:文件描述符与共享
- 第八部分:GDB 调试题
- 第九部分:综合操作题
- 第十部分:面试模拟题
- 附录:答案速查与自测清单
第一部分:基础概念题
题目 1
问:程序和进程有什么区别?
考察点:基本概念。
参考答案:
程序是磁盘上的静态可执行文件,进程是程序的一次运行实例。进程有独立的 PID、虚拟地址空间、文件描述符、PCB 等。同一个程序可以运行出多个进程。
易错提醒:不要只说“程序是静态的,进程是动态的”,要说出进程有独立资源。
题目 2
问:什么是 PCB?它包含哪些内容?为什么僵尸进程会占用 PCB?
考察点:PCB 理解。
参考答案:
PCB 是内核管理进程的数据结构,Linux 中对应 task_struct。包含 PID、PPID、状态、寄存器上下文、虚拟内存信息、打开文件表、信号信息、调度信息、父子关系等。
子进程退出后,内核把退出状态保存在 PCB 中,等待父进程读取。父进程未调用 wait / waitpid 前,PCB 不释放,所以僵尸进程占用 PCB 和 PID。
易错提醒:僵尸进程不占 CPU 和内存空间,但占 PCB 和 PID。
题目 3
问:fork 的返回值是什么?为什么这样设计?
考察点:fork 返回值。
参考答案:
- 父进程返回子进程 PID,大于 0。
- 子进程返回 0。
- 失败返回 -1,
errno说明原因。
父进程可能有多个子进程,必须知道新建的是哪个。子进程只有一个父进程,用 getppid 就能拿到,所以返回 0 表示“我是子进程”。
易错提醒:不要写“父进程返回 0,子进程返回 PID”。
题目 4
问:什么是写时复制?为什么 fork 要用它?
考察点:COW 原理。
参考答案:
写时复制是 fork 后父子进程先共享物理页,页表项标记只读。某一方写入时触发页保护异常,内核复制出新页,修改页表,恢复可写。
好处:fork 快,省内存。很多子进程马上 exec,复制的内存根本用不上。
易错提醒:COW 以页为单位,通常 4KB。
题目 5
问:fork 后父子进程共享哪些内容?不共享哪些?
考察点:父子共享。
参考答案:
共享或继承:代码段、文件描述符表、打开文件描述、环境变量副本、工作目录、用户 ID、信号处理方式。
不共享:PID、PPID、虚拟地址空间、栈、堆、全局变量、寄存器、未决信号、定时器。
易错提醒:文件描述符表是复制的,但 fd 指向同一个打开文件描述,共享文件偏移。
第二部分:fork 基础编程题
题目 6
题目:写一个程序,使用 fork 创建子进程。父进程打印自己的 PID 和子进程 PID,子进程打印自己的 PID 和父进程 PID。
考察点:fork 基本使用、getpid、getppid。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
printf("child: pid=%d, ppid=%d\n", getpid(), getppid());
_exit(0);
} else {
printf("parent: pid=%d, child=%d\n", getpid(), pid);
wait(NULL);
}
return 0;
}易错提醒:
- 子进程用
_exit,避免刷新父进程缓冲区。 - 父进程要
wait,避免僵尸。
题目 7
题目:写一个程序,验证 fork 后父子进程的全局变量互不影响。
考察点:写时复制。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int g = 100;
int main(void) {
pid_t pid = fork();
if (pid == 0) {
g = 200;
printf("child: g=%d, &g=%p\n", g, (void *)&g);
_exit(0);
} else {
wait(NULL);
printf("parent: g=%d, &g=%p\n", g, (void *)&g);
}
return 0;
}预期输出:
child: g=200, &g=0x...
parent: g=100, &g=0x...易错提醒:
- 地址相同是虚拟地址相同。
- 值不同是因为写时复制后物理页不同。
题目 8
题目:写一个程序,父进程打开一个文件,fork 后子进程写入 "child\n",父进程写入 "parent\n",观察文件内容顺序。
考察点:文件描述符共享、文件偏移共享。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(void) {
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
write(fd, "child\n", 6);
_exit(0);
} else {
wait(NULL);
write(fd, "parent\n", 7);
close(fd);
}
return 0;
}预期:out.txt 中内容为 child\nparent\n,顺序取决于调度,但偏移连续。
易错提醒:
- 父子共享文件偏移。
- 不要用
fopen+printf,缓冲会干扰观察。
题目 9
题目:写一个程序,循环创建 5 个子进程,每个子进程打印自己的编号,父进程最后回收所有子进程。
考察点:循环 fork、回收。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 5
int main(void) {
pid_t pids[N];
for (int i = 0; i < N; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
printf("child %d: pid=%d, ppid=%d\n", i, getpid(), getppid());
_exit(0);
}
pids[i] = pid;
}
for (int i = 0; i < N; i++) {
waitpid(pids[i], NULL, 0);
}
printf("parent: all children reaped\n");
return 0;
}易错提醒:
- 子进程必须
_exit,否则继续循环。 - 父进程必须回收所有子进程。
第三部分:fork 进阶与易错题
题目 10
题目:下面代码会输出几个 "hello"?为什么?
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("hello\n");
fork();
fork();
return 0;
}考察点:fork 次数与进程数。
参考答案:
两次 fork,进程总数为 2^2 = 4。但 printf("hello\n") 在第一个 fork 前执行,只有父进程缓冲区里有。若输出到终端,行缓冲遇到 \n 立即刷新,只有一次输出。
如果重定向到文件,全缓冲,缓冲区被两次 fork 复制,可能输出 4 次。
易错提醒:
- 终端行缓冲 vs 文件全缓冲。
- fork 复制缓冲区。
题目 11
题目:下面代码创建了多少个进程?
for (int i = 0; i < 3; i++) {
fork();
}考察点:循环 fork 不 break 的后果。
参考答案:
2^3 = 8 个进程,包括原父进程。
易错提醒:子进程继续循环,产生指数增长。
题目 12
题目:下面代码输出什么?为什么?
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
printf("A");
fork();
printf("B");
return 0;
}考察点:stdio 缓冲区复制。
参考答案:
输出到终端时,"A" 可能先输出,然后父子各输出 "B",结果类似 AB B。
重定向到文件时,"A" 在缓冲区,被 fork 复制,父子各刷新,结果 ABAB。
易错提醒:用 fflush(stdout) 或 write 避免。
题目 13
题目:下面代码有什么问题?
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("child %d\n", i);
}
}考察点:子进程未退出循环。
参考答案:
子进程没有 _exit 或 break,会继续执行循环,创建孙子进程,进程数量爆炸。
正确:
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("child %d\n", i);
_exit(0);
}
}易错提醒:子进程一定要退出循环。
题目 14
题目:写一个程序,父进程创建一个子进程,子进程打印 "child" 后 exit(0),父进程打印 "parent" 后不回收子进程,然后休眠 30 秒。用 ps 查看僵尸进程。
考察点:僵尸进程制造与查看。
参考答案:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
printf("child\n");
exit(0);
} else {
printf("parent\n");
sleep(30);
}
return 0;
}查看:
ps aux | grep Z
# 或
ps -ef | grep defunct易错提醒:
- 僵尸进程状态为
Z或defunct。 - 父进程退出后,僵尸被 init 回收。
第四部分:exec 编程题
题目 15
题目:写一个程序,使用 fork + execlp 执行 ls -l,父进程等待子进程结束。
考察点:fork + exec + wait。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
execlp("ls", "ls", "-l", NULL);
perror("execlp");
_exit(1);
} else {
wait(NULL);
printf("done\n");
}
return 0;
}易错提醒:
execlp参数以NULL结尾。- exec 失败要
perror并_exit。
题目 16
题目:用 execv 实现同样的功能。
考察点:execv 参数数组。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
perror("execv");
_exit(1);
} else {
wait(NULL);
}
return 0;
}易错提醒:argv[0] 通常是程序名,数组以 NULL 结尾。
题目 17
题目:写一个程序,子进程用 execle 执行 /bin/echo,传入自定义环境变量 MYVAR=hello,并让 echo 打印 $MYVAR。
考察点:execle 指定环境。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
char *envp[] = {"MYVAR=hello", "PATH=/bin:/usr/bin", NULL};
execl("/bin/sh", "sh", "-c", "echo $MYVAR", NULL);
// 或用 execlp 执行 sh
perror("execl");
_exit(1);
} else {
wait(NULL);
}
return 0;
}更直接的 execle 示例:
char *envp[] = {"MYVAR=hello", NULL};
execle("/bin/sh", "sh", "-c", "echo $MYVAR", NULL, envp);易错提醒:
- echo 是 shell 内建,直接用 exec 执行
echo $MYVAR不会展开变量,需通过sh -c。 execle最后一个参数是环境表。
题目 18
题目:下面代码有什么问题?
execlp("ls", "ls", "-l");
printf("after exec\n");考察点:exec 参数和环境。
参考答案:
- 问题一:
execlp参数缺少NULL结尾。 - 问题二:exec 成功不返回,
printf不会执行;失败才返回,但没有错误处理。
正确:
execlp("ls", "ls", "-l", NULL);
perror("execlp");
_exit(1);第五部分:wait / waitpid 回收题
题目 19
题目:写一个程序,子进程退出码为 42,父进程用 wait 回收并打印退出码。
考察点:wait 和 WEXITSTATUS。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
_exit(42);
} else {
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("exit code = %d\n", WEXITSTATUS(status));
}
}
return 0;
}易错提醒:不要直接打印 status。
题目 20
题目:子进程被 SIGSEGV 终止,父进程如何获取终止信号编号?
考察点:WIFSIGNALED、WTERMSIG。
参考答案:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
raise(SIGSEGV);
_exit(1);
} else {
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status)) {
printf("killed by signal %d\n", WTERMSIG(status));
}
}
return 0;
}易错提醒:异常终止时退出码无意义,用 WTERMSIG。
题目 21
题目:写一个程序,父进程创建 3 个子进程,分别休眠 1、2、3 秒后退出,父进程用 waitpid(-1, ...) 按退出顺序回收。
考察点:waitpid 任意顺序回收。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(i + 1);
printf("child %d exit\n", i);
_exit(i);
}
}
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, 0)) > 0) {
if (WIFEXITED(status)) {
printf("reaped child %d, exit=%d\n", pid, WEXITSTATUS(status));
}
}
return 0;
}易错提醒:waitpid(-1, ...) 等价于 wait,但更灵活。
题目 22
题目:用 WNOHANG 实现非阻塞回收,父进程每 0.5 秒检查一次子进程是否退出。
考察点:WNOHANG 非阻塞。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
sleep(3);
_exit(7);
}
while (1) {
int status;
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == pid) {
printf("child exit=%d\n", WEXITSTATUS(status));
break;
} else if (ret == 0) {
printf("child still running...\n");
usleep(500000);
} else {
perror("waitpid");
break;
}
}
return 0;
}易错提醒:返回 0 表示暂时没有子进程退出,不是错误。
题目 23
题目:下面代码有什么问题?
int status;
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == 0) {
perror("waitpid");
return 1;
}考察点:WNOHANG 返回值理解。
参考答案:
WNOHANG 时返回 0 表示暂时没有子进程退出,不是错误。不应该 perror。
正确:
if (ret > 0) {
// 回收成功
} else if (ret == 0) {
// 暂无子进程退出
} else {
perror("waitpid");
}第六部分:僵尸进程与孤儿进程
题目 24
题目:写一个程序制造孤儿进程,并观察 getppid 变化。
考察点:孤儿进程。
参考答案:
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
sleep(3);
printf("child ppid=%d\n", getppid());
_exit(0);
} else {
printf("parent exits\n");
_exit(0);
}
return 0;
}预期:子进程 ppid 变为 1 或 systemd 的 PID。
易错提醒:父进程用 _exit 立即退出,子进程才能成为孤儿。
题目 25
题目:写一个程序,父进程不回收子进程,制造僵尸。然后用 SIGCHLD 信号处理函数回收。
考察点:SIGCHLD 回收。
参考答案:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void handler(int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("reaped child %d, exit=%d\n", pid, WEXITSTATUS(status));
}
}
}
int main(void) {
signal(SIGCHLD, handler);
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(i + 1);
_exit(i);
}
}
sleep(5);
return 0;
}易错提醒:
- 处理函数中必须循环
waitpid,因为多个SIGCHLD可能合并。 - 只用异步安全函数,
printf严格来说不安全,示例为演示方便。
题目 26
题目:为什么 SIGCHLD 处理函数中要循环调用 waitpid?
考察点:信号合并。
参考答案:
信号不是队列,是位图。多个子进程几乎同时退出,内核可能只记录一次 SIGCHLD。如果只调用一次 waitpid,只能回收一个子进程,其他子进程变成僵尸。必须循环回收直到没有更多子进程。
第七部分:文件描述符与共享
题目 27
题目:下面代码中,父子进程都写同一个 fd,文件内容顺序是什么?
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
pid_t pid = fork();
if (pid == 0) {
write(fd, "child\n", 6);
_exit(0);
} else {
wait(NULL);
write(fd, "parent\n", 7);
}考察点:文件偏移共享。
参考答案:
out.txt 内容为 child\nparent\n。父子共享文件偏移,子进程写完后偏移后移,父进程继续写。
易错提醒:顺序取决于调度,但偏移连续。
题目 28
题目:如果父进程和子进程分别 open 同一个文件,文件偏移会共享吗?
考察点:打开文件描述。
参考答案:
不会。每次 open 创建新的打开文件描述,偏移独立。只有 fork 继承的 fd 才共享偏移。
题目 29
题目:写一个程序,子进程用 exec 执行新程序,验证 fd 默认保留。
考察点:exec 与 fd。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(void) {
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
pid_t pid = fork();
if (pid == 0) {
// 子进程把 fd 复制到 stdout,然后 exec
dup2(fd, STDOUT_FILENO);
close(fd);
execlp("echo", "echo", "hello from exec", NULL);
perror("execlp");
_exit(1);
} else {
wait(NULL);
close(fd);
printf("parent done\n");
}
return 0;
}预期:out.txt 中有 hello from exec。
易错提醒:exec 默认保留未设置 FD_CLOEXEC 的 fd。
题目 30
题目:如何让 fd 在 exec 时自动关闭?
考察点:FD_CLOEXEC。
参考答案:
int fd = open("file", O_RDONLY);
fcntl(fd, F_SETFD, FD_CLOEXEC);或者 open 时使用 O_CLOEXEC:
int fd = open("file", O_RDONLY | O_CLOEXEC);第八部分:GDB 调试题
题目 31
题目:用 GDB 调试一个 fork 程序,如何跟踪子进程?
考察点:GDB follow-fork-mode。
参考答案:
set follow-fork-mode child
catch fork
run或者同时保留父子:
set detach-on-fork off
info inferiors
inferior 2题目 32
题目:下面 GDB 输出中,inferior 2 是什么?
(gdb) info inferiors
Num Description Executable
* 1 process 12345 ./a.out
2 process 12346 ./a.out考察点:GDB inferior。
参考答案:
inferior 1 是父进程,inferior 2 是子进程。* 表示当前调试的进程。可用 inferior 2 切换。
题目 33
题目:在 GDB 中如何在 fork 后同时调试父子进程?
考察点:detach-on-fork。
参考答案:
set detach-on-fork off
set schedule-multiple on
catch fork
run
info inferiorsdetach-on-fork off 让 GDB 保留父子进程,schedule-multiple on 允许同时运行。
第九部分:综合操作题
题目 34
题目:实现一个简化版 shell,支持执行外部命令。
考察点:fork + exec + wait。
参考答案:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
char line[1024];
while (1) {
printf("mysh> ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0) break;
pid_t pid = fork();
if (pid == 0) {
execlp(line, line, NULL);
perror("execlp");
_exit(1);
} else {
int status;
waitpid(pid, &status, 0);
}
}
return 0;
}扩展:
- 支持参数:用
strtok分割。 - 支持管道:
pipe+dup2。 - 支持重定向:
open+dup2。
易错提醒:
fgets后去掉换行。execlp参数以NULL结尾。- 子进程 exec 失败要
_exit。
题目 35
题目:实现 mysystem 函数,功能类似 system()。
考察点:fork + execl + wait。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int mysystem(const char *cmd) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) {
execl("/bin/sh", "sh", "-c", cmd, NULL);
perror("execl");
_exit(127);
}
int status;
if (waitpid(pid, &status, 0) < 0) {
perror("waitpid");
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1;
}
int main(void) {
int ret = mysystem("ls -l");
printf("command returned %d\n", ret);
return 0;
}易错提醒:
execl参数以NULL结尾。- exec 失败用
_exit(127),这是 shell 的惯例。 waitpid要检查返回值。
题目 36
题目:实现父子进程通过管道通信,父进程写 "hello",子进程读并打印。
考察点:pipe + fork。
参考答案:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fd[2];
if (pipe(fd) < 0) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
close(fd[1]); // 子进程关闭写端
char buf[64];
ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("child read: %s\n", buf);
}
close(fd[0]);
_exit(0);
} else {
close(fd[0]); // 父进程关闭读端
write(fd[1], "hello", 5);
close(fd[1]);
wait(NULL);
}
return 0;
}易错提醒:
- 管道读端写端要正确关闭,否则
read不返回。 - 父进程关闭读端,子进程关闭写端。
题目 37
题目:实现一个程序,父进程创建 3 个子进程,每个子进程计算一个数的平方,通过管道把结果传给父进程,父进程汇总打印。
考察点:多子进程 + 管道 + 回收。
参考答案:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int nums[3] = {2, 3, 4};
for (int i = 0; i < 3; i++) {
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
close(fd[0]);
int result = nums[i] * nums[i];
write(fd[1], &result, sizeof(result));
close(fd[1]);
_exit(0);
} else {
close(fd[1]);
int result;
read(fd[0], &result, sizeof(result));
close(fd[0]);
printf("%d^2 = %d\n", nums[i], result);
}
}
for (int i = 0; i < 3; i++) {
wait(NULL);
}
return 0;
}易错提醒:
- 每次循环都要新建管道。
- 父子关闭不用的端。
- 最后回收所有子进程。
题目 38
题目:实现一个守护进程,每 5 秒向 /tmp/daemon.log 写一行时间戳。
考察点:守护进程创建。
参考答案:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/stat.h>
int main(void) {
pid_t pid = fork();
if (pid > 0) _exit(0);
if (pid < 0) { perror("fork"); return 1; }
setsid();
pid = fork();
if (pid > 0) _exit(0);
if (pid < 0) { perror("fork"); return 1; }
chdir("/");
umask(0);
int fd = open("/tmp/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
while (1) {
time_t t = time(NULL);
char buf[64];
int n = snprintf(buf, sizeof(buf), "time: %ld\n", t);
write(fd, buf, n);
sleep(5);
}
return 0;
}易错提醒:
- 两次 fork 确保不是会话首进程。
setsid创建新会话。chdir("/")避免占用目录。- 重定向标准 fd 到
/dev/null可省略。
题目 39
题目:实现一个程序,父进程创建子进程执行 sleep 10,3 秒后父进程发送 SIGTERM 给子进程,然后回收并打印终止信号。
考察点:kill + waitpid + WTERMSIG。
参考答案:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
execlp("sleep", "sleep", "10", NULL);
perror("execlp");
_exit(1);
} else {
sleep(3);
kill(pid, SIGTERM);
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status)) {
printf("child killed by signal %d\n", WTERMSIG(status));
}
}
return 0;
}易错提醒:
- kill 发送信号,不是杀死。
SIGTERM可被捕获,SIGKILL不可捕获。
题目 40
题目:实现一个程序,父进程创建 5 个子进程,每个子进程休眠随机时间后退出,父进程用 SIGCHLD 处理函数回收,主进程继续做其他事。
考察点:SIGCHLD + 循环 waitpid。
参考答案:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
void handler(int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("reaped child %d, exit=%d\n", pid, WEXITSTATUS(status));
}
}
}
int main(void) {
signal(SIGCHLD, handler);
srand(time(NULL));
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(rand() % 5 + 1);
_exit(i);
}
}
while (1) {
printf("main doing work...\n");
sleep(1);
}
return 0;
}易错提醒:
- 处理函数中循环
waitpid。 printf在信号处理函数中不严格安全,示例为演示。- 主进程可继续做其他事。
第十部分:面试模拟题
题目 41
问:fork 后父子进程谁先执行?
参考答案:
不确定,取决于调度器。不要假设顺序,需要同步就用 wait、管道、信号等。
题目 42
问:fork 和 vfork 有什么区别?
参考答案:
| 对比项 | fork | vfork |
|---|---|---|
| 地址空间 | 写时复制 | 共享 |
| 父进程 | 不阻塞 | 阻塞 |
| 场景 | 通用 | 子进程马上 exec |
| 风险 | 低 | 高 |
现在 vfork 基本不推荐。
题目 43
问:僵尸进程和孤儿进程有什么区别?
参考答案:
| 对比项 | 僵尸 | 孤儿 |
|---|---|---|
| 谁先退出 | 子进程 | 父进程 |
| 状态 | 已退出,PCB 未释放 | 还在运行 |
| 危害 | 占 PID 和 PCB | 一般无害 |
| 解决 | wait/waitpid | 被 init 收养 |
题目 44
问:为什么 exec 成功后原程序后续代码不执行?
参考答案:
exec 替换了当前进程的用户空间,包括代码段、栈、程序计数器。原程序的代码不再存在,所以不会返回。
题目 45
问:父子进程共享文件描述符会有什么效果?
参考答案:
父子 fd 指向同一个打开文件描述,共享文件偏移和状态标志。父子都写同一个 fd,内容连续追加。如果想独立偏移,需要重新 open。
题目 46
问:什么是写时复制?为什么 fork 要用它?
参考答案:
写时复制是父子进程先共享物理页,页表只读。写入时触发异常,内核复制新页。好处是 fork 快、省内存,因为很多子进程马上 exec。
题目 47
问:为什么 SIGCHLD 处理函数要循环 waitpid?
参考答案:
信号不是队列,是位图。多个子进程同时退出可能只产生一个 SIGCHLD。只调用一次 waitpid 只能回收一个,其他变僵尸。必须循环回收。
题目 48
问:waitpid 的 WNOHANG 返回 0 是什么意思?
参考答案:
表示暂时没有子进程退出,不是错误。返回 -1 才是错误。
题目 49
问:为什么 fork 前要 fflush?
参考答案:
fork 复制 stdio 缓冲区。如果缓冲区有数据,父子各有一份,退出时各自刷新,导致重复输出。fflush 提前清空缓冲区。
题目 50
问:为什么子进程推荐用 _exit 而不是 exit?
参考答案:
exit 会刷新 stdio 缓冲区、调用 atexit。子进程继承的缓冲区如果有数据,exit 会刷新,导致重复输出。_exit 直接进入内核,不刷新缓冲区。
附录:答案速查与自测清单
自测清单
做完题目后,检查自己是否能回答:
- [ ] 程序和进程的区别
- [ ] PCB 包含什么
- [ ] fork 返回值
- [ ] 写时复制原理
- [ ] 父子进程共享什么
- [ ] 循环 fork 为什么要 break
- [ ] exec 函数族命名规律
- [ ] exec 成功为什么不返回
- [ ] 僵尸进程和孤儿进程的区别
- [ ] wait 和 waitpid 的区别
- [ ] status 宏怎么用
- [ ] WNOHANG 返回 0 的含义
- [ ] SIGCHLD 处理函数为什么循环
- [ ] 文件描述符共享和文件偏移
- [ ] GDB 如何跟踪子进程
- [ ] 守护进程创建步骤
- [ ] 管道通信基本用法
- [ ] fork 和多线程的问题
