进入系统章节前:先读接口与位操作

完成完整G0后,先学SYS-00的固定宽度、位标志、字节布局与接口返回值,再阅读本页程序。

进入SYS-00系统入口 →

CHAPTER 23 / Linux 与并发系统

进程、系统调用与文件描述符

为什么一次 read 不能保证拿到完整数据,子进程又怎样把结果交回父进程?

阅读与推演约 60 分钟练习时间另计

这一章要弄清楚

  • 区分程序、进程与系统调用
  • 正确解释描述符、短读和 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 定义更完整。

观察 · 推演

六个字节经过管道

写端abcdefpipeabcdef结果01 / 03 · PIPELINE写端abcdefpipeabcdef结果01 / 03 · PIPELINE
写入

写端提交六个字节,读端尚未消费。

1 / 3
阅读完整推演文字
  1. 写入

    写端:abcdef;pipe:abcdef;结果:空

    写端提交六个字节,读端尚未消费。

  2. 读取两次

    写端:已关闭;pipe:ef;结果:abcd

    每次最多两个字节,已累计 abcd。

  3. 读到结束

    pipe:空;read:0 / EOF;结果:abcdef

    最后一次取走 ef;所有写端关闭且缓存空,下一次返回零。

跟着例子,走完一遍

例题 01POSIX · 需对应系统

用两个字节的窗口读六个字节

管道写入 abcdef,关闭写端;读缓冲区仅有 2 字节。

  1. write 循环推进已写字节数。
  2. close 写端后,每次 read 最多取得两个字节。
  3. 累计 6 字节,下一次 read 返回 0。
19-a.cpp
下载
#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';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

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
例题 02POSIX · 需对应系统

回收子进程并解码退出状态

父进程创建一个子进程,子进程直接以状态 7 退出。

  1. fork 后只有返回值为零的分支调用 _exit(7)。
  2. 父进程 waitpid 等待这个具体的 PID。
  3. 确认正常退出后读取退出码。
19-b.cpp
下载
#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 字节,并加入空输入案例;预测输出和结束条件。

给我一点提示
  1. 读的次数不是消息数。
  2. 空输入也必须关闭所有写端。
查看答案与推理

六字节输入通常在已完整写入的此例中分为 4 和 2,共两次成功读取;不要将一般并发管道程序绑定这种分块。空输入没有成功读取,第一次 read 为零。每个案例检查累计字节完全相等。

练习 2

父进程 fork 后保留了一个自己不用的 pipe 写端,子进程退出后父进程的读取一直等待,怎样修复?

给我一点提示
  1. EOF 依赖全部写端引用。
  2. 画出父子各自持有的两个 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.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。