进入系统章节前:先读接口与位操作
完成完整G0后,先学SYS-00的固定宽度、位标志、字节布局与接口返回值,再阅读本页程序。
进入SYS-00系统入口 →CHAPTER 23 / Linux 与并发系统
进程、系统调用与文件描述符
为什么一次 read 不能保证拿到完整数据,子进程又怎样把结果交回父进程?
这一章要弄清楚
- 区分程序、进程与系统调用
- 正确解释描述符、短读和 EOF
- 用 waitpid 检查子进程退出状态
先备知识:指针:地址、空值与有效访问 / 真实资源与RAII:独占拥有 / 多文件编译、链接与构建
POSIX / macOS 与 Linux;19-a、19-b 实际调用 pipe、fork、waitpid。
从一个日志读取器开始
先完成SYS-00 S4:接口类型与实际进展,再读本章真实I/O:其中的ssize_t、失败前后转换顺序和按返回数量推进已有窄合同。SYS模型没有执行write;管道、read的结束条件和进程接口仍按本章逐项学习。
假设工具要读取六个字节 abcdef。直觉上,申请六个字节再调用一次 read 就够了;但接口承诺的是“这次实际读到多少”,不是“满足全部请求”。返回正数时,只能处理那几个字节;返回零表示当前位置已到结束;返回负数才去检查 errno。信号在读取前打断调用可能产生 EINTR,可重试;非阻塞描述符暂时没有数据可能产生 EAGAIN,需要等待可读,而不是不停占满 CPU。普通文件和管道的结束规则也不同:管道中所有写端都关闭、缓冲区也空了,读端才看到 EOF。
文件名不是已经打开的文件
文件描述符(file descriptor,简称 fd)是进程描述符表里的整数索引。这个整数关联内核维护的打开文件状态;状态可能包含偏移、访问模式以及底层对象。dup 得到不同整数,却通常指向同一打开文件描述,因此通过一个描述符读取会推进共享偏移。两次独立 open 同一个文件,则可以拥有各自的偏移。文件名是查找入口,fd 是打开以后使用的句柄;重命名不意味着已经打开的 fd 自动失效。
把关闭责任讲清楚与写 C++ 所有权代码是同一件事。若描述符被复制,只复制了整数并不能安全地产生第二个所有者。真正的复制要明确调用 dup,或把封装设计成只能移动的 RAII 对象。局部教学程序用进程退出回收异常路径,长期服务必须让失败路径也释放 fd,否则最终出现“too many open files”。不要跨平台盲目重试 close,因为不同系统对被打断关闭的处理并不完全一致,描述符还有被复用的风险。
程序怎样成为进程
程序是存放在磁盘上的指令与数据;进程是它正在运行时的地址空间、寄存器、线程和资源集合。系统调用(system call)让用户程序请求受保护的内核操作。普通函数调用不必切换到内核;一个库函数也可能先在用户态处理缓冲,再在必要时调用内核。不要把每次 cout 插入都数成一次 write。
fork 返回两次:子进程拿到零,父进程拿到子 PID,失败返回负值。父子有独立的虚拟地址空间,普通变量的修改不会自动互相可见;继承的 fd 却可能共享打开文件状态。exec 用新的程序映像替换当前进程,不是创建第二个子进程。多线程程序 fork 后,子进程在 exec 前只能执行适用的异步信号安全操作,这也是实际服务常考虑 posix_spawn 的原因。
完成、等待与可观测结果
第二个例子让子进程用退出码 7 报告完成。父进程先循环处理 waitpid 的中断,再通过 WIFEXITED 确认正常退出,最后才用 WEXITSTATUS 解码。原始 status 不是退出码;被信号终止也不能伪装成正常完成。父进程回收已结束子进程的记录,避免僵尸进程累积。
两个例子分别观察字节流边界和进程边界,不依赖 PID 数值或调度顺序生成输出。面试时先说清“哪个资源属于谁、什么时候结束、失败怎样报告”,再写 API 名称,答案会比背诵 fork 定义更完整。
六个字节经过管道
写端提交六个字节,读端尚未消费。
每次最多两个字节,已累计 abcd。
最后一次取走 ef;所有写端关闭且缓存空,下一次返回零。
阅读完整推演文字
- 写入
写端:abcdef;pipe:abcdef;结果:空
写端提交六个字节,读端尚未消费。
- 读取两次
写端:已关闭;pipe:ef;结果:abcd
每次最多两个字节,已累计 abcd。
- 读到结束
pipe:空;read:0 / EOF;结果:abcdef
最后一次取走 ef;所有写端关闭且缓存空,下一次返回零。
跟着例子,走完一遍
用两个字节的窗口读六个字节
管道写入 abcdef,关闭写端;读缓冲区仅有 2 字节。
- write 循环推进已写字节数。
- close 写端后,每次 read 最多取得两个字节。
- 累计 6 字节,下一次 read 返回 0。
#include <unistd.h>
#include <cerrno>
#include <iostream>
#include <stdexcept>
#include <string>
int main() {
int fd[2]; if (::pipe(fd) != 0) throw std::runtime_error("pipe");
const std::string input="abcdef";
std::size_t sent=0;
while (sent<input.size()) {
const ssize_t n=::write(fd[1],input.data()+sent,input.size()-sent);
if(n<0 && errno==EINTR) continue;
if(n<=0) throw std::runtime_error("write");
sent+=static_cast<std::size_t>(n);
}
if(::close(fd[1])!=0) throw std::runtime_error("close writer");
std::string output; int chunks=0; char buffer[2];
for(;;) {
const ssize_t n=::read(fd[0],buffer,sizeof buffer);
if(n<0 && errno==EINTR) continue;
if(n<0) throw std::runtime_error("read");
if(n==0) break;
output.append(buffer,static_cast<std::size_t>(n)); ++chunks;
}
if(::close(fd[0])!=0 || output!=input || chunks!=3)
throw std::runtime_error("pipe invariant");
std::cout<<output<<"|chunks="<<chunks<<'\n';
}abcdef|chunks=3
缓冲区大小限制单次读量;EOF 不是空字符串数据。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 19-a.cpp -o example && ./example预期标准输出:
abcdef|chunks=3
回收子进程并解码退出状态
父进程创建一个子进程,子进程直接以状态 7 退出。
- fork 后只有返回值为零的分支调用 _exit(7)。
- 父进程 waitpid 等待这个具体的 PID。
- 确认正常退出后读取退出码。
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <iostream>
#include <stdexcept>
int main() {
const pid_t child=::fork();
if(child<0) throw std::runtime_error("fork");
if(child==0) ::_exit(7);
int status=0; pid_t result;
do { result=::waitpid(child,&status,0); } while(result<0 && errno==EINTR);
if(result!=child || !WIFEXITED(status) || WEXITSTATUS(status)!=7)
throw std::runtime_error("child status");
std::cout<<"child_exit="<<WEXITSTATUS(status)<<'\n';
}child_exit=7
waitpid 的 status 是编码状态;必须先检查类型再解码。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 19-b.cpp -o example && ./example预期标准输出:
child_exit=7
把一次 read 当作一条消息
read(fd,buf,100) 返回 12,却处理 buf 的全部 100 字节;未初始化尾部混入结果。
修正思路:只处理返回长度;按协议累计;分别处理 EOF、EINTR、暂不可读和永久错误。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
把第一个例子的读缓冲区改为 4 字节,并加入空输入案例;预测输出和结束条件。
给我一点提示
- 读的次数不是消息数。
- 空输入也必须关闭所有写端。
查看答案与推理
六字节输入通常在已完整写入的此例中分为 4 和 2,共两次成功读取;不要将一般并发管道程序绑定这种分块。空输入没有成功读取,第一次 read 为零。每个案例检查累计字节完全相等。
练习 2
父进程 fork 后保留了一个自己不用的 pipe 写端,子进程退出后父进程的读取一直等待,怎样修复?
给我一点提示
- EOF 依赖全部写端引用。
- 画出父子各自持有的两个 fd。
查看答案与推理
父进程立即关闭自己不用的写端,子进程关闭不用的读端,完成发送后关闭写端。父进程读至 EOF 并 waitpid 回收。不能靠 sleep 猜子进程已经退出,因为多余的写端引用仍会阻止 EOF。
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
程序与进程有什么区别?
参考回答 / English answer
程序是静态代码和数据;进程是一次执行的地址空间、寄存器、资源与线程。一个程序可以有多个进程实例。
A program is code and data. A process is a running instance with its own execution state and resources.dup 后通过两个 fd 各读一个字节,会读到同一个字节吗?
参考回答 / English answer
对同一可寻址打开文件描述,两个 fd 共享偏移,通常依次读取相邻字节。两次 open 的独立偏移是另一种情况。
Duplicated descriptors share an open file description, including its offset. Two separate opens can have independent offsets.为什么读取管道的程序在写入者退出后仍不结束?
参考回答 / English answer
检查进程树中是否还有任何写端引用;包括父进程自己的副本。只有缓冲空且所有写端关闭才有 EOF。
EOF requires an empty pipe and no remaining write ends. An accidentally retained descriptor can keep the reader blocked.fork、exec、wait 分别做什么?
参考回答 / English answer
fork 创建子进程;exec 替换当前进程映像;wait 类接口观察并回收子进程状态。exec 成功不返回原程序。
Fork creates a child process. Exec replaces a process image, and wait collects child termination status.read 返回 -1,是否应无限重试?
参考回答 / English answer
不应。EINTR 可以按操作语义重试;EAGAIN 交给可读等待;EBADF 等永久错误必须报告。重试需保留已完成部分。
I classify errno before retrying. Interruptions, temporary unavailability, and permanent failures need different handling.把 waitpid 的 status 直接打印为退出码有什么问题?
参考回答 / English answer
status 编码了正常退出、信号等信息;需先 WIFEXITED 再 WEXITSTATUS,信号路径用 WIFSIGNALED。
The status word encodes several termination modes. I check the mode before extracting an exit code or signal.继续查证
- OSTEP:Process API ↗
5.1 fork、5.2 wait、5.3 exec
- POSIX read ↗
RETURN VALUE、ERRORS、管道结束行为
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。