读书笔记 · 约 13,589 字 · 预计推送 2 部分

精读笔记:Linux 进程模型:fork/exec、信号、文件描述符与守护进程 未开始

目录

题记与说明

来源清单

#手册页URL
1fork(2)https://man7.org/linux/man-pages/man2/fork.2.html
2execve(2)https://man7.org/linux/man-pages/man2/execve.2.html
3signal(7)https://man7.org/linux/man-pages/man7/signal.7.html
4signal-safety(7)https://man7.org/linux/man-pages/man7/signal-safety.7.html
5open(2)(O_CLOEXEC 节)https://man7.org/linux/man-pages/man2/open.2.html
6fcntl(2)(概览页)https://man7.org/linux/man-pages/man2/fcntl.2.html
7setsid(2)https://man7.org/linux/man-pages/man2/setsid.2.html
8wait(2)https://man7.org/linux/man-pages/man2/wait.2.html
9daemon(3)https://man7.org/linux/man-pages/man3/daemon.3.html
10daemon(7)(SysV vs systemd)https://man7.org/linux/man-pages/man7/daemon.7.html

七维提取

1. 核心问题

Linux 只提供一个"进程"抽象和一小撮系统调用,却要支撑从一次性命令、交互式 shell、多线程服务器到系统级后台服务的所有形态。这份工作要回答的问题是:

一句话:内核在 fork/exec 边界上画的"默认继承 vs 显式重置"清单,决定了系统编程中大多数经典 bug(fd 泄漏、僵尸进程、EINTR、控制终端劫持)的形态。

2. 主要主张

  1. fork+exec 是组合原语而非单体 spawn。fork 把当前进程一分为二(子进程得到新 PID、单线程副本、空 pending 信号集);exec 不创建新进程——execve(2) 明言 "there is no new process; many attributes of the calling process remain unchanged (in particular, its PID)",只是把映像整个换掉。两步之间保留什么、重置什么,各有一份明确清单。
  2. fork 的简单性来自写时复制(COW)。fork(2):*"Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child."* 复制的只是页表和任务结构,不是内存本身。
  3. 信号是进程级异步事件机制,但有结构性缺陷:标准信号不排队(*"Standard signals do not queue."*,同一信号多次到达只会置位一次 pending);handler 打断任意代码,只能调用 async-signal-safe 函数;SIGKILL/SIGSTOP 无法捕获。因此现代做法是把信号转化为数据事件(self-pipe、signalfd、sigwaitinfo)。
  4. fd 的继承语义是"默认继承、显式关闭",而正确做法是"原子地声明关闭"。execve(2):*"By default, file descriptors remain open across an execve(). File descriptors that are marked close-on-exec are closed."* 先 open 再用 fcntl 设 FD_CLOEXEC 在多线程程序中有 fork/exec 竞态,O_CLOEXEC(Linux 2.6.23)用一次原子操作修掉它——fd 可能 *"unintentionally leaked"* 给被 exec 的程序。
  5. 守护进程配方 = fork/信号/fd 三个主题的串联惯例。daemon(7) 把 SysV 守护进程总结为 15 步(双重 fork、setsid、chdir /、umask 0、重定向 stdio 到 /dev/null、处理信号等),并断言 *"The BSD daemon() function should not be used, as it implements only a subset of these steps."*;systemd 新式守护进程则 *"none of the initialization steps recommended for SysV daemons need to be implemented"*——服务管理器接管了会话、fd 清理、日志与生命周期。
  6. 进程生命周期由 wait 一侧闭环:*"A child that terminates, but has not been waited for becomes a 'zombie'."*(wait(2))不收割就占内核进程表槽位;父进程死掉后由 init/最近 subreaper 收养并收割。

3. 方法

4. 数据与证据

关键论断与原文引述(全部来自本次实际检索的 man7.org 页面):

5. 结论

Linux 进程模型可以压缩为五块拼图,彼此咬合:

  1. 状态复制(fork):COW 让"复制整个地址空间"便宜到只是复制页表;但复制的是"调用线程"的快照,多线程程序 fork 出的是一个只有单线程的世界,锁可能永远锁着。
  2. 状态替换(exec):不换进程只换映像;PID、打开的 fd(默认)、忽略态信号处置穿越 exec,被捕获的处置、内存映射、退出 handler 全部归零。fork 与 exec 之间的窄缝里,子进程只能调 async-signal-safe 函数。
  3. 异步事件(信号):进程级、异步、不可靠(不排队、EINTR、handler 限制)。SIGKILL/SIGSTOP 是内核留给自己和系统管理员的不可协商项。现代程序把信号降格为"往 self-pipe/signalfd 写一个字节",让事件进入普通的事件循环。
  4. 资源句柄(fd):fd 数字是进程私有索引,指向共享的 open file description(offset 共享),再到 inode。fork 复制 fd、exec 保留 fd,两个"默认继承"叠加 = 经典 fd 泄漏;O_CLOEXEC 的历史证明:多线程下"两步操作"必须变成"一步原子操作"。
  5. 生命周期与后台化(wait/daemon/service manager):子进程终止后是僵尸,直到被 wait 或被 init/subreaper 收养;守护进程配方本质是把 1/3/4 三件事按惯例串行执行一次;systemd 把这套惯例上移为服务管理器的职责,守护进程退化为"一个被监督的普通进程"。

6. 局限

7. 对我的启发


附:按子主题的精读细目

A. fork:一次调用,两次返回

B. exec:换映像不换进程

C. 信号:不可靠的异步事件通道

D. 文件描述符:两层标志与一个竞态

E. 守护进程:从 15 步配方到 Type=notify


局限声明

本笔记基于 2026-11-05 实际检索的 10 个 man7.org 手册页。未检索内核源码/实现文档;close_range(2)、pidfd、posix_spawn、signalfd 等现代机制的描述基于已有知识并已在局限节明确标注,未逐条核对一手来源。所有平台差异结论仅对 Linux 成立。

← 资料库今日播报 →