本文档配合前面的教程使用。

  • 题目按难度递进,覆盖 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 inferiors

detach-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 有什么区别?

参考答案:

对比项forkvfork
地址空间写时复制共享
父进程不阻塞阻塞
场景通用子进程马上 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 和多线程的问题