文件描述符、I/O、mmap 与资源所有权 / File Descriptors, I/O, mmap, and Resource Ownership
1. 为什么 fd 是系统编程入口
在 Unix-like 系统中,文件、目录、pipe、socket、终端、eventfd、timerfd 都可以通过 fd 进入统一 I/O 模型。fd 是进程 fd 表里的整数,不是资源本身。
process fd table
fd 0 -> terminal/stdin
fd 1 -> terminal/stdout
fd 2 -> terminal/stderr
fd 3 -> open file description -> inode/file/socket/pipe2. fd、open file description、inode
open() 返回 fd。fd 指向 open file description,后者包含文件 offset、status flags 等状态。多个 fd 可以通过 dup 或 fork 指向同一个 open file description,因此共享 offset。
fd 3 ----+
+--> open file description(offset=128, flags=O_APPEND)
fd 4 ----+
|
v
file object / inode这解释了为什么 dup 后两个 fd 读同一个文件会互相影响 offset。需要独立 offset 时重新 open,或使用 pread/pwrite。
3. open flags
常见 flags 要区分:
- access mode:O_RDONLY、O_WRONLY、O_RDWR;
- creation:O_CREAT、O_EXCL、O_TRUNC;
- behavior:O_APPEND、O_NONBLOCK、O_CLOEXEC、O_SYNC。
O_CLOEXEC 是防 fd 泄漏到 exec 子进程的重要选项;O_NONBLOCK 改变 read/write 在无数据或缓冲区满时的行为。
4. read/write 的真实语义
read 返回正数表示读到字节,0 表示 EOF,-1 表示错误。对普通文件、pipe、socket 的阻塞和 EOF 语义不同。write 也可能部分写,不能假设一次写完。
read(fd, buf, n)
>0 : bytes read
0 : EOF or peer closed write side
-1 : errno explains failure
write(fd, buf, n)
>0 and < n : partial write, continue
-1 : EPIPE/EAGAIN/EINTR/...5. EINTR 和 EAGAIN
EINTR 表示系统调用被信号打断;EAGAIN/EWOULDBLOCK 表示非阻塞资源当前条件不满足。二者处理方式不同:EINTR 常可重试,EAGAIN 要等待就绪或稍后再试。
6. pread/pwrite
pread/pwrite 带显式 offset,不改变 open file description 的共享 offset,适合多线程或随机访问文件。
7. dup 和重定向
shell 的 cmd > out.txt 本质是打开文件,然后把 stdout fd 复制到该 open file description。C++ 启动子进程时也常用 dup2 做 stdin/stdout/stderr 重定向。
int out = open("out.txt", O_CREAT|O_WRONLY|O_TRUNC|O_CLOEXEC, 0644);
dup2(out, STDOUT_FILENO);
close(out);
execlp("ls", "ls", nullptr);8. pipe
pipe 是内核缓冲区,一端读一端写。读端全部关闭后写会 EPIPE/SIGPIPE;写端全部关闭后读返回 0。父子进程通信时必须关闭不用的端,否则 EOF 永远不来。
9. mmap 的心智模型
mmap 把文件或匿名内存映射进进程虚拟地址空间。它返回的指针不是普通 new 出来的内存,生命周期由映射区、文件、权限和页错误共同决定。
file bytes
-> mmap
-> virtual address range
-> page fault loads pages on demand
-> munmap releases mappingmmap 适合随机访问大文件、共享内存和减少显式 read/write 拷贝,但它带来 SIGBUS、页错误、刷新时机和文件截断风险。
10. msync、munmap 和 SIGBUS
MAP_SHARED 修改是否落盘受内核策略影响,可用 msync 请求同步。文件被截断后访问原映射的越界页可能 SIGBUS。生产代码要把映射大小、文件大小和异常信号风险写进边界。
11. RAII 封装
class unique_fd {
public:
explicit unique_fd(int fd = -1) noexcept : fd_(fd) {}
~unique_fd() { if (fd_ >= 0) ::close(fd_); }
unique_fd(const unique_fd&) = delete;
unique_fd& operator=(const unique_fd&) = delete;
unique_fd(unique_fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
int get() const noexcept { return fd_; }
private:
int fd_;
};析构函数里 close 失败通常无法恢复,但显式 flush/fsync/msync 的错误必须在析构前处理。不要把关键写入错误藏进析构。
12. 常见错误
- 把 fd 整数复制给多个对象,导致 double close。
- 忽略 partial read/write。
- 忘记 O_CLOEXEC,子进程继承 fd。
- pipe 父子两边没关闭不用端,导致读不到 EOF。
- mmap 后文件被截断仍继续访问。
- 析构时才发现 fsync 失败,错误已无法报告。
13. 调试命令
lsof -p <pid>
ls -l /proc/<pid>/fd
strace -e openat,read,write,close,mmap,munmap ./program
pmap -x <pid>
cat /proc/<pid>/maps14. 练习
- 写 cp 的简化版,正确处理 partial read/write。
- 用 dup2 把子进程 stdout 重定向到文件。
- 写 pipe 父子通信,故意不关闭写端观察 EOF 问题。
- mmap 一个文件并打印前 1KB,再截断文件观察风险。
- 给 unique_fd 加 move assignment。
15. 总结
fd 篇的关键是三层关系:fd 是进程局部句柄,open file description 保存 offset/flags,底层对象可能是文件、pipe、socket 或设备。C++ RAII 要管理 fd 生命周期,但不能掩盖 I/O 错误和映射风险。
深化切片 1:最小实验
围绕 fd 和 open file description 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 2:错误注入
围绕 open file description 和 read 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 3:外部观察
围绕 read 和 write 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 4:RAII 封装
围绕 write 和 dup2 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 5:并发边界
围绕 dup2 和 pipe 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 6:生产复盘
围绕 pipe 和 mmap 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 7:最小实验
围绕 mmap 和 RAII 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 8:错误注入
围绕 RAII 和 fd 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 9:外部观察
围绕 fd 和 open file description 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 10:RAII 封装
围绕 open file description 和 read 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 11:并发边界
围绕 read 和 write 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 12:生产复盘
围绕 write 和 dup2 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 13:最小实验
围绕 dup2 和 pipe 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 14:错误注入
围绕 pipe 和 mmap 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 15:外部观察
围绕 mmap 和 RAII 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 16:RAII 封装
围绕 RAII 和 fd 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 17:并发边界
围绕 fd 和 open file description 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 18:生产复盘
围绕 open file description 和 read 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 19:最小实验
围绕 read 和 write 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 20:错误注入
围绕 write 和 dup2 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 21:外部观察
围绕 dup2 和 pipe 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 22:RAII 封装
围绕 pipe 和 mmap 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 23:并发边界
围绕 mmap 和 RAII 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 24:生产复盘
围绕 RAII 和 fd 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 25:最小实验
围绕 fd 和 open file description 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 26:错误注入
围绕 open file description 和 read 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 27:外部观察
围绕 read 和 write 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 28:RAII 封装
围绕 write 和 dup2 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 29:并发边界
围绕 dup2 和 pipe 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 30:生产复盘
围绕 pipe 和 mmap 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 31:最小实验
围绕 mmap 和 RAII 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 32:错误注入
围绕 RAII 和 fd 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 33:外部观察
围绕 fd 和 open file description 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。
深化切片 34:RAII 封装
围绕 open file description 和 read 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,文件描述符、I/O、mmap 与资源所有权 就越不容易学成 API 背诵。