第12章:重定向¶
一、概述¶
- 进程与文件描述符
- 活动文件目录(AFD)
- 文件描述符的继承与关闭
- close-on-exec 标志
二、活动文件目录(AFD)¶
活动文件目录(Active File Directory, AFD)是操作系统内核用于管理进程打开文件的核心数据结构体系。它与磁盘上的静态文件系统结构相对应,但更侧重于运行时的动态文件访问状态。PPT 中明确将 AFD 描述为“三级结构”,并与磁盘文件目录的“两级结构”形成对比,体现了从静态存储到动态使用的映射机制。
1. 磁盘文件目录(两级结构)¶
磁盘上的文件系统采用两级目录结构来组织文件:
- 第一级:用户可见的文件名(如
example.txt) - 第二级:文件名通过目录项(directory entry)关联到其对应的i节点(inode)
在 Unix/Linux 文件系统中,文件名本身并不直接包含文件内容或元数据,而是作为指向 inode 的“链接”。多个文件名(硬链接)可指向同一个 inode,从而实现文件共享。
这种两级结构仅描述了文件在磁盘上的静态存在形式,不涉及运行时的打开状态、读写位置等信息。
2. 活动文件目录(三级结构)¶
当进程通过 open() 系统调用打开一个文件时,内核会在内存中构建一套动态的、多层的活动文件目录结构,即三级结构,用于高效管理文件的并发访问和状态维护。该结构由以下三个层次组成:
- 文件描述符表(File Descriptor Table, FDT)¶
- 作用:为每个进程提供独立的文件句柄视图。
- 特性:
- 每进程一张:每个进程拥有自己私有的 FDT。
- 存储位置:位于进程控制块(PCB)的
user结构中。 - 实现方式:以整型数组
u_ofile[]的形式存在。- 数组下标即为文件描述符(fd)(如 0、1、2 分别对应 stdin、stdout、stderr)。
- 数组元素的值是指向系统文件表(SFT) 中某一项的索引或指针。
- 关键点:fd 本身只是一个整数,其语义完全依赖于当前进程的
u_ofile表。
例如,
fd = 3表示u_ofile[3]中存储了一个引用,指向 SFT 中某个struct file实例。
- 系统文件表(System File Table, SFT)¶
- 作用:在内核全局范围内管理所有被打开的“文件实例”(即打开文件的上下文)。
- 特性:
- 整个内核一张:所有进程共享同一张 SFT。
- 数据结构:每一项是一个
struct file结构体,定义如下(根据 PPT 内容): - 关键机制:
- 多个进程(或同一进程的多个 fd)若打开同一文件,可能共享同一个
struct file(如通过fork()继承),此时f_count > 1。 f_offset是共享的:若父子进程共用一个struct file,则它们的读写位置会相互影响(这正是 PPT 中 f1/f2 示例所演示的现象)。
- 活动 i 节点表(Active Inode Table)¶
- 作用:缓存磁盘上 inode 的内容到内存,避免频繁访问磁盘。
- 特性:
- 整个内核一张:全局唯一的 inode 缓冲池。
- 内容:内存中的
inode结构是外存 inode 的副本,包含文件类型、权限、大小、数据块指针等元数据。 - 引用计数:每个内存 inode 包含一个专用的引用计数,记录有多少个
struct file正在引用它。 - 与 SFT 的关系:
struct file中的f_inode字段指向此表中的某一项。- 当所有引用该 inode 的
file结构都被关闭后,若引用计数归零,该 inode 可能被从内存中释放(回写脏数据后)。
设计意义:三级结构实现了“分离关注”——
- FDT 提供进程级抽象(fd);
- SFT 管理打开文件的上下文(偏移、权限);
- 活动 inode 表统一管理文件元数据。
这种分层既支持高效的文件共享(如 fork 后继承),又保证了资源的安全回收。(注:PPT 中包含 AFD 三级结构示意图,清晰展示了“进程 → FDT → SFT → 活动 inode → 磁盘 inode”的链式关系。)
综上,活动文件目录(AFD)的三级结构是 Linux 内核实现文件 I/O、重定向、管道等高级功能的基础架构。理解这一结构,是掌握后续“文件描述符继承”“dup2 重定向”“管道通信”等机制的关键前提。
三、文件描述符的继承与关闭¶
在 Linux 系统中,进程通过 fork() 创建子进程时,会复制父进程的大部分运行状态,其中就包括对已打开文件的访问权限和上下文。PPT 明确指出,这种机制是实现进程间协作(如管道、重定向)的基础,同时也带来共享状态(如文件偏移)的语义特性。
1. 继承机制¶
根据 PPT 内容,fork() 系统调用具有以下关键行为:
-
子进程继承父进程的文件描述符表(FDT)
子进程获得父进程u_ofile[]数组的完整副本,因此其文件描述符(如 fd=3)与父进程指向相同的系统文件表(SFT)项。 -
父子进程共享相同的文件偏移(
f_offset)
由于 FDT 中的每个 fd 最终都指向 SFT 中的同一个struct file实例,而f_offset是struct file的成员,因此父子进程对同一文件的读写操作会共享读写位置指针。
这意味着: - 若父进程写入若干字节后 fork,子进程继续写入时将从父进程停止的位置开始;
- 反之亦然,若子进程先写,父进程后续写入也会接在其后。
此机制保证了父子进程对同一文件的 I/O 操作是顺序连续的,避免了覆盖或交错混乱(前提是无并发竞争)。这也是标准输出重定向、日志追加等场景能正确工作的基础。
此外,PPT 强调:只有在 fork() 之前打开的文件才会被继承;fork() 之后各自打开的文件互不影响。
2. 示例程序¶
PPT 提供了两个 C 程序 f1.c 和 f2.c,用于演示文件描述符继承与跨进程传递的实际效果。
f1.c:父进程打开文件并 fork¶
- 父进程首先以只写模式创建并打开文件
xxf1f2.txt,获得 fd。 - 调用
fork()后: - 父进程:循环写入 200 行消息,每秒一行,结束后关闭 fd。
- 子进程:不直接写文件,而是通过
execlp执行另一个程序f2,并将当前 fd 的数值作为命令行参数传递(如"3")。
关键点:
execlp属于exec系列系统调用。默认情况下,exec 不会关闭已打开的文件描述符(除非设置了 close-on-exec 标志,见第四章内容),因此子进程执行f2时,fd 仍然有效。
f2.c:子进程接收 fd 并继续写入¶
| C | |
|---|---|
f2从argv[1]获取 fd 值(如"3"),转换为整数后直接用于write()。- 由于该 fd 与父进程中打开的 fd 指向同一个
struct file,其f_offset是共享的。 - 因此,
f2的写入会紧接在父进程最后写入的位置之后,最终文件内容呈现为:
说明:父子进程共用同一个 struct file¶
PPT 通过此示例强调:
- 尽管
f1和f2是两个不同的可执行程序,但由于f2继承了f1子进程的文件描述符,并且未设置close-on-exec,因此两者操作的是内核中同一个struct file实例。 - 共享的
f_offset保证了写入的原子性和顺序性(在单线程、无并发前提下)。 - 此机制也说明:文件描述符本质上是进程局部的整数,但其背后代表的是内核全局资源。
注意:若在
f1.c中对 fd 设置了FD_CLOEXEC标志,则execlp会自动关闭该 fd,导致f2无法使用——这正是“close-on-exec”机制的作用(见第四部分内容)。
综上,该示例不仅验证了 fork() 的文件描述符继承行为,还展示了如何通过命令行参数在 exec 后的进程中复用已打开的文件描述符,是理解重定向、管道及进程协作的重要实践基础。
四、close-on-exec 标志¶
在 Linux 系统编程中,exec() 系列函数(如 execl, execv, execlp 等)用于加载并运行一个新的程序映像,替换当前进程的代码段、数据段等,但保留进程 ID 和已打开的文件描述符。这种行为在多数场景下是有益的(如 shell 重定向),但在某些安全或资源管理场景中,可能希望在执行新程序前自动关闭某些敏感或临时文件。为此,Linux 提供了 close-on-exec 标志(Close-on-Exec Flag)。
1. 功能¶
根据 PPT 内容,close-on-exec 标志的核心功能是:
- 若某个文件描述符对应的文件设置了该标志,则当进程调用
exec()系列函数时,内核会自动关闭该文件描述符。 - 未设置该标志的文件描述符则保持打开状态,并被新程序继承。
这一机制主要用于: - 安全性:防止敏感文件(如配置文件、密钥)被意外传递给不可信的子程序; - 资源清理:避免临时文件句柄泄漏到新进程中; - 控制继承范围:精确指定哪些文件应被子程序使用,哪些不应。
PPT 特别强调:默认情况下,文件描述符在 exec() 后是保持打开的,因此若需自动关闭,必须显式设置 close-on-exec 标志。
2. 设置方式¶
PPT 提供了两种设置 close-on-exec 标志的方法:在 open() 时直接指定,或通过 fcntl() 系统调用动态修改。
(1)在 open() 调用中设置¶
- 使用
O_CLOEXEC标志作为open()的 flags 参数之一:C - 此方式在打开文件的同时就启用了 close-on-exec,是最简洁、原子性最好的方法。
- 适用于从一开始就确定该文件不应被后续
exec继承的场景。
(2)通过 fcntl() 动态设置¶
当文件已经打开,但后续决定需要为其启用 close-on-exec 时,可使用 fcntl() 系统调用:
PPT 明确指出操作步骤如下:
- 获取当前文件描述符标志:
C F_GETFD返回的是文件描述符标志(file descriptor flags),而非文件状态标志(file status flags)。-
其中,最低有效位(bit 0)即为 close-on-exec 标志。
-
设置 close-on-exec 标志:
-
完整示例(来自 PPT):
注意:
FD_CLOEXEC是一个常量(通常值为 1),专用于操作 close-on-exec 位。不要与O_CLOEXEC混淆——前者用于fcntl(),后者用于open()。
补充说明(基于 PPT 上下文)¶
- 在第三章的
f1.c / f2.c示例中,若父进程对fd设置了FD_CLOEXEC,则子进程执行execlp("./f2", ...)时,该fd会被内核自动关闭,导致f2接收到的 fd 无效(write失败)。 - 因此,是否设置 close-on-exec 直接决定了 exec 后文件描述符的可用性,是控制进程间文件继承的关键开关。
综上,close-on-exec 标志是 Linux 进程控制和安全编程中的重要机制。PPT 通过系统调用接口和代码示例,清晰地展示了其作用原理与使用方法,为理解后续的重定向、管道及 shell 实现奠定了基础。
五、重定向¶
在 Linux 系统中,重定向(Redirection) 是指改变进程的标准输入(stdin, fd=0)、标准输出(stdout, fd=1)或标准错误(stderr, fd=2)的默认目标,使其指向其他文件或设备。PPT 指出,重定向的核心机制依赖于文件描述符的复制,并通过一个简易 shell 程序 xsh1 展示了其实现原理。
1. 文件描述符的复制¶
PPT 明确介绍了实现重定向的关键系统调用:
- 系统调用:
dup2(int fd1, int fd2) - 功能:将文件描述符
fd1复制到fd2。 - 行为细节:
- 若
fd2已经打开,则内核先自动关闭fd2所引用的文件,再将其指向fd1所引用的同一struct file实例; - 成功后,
fd1和fd2共享同一个系统文件表项(SFT entry),因此具有相同的f_offset、f_flag等状态; - 返回值为
fd2(成功)或-1(失败)。
- 若
- 典型用途:
dup2(fd, 0):将fd重定向为标准输入(stdin);dup2(fd, 1):将fd重定向为标准输出(stdout);dup2(fd, 2):重定向标准错误(stderr)。
例如,若某进程执行
dup2(3, 1),则此后所有对 stdout(如printf,write(1, ...))的写入都会被导向fd=3所对应的文件。
该机制之所以有效,正是因为 dup2 使得目标 fd(如 0、1、2)与源 fd 指向 SFT 中的同一个 struct file,从而实现了“逻辑替换”而无需修改应用程序代码。
2. xsh1:输入输出重定向实现¶
PPT 提供了一个名为 xsh1 的简易 shell 程序,用于演示如何在用户态实现 <(输入重定向)和 >(输出重定向)功能。
支持语法¶
- 基本命令格式:
command < input_file > output_file - 示例:
sort < data.txt > sorted.txt
实现逻辑(结合 PPT 代码)¶
- 命令解析:
- 使用
fgets()读取用户输入命令行; - 通过
strstr(buf, "<")和strstr(buf, ">")查找重定向符号; - 若存在
<或>,则将其替换为\0,并使用strtok()提取文件名(in和out); -
同时将命令主体(如
sort)通过strtok()分割为argv[]数组。 -
创建子进程并重定向 I/O:
- 调用
fork()创建子进程; - 在子进程中执行以下操作:
- 输入重定向(
<): - 输出重定向(
>):
- 输入重定向(
-
关键点:
close(fd0)和close(fd1)是安全的,因为dup2已使 fd=0/1 指向同一struct file,引用计数已增加,关闭原 fd 不会影响重定向后的 I/O。 -
执行命令:
- 调用
execvp(argv[0], argv)执行用户指定的命令; - 此时,新程序的 stdin/stdout 已被重定向到指定文件;
-
若
execvp失败,则打印错误并退出子进程。 -
父进程等待:
- 父进程调用
wait(&sv)等待子进程结束,确保 shell 不会提前返回提示符。
设计意义¶
xsh1完整展示了 shell 重定向的底层实现逻辑:通过open+dup2+exec三步实现 I/O 重定向;- 利用了
fork()的继承特性:子进程继承父进程环境,但可独立修改其 fd 表而不影响父进程; - 遵循 Unix 哲学:重定向对应用程序透明——被
exec的程序无需知道其 stdin/stdout 被重定向,只需正常读写 fd 0/1 即可。
综上,PPT 通过 dup2 的机制说明和 xsh1 的完整代码,清晰地揭示了 Linux 中重定向的本质:通过文件描述符复制,将标准流绑定到任意打开的文件上,从而实现灵活的 I/O 控制。这一机制是 shell、管道、后台任务等高级功能的基础。
六、管道(Pipe)¶
管道(Pipe)是 Linux 中一种经典的进程间通信(IPC)机制,主要用于具有亲缘关系的进程(如父子进程)之间传递数据。PPT 从操作接口、通信模型、示例程序到 shell 实现,系统地阐述了管道的工作原理与使用注意事项。
1. 管道操作¶
PPT 首先介绍了管道的基本系统调用和 I/O 行为:
- 创建:
int pipe(int pfd[2]); - 成功时,
pfd[0]为读端(用于read),pfd[1]为写端(用于write); -
管道在内核中表现为一个环形缓冲区,默认容量有限(如 8192 字节,具体取决于系统实现)。
-
写操作:
write(pfd[1], buf, n) - 若管道已满,
write调用将阻塞,直到读端进程通过read取走部分数据,腾出空间; -
写入的数据以字节流形式存入缓冲区,无消息边界。
-
读操作:
read(pfd[0], buf, n) - 若管道为空且写端仍打开,则
read阻塞等待数据; - 若管道为空且所有写端均已关闭,则
read返回 0,表示 EOF(文件结束); -
若管道中有
m字节数据:- 当
n ≥ m时,读取全部m字节; - 当
n < m时,仅读取n字节; - 返回值为实际读取的字节数。
- 当
-
关闭行为
- 关闭读端(
close(pfd[0])):- 若仍有进程尝试向写端写入,内核会向该进程发送
SIGPIPE信号; - 默认处理动作是终止进程;
- 同时
write()返回-1,并设置errno = EPIPE。
- 若仍有进程尝试向写端写入,内核会向该进程发送
- 关闭写端(
close(pfd[1])):- 读端后续的
read()将返回 0,通知接收方“数据已发完”。
- 读端后续的
PPT 特别强调:管道是单向通信、字节流、无记录边界的通道,这决定了其使用方式和限制。
2. 使用管道进行进程间通信¶
PPT 描述了典型的父子进程通信模式:
- 父进程调用
pipe()创建管道; - 调用
fork()后: - 父进程关闭读端:
close(pfd[0]),仅保留写端用于发送数据; - 子进程关闭写端:
close(pfd[1]),仅保留读端用于接收数据; -
此“各关一端”的做法避免了资源浪费,并明确了数据流向(父 → 子)。
-
双向通信需求:
- 单个管道仅支持单向传输;
- 若要实现父子进程双向通信(如请求-响应模型),必须创建两个管道:
- 管道 A:父 → 子;
- 管道 B:子 → 父;
- PPT 警告:若试图用一个管道实现双向通信,进程可能读到自己刚写入的数据,造成逻辑错误。
3. 示例程序¶
PPT 提供了 pwrite.c 和 pread.c 一对程序,演示管道在 fork + exec 场景下的使用。
pwrite.c(写端)¶
pread.c(读端)¶
| C | |
|---|---|
- 关键点:
- 父进程通过
fdopen(pfd[1], "w")将写端包装为FILE*,便于使用fprintf; - 子进程通过命令行参数接收读端 fd(数值),再用
fdopen包装为FILE*读取; - 由于
exec默认不关闭 fd,且未设置close-on-exec,因此pfd[0]在pread中仍然有效; - 数据通过管道无缝传递,体现了“匿名管道 + exec 传参”的协作模式。
4. 注意事项¶
PPT 特别指出使用管道时需警惕以下问题:
- 死锁风险:
- 若父子进程均向对方写入大量数据(如通过两个管道),而未及时读取,可能导致:
- 父进程因输出管道满而阻塞在
write; - 子进程同样因响应管道满而阻塞;
- 双方互相等待,形成死锁。
- 父进程因输出管道满而阻塞在
-
解决方案:采用非阻塞 I/O、多线程/异步读写,或严格控制数据流量。
-
无记录边界:
- 管道是纯字节流,不保留 write 的边界信息;
- 例如:三次
write(fd, "A", 1)可能被一次read(fd, buf, 10)全部读出为"AAA"; -
应用层需自行设计协议(如行分隔、长度前缀)来解析消息。
-
流量控制:
- 管道容量有限,发送方不能无限制写入;
- 接收方必须及时消费数据,否则发送方将阻塞,影响整体流程。
5. xsh2:支持管道的 shell¶
PPT 最后通过 xsh2 程序展示了 shell 中 cmd1 | cmd2 的实现机制:
- 支持语法:
command1 | command2 - 实现逻辑:
- 使用
strstr(buf, "|")分割命令行,得到argv1(左命令)和argv2(右命令); - 调用
pipe(fd)创建管道; - 第一个子进程(执行
cmd1): - 第二个子进程(执行
cmd2): - 父进程:
- 关闭
fd[0]和fd[1](自身不参与通信); - 调用两次
wait()等待两个子进程结束。
- 关闭
此实现完美复现了标准 shell 的管道行为:
cmd1的 stdout 被重定向到管道写端,cmd2的 stdin 被重定向到管道读端,两者通过内核管道连接,无需临时文件。
综上,PPT 通过理论、示例与 shell 实现三层递进,全面揭示了管道作为轻量级 IPC 机制的核心原理、使用范式及潜在陷阱,为理解更复杂的进程协作模型奠定了坚实基础。
七、命名管道(FIFO)¶
命名管道(Named Pipe),也称为 FIFO(First In, First Out),是 Linux 提供的一种持久化、基于文件系统路径的管道通信机制。与匿名管道不同,命名管道通过一个特殊的文件节点存在于文件系统中,使得任意两个无关进程(即使没有父子关系)只要知道该路径,即可通过它进行通信。PPT 在本节中重点对比了命名管道与匿名管道的差异,并介绍了其创建与使用方法。
1. 与匿名管道的区别¶
PPT 明确指出两者的核心区别在于通信范围和生命周期:
- 匿名管道(Anonymous Pipe)
- 由
pipe()系统调用创建; - 仅限具有共同祖先的进程之间通信(如父子进程、兄弟进程);
- 无文件系统入口:管道缓冲区完全存在于内核内存中,不对应任何路径名;
-
生命周期短暂:当所有引用该管道的进程关闭其两端后,内核自动回收资源。
-
命名管道(FIFO)
- 通过
mkfifo()或mknod()在文件系统中创建一个特殊文件; - 允许任意无关进程通信:只要进程有权限访问该路径,即可打开并读写;
- 具有文件系统路径:表现为一个普通文件名(如
/tmp/myfifo),但类型为p; - 持久存在:即使当前无进程打开它,FIFO 文件仍保留在磁盘上,直到被显式删除(
unlink或rm)。
这一区别使得 FIFO 非常适合用于不同用户、不同会话、甚至不同启动周期的进程间通信,例如守护进程与客户端工具之间的交互。
2. 创建与使用¶
PPT 给出了命名管道的创建命令和编程接口:
创建方式¶
- 命令行创建:
-
成功后,当前目录下将出现名为
pipe0的文件。 -
查看文件类型:
Bash - 输出示例:
prw-r--r-- 1 user group 0 ... pipe0 - 首字符
p表示这是一个命名管道(FIFO)。
使用方式¶
命名管道的读写遵循“先开后通”原则,且默认行为与匿名管道一致(阻塞式、字节流、单向):
-
发送者(写端):
-
接收者(读端):
关键行为说明(PPT 隐含要点): - 若只有写端打开而无读端,
open(..., O_WRONLY)会阻塞,直到另一个进程以O_RDONLY打开该 FIFO; - 反之亦然,若只有读端打开,open(..., O_RDONLY)也会阻塞; - 一旦两端都打开,通信行为与匿名管道完全相同:支持read/write、有容量限制、写满阻塞、读空阻塞、写端全关则读端返回 EOF。
编程创建(补充 PPT 未显式写出但隐含的内容)¶
- C 语言中可通过
mkfifo()创建:
典型应用场景¶
- 客户端向守护进程发送控制命令(如
echo "reload" > /var/run/mydaemon.fifo); - 脚本间协调(一个脚本写入 FIFO,另一个脚本从中读取);
- 调试或日志收集(多个生产者写入同一 FIFO,单一消费者读取)。
注意:虽然 FIFO 支持多写者/单读者或多读者/单写者,但多写者并发写入时,若每次写入 ≤ PIPE_BUF(通常 4KB),则写操作是原子的;否则可能交错。PPT 虽未深入此细节,但这是实际使用中需注意的问题。
综上,命名管道(FIFO)扩展了管道机制的适用范围,使其从“进程家族内部通信”走向“系统级通用 IPC”。PPT 通过简洁的对比和操作示例,清晰地展示了其核心价值与基本用法,为理解更广泛的 IPC 机制(如 Unix 域套接字)提供了过渡基础。
八、本章小结¶
fork/exec与文件描述符的关系- 三级活动文件目录(AFD)结构及其设计原因
- 文件描述符的继承与关闭机制
- close-on-exec 标志的作用与设置
- 文件描述符复制(
dup2)与 I/O 重定向(xsh1) - 管道原理、读写模型、状态变化与死锁问题
- 命名管道(FIFO)实现跨进程通信
- 管道在 shell 中的应用(xsh2)