跳转至

第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 内容):
    C
    1
    2
    3
    4
    5
    6
    struct file {
        char f_flag;   /* 读/写操作权限标志(如 O_RDONLY, O_WRONLY) */
        char f_count;  /* 引用计数:有多少个 fd 指向此 file 结构 */
        long f_offset; /* 当前文件读写位置指针(偏移量) */
        int  f_inode;  /* 指向活动 i 节点表中 inode 的索引 */
    };
    
  • 关键机制
  • 多个进程(或同一进程的多个 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_offsetstruct file 的成员,因此父子进程对同一文件的读写操作会共享读写位置指针
    这意味着:

  • 若父进程写入若干字节后 fork,子进程继续写入时将从父进程停止的位置开始;
  • 反之亦然,若子进程先写,父进程后续写入也会接在其后。

此机制保证了父子进程对同一文件的 I/O 操作是顺序连续的,避免了覆盖或交错混乱(前提是无并发竞争)。这也是标准输出重定向、日志追加等场景能正确工作的基础。

此外,PPT 强调:只有在 fork() 之前打开的文件才会被继承fork() 之后各自打开的文件互不影响。

2. 示例程序

PPT 提供了两个 C 程序 f1.cf2.c,用于演示文件描述符继承与跨进程传递的实际效果。

f1.c:父进程打开文件并 fork

C
void main() {
    int fd;
    fd = open("xxf1f2.txt", O_CREAT | O_WRONLY, 0666);
    if (fork() > 0) {
        // 父进程
        char *str = "Message from process F1\n";
        for (int i = 0; i < 200; i++) {
            write(fd, str, strlen(str));
            sleep(1);
        }
        close(fd);
    } else {
        // 子进程
        char fdstr[16];
        sprintf(fdstr, "%d", fd);  // 将 fd 转为字符串
        execlp("./f2", "f2", fdstr, (char *)0);
        printf("failed to start 'f2': %m\n");
    }
}
  • 父进程首先以只写模式创建并打开文件 xxf1f2.txt,获得 fd。
  • 调用 fork() 后:
  • 父进程:循环写入 200 行消息,每秒一行,结束后关闭 fd。
  • 子进程:不直接写文件,而是通过 execlp 执行另一个程序 f2,并将当前 fd 的数值作为命令行参数传递(如 "3")。

关键点:execlp 属于 exec 系列系统调用。默认情况下,exec 不会关闭已打开的文件描述符(除非设置了 close-on-exec 标志,见第四章内容),因此子进程执行 f2 时,fd 仍然有效。

f2.c:子进程接收 fd 并继续写入

C
int main(int argc, char **argv) {
    int fd = strtol(argv[1], NULL, 0);  // 从命令行参数解析 fd
    static char *str = "Message from process F2\n";
    for (int i = 0; i < 200; i++) {
        if (write(fd, str, strlen(str)) < 0)
            printf("Write error: %m\n");
        sleep(1);
    }
    close(fd);
}
  • f2argv[1] 获取 fd 值(如 "3"),转换为整数后直接用于 write()
  • 由于该 fd 与父进程中打开的 fd 指向同一个 struct file,其 f_offset 是共享的。
  • 因此,f2 的写入会紧接在父进程最后写入的位置之后,最终文件内容呈现为:
    Text Only
    1
    2
    3
    4
    5
    6
    Message from process F1
    Message from process F1
    ...
    Message from process F2
    Message from process F2
    ...
    

说明:父子进程共用同一个 struct file

PPT 通过此示例强调:

  • 尽管 f1f2 是两个不同的可执行程序,但由于 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
    int fd = open("file", O_CREAT | O_WRONLY | O_CLOEXEC, 0666);
    
  • 此方式在打开文件的同时就启用了 close-on-exec,是最简洁、原子性最好的方法。
  • 适用于从一开始就确定该文件不应被后续 exec 继承的场景。

(2)通过 fcntl() 动态设置

当文件已经打开,但后续决定需要为其启用 close-on-exec 时,可使用 fcntl() 系统调用:

C
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);

PPT 明确指出操作步骤如下:

  • 获取当前文件描述符标志
    C
    int flags = fcntl(fd, F_GETFD, 0);
    
  • F_GETFD 返回的是文件描述符标志(file descriptor flags),而非文件状态标志(file status flags)。
  • 其中,最低有效位(bit 0)即为 close-on-exec 标志

  • 设置 close-on-exec 标志

    C
    flags |= FD_CLOEXEC;           // FD_CLOEXEC 通常定义为 1
    fcntl(fd, F_SETFD, flags);     // F_SETFD 用于设置文件描述符标志
    

  • 完整示例(来自 PPT)

    C
    1
    2
    3
    flags = fcntl(fd, F_GETFD, 0);
    flags |= FD_CLOEXEC;
    fcntl(fd, F_SETFD, flags);
    

注意: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 实例;
    • 成功后,fd1fd2 共享同一个系统文件表项(SFT entry),因此具有相同的 f_offsetf_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 代码)

  1. 命令解析
  2. 使用 fgets() 读取用户输入命令行;
  3. 通过 strstr(buf, "<")strstr(buf, ">") 查找重定向符号;
  4. 若存在 <>,则将其替换为 \0,并使用 strtok() 提取文件名(inout);
  5. 同时将命令主体(如 sort)通过 strtok() 分割为 argv[] 数组。

  6. 创建子进程并重定向 I/O

  7. 调用 fork() 创建子进程;
  8. 子进程中执行以下操作:
    • 输入重定向(<
      C
      1
      2
      3
      4
      5
      6
      7
      if (in != NULL) {
          fd0 = open(in, O_RDONLY);
          if (fd0 != -1) {
              dup2(fd0, 0);   // 将 fd0 复制到 stdin (fd=0)
              close(fd0);     // 关闭原始 fd0,仅保留 fd=0 的引用
          }
      }
      
    • 输出重定向(>
      C
      1
      2
      3
      4
      5
      6
      7
      if (out != NULL) {
          fd1 = open(out, O_CREAT | O_WRONLY, 0666);
          if (fd1 != -1) {
              dup2(fd1, 1);   // 将 fd1 复制到 stdout (fd=1)
              close(fd1);     // 关闭原始 fd1
          }
      }
      
  9. 关键点close(fd0)close(fd1) 是安全的,因为 dup2 已使 fd=0/1 指向同一 struct file,引用计数已增加,关闭原 fd 不会影响重定向后的 I/O。

  10. 执行命令

  11. 调用 execvp(argv[0], argv) 执行用户指定的命令;
  12. 此时,新程序的 stdin/stdout 已被重定向到指定文件;
  13. execvp 失败,则打印错误并退出子进程。

  14. 父进程等待

  15. 父进程调用 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.cpread.c 一对程序,演示管道在 fork + exec 场景下的使用。

pwrite.c(写端)

C
int main(void) {
    int pfd[2];
    pipe(pfd);
    if (fork() == 0) {
        // 子进程:执行 pread
        char fdstr[10];
        close(pfd[1]);               // 关闭写端
        sprintf(fdstr, "%d", pfd[0]);
        execlp("./pread", "pread", fdstr, NULL);
        perror("execlp failed");
    } else {
        // 父进程:写入数据
        FILE *f = fdopen(pfd[1], "w");
        close(pfd[0]);               // 关闭读端
        fprintf(f, "Alice 95\n");
        fprintf(f, "Bob 87\n");
        fprintf(f, "Mallory 79\n");
        fclose(f);                   // 自动 close(pfd[1])
    }
}

pread.c(读端)

C
1
2
3
4
5
6
7
8
int main(int argc, char **argv) {
    int score;
    char name[128];
    FILE *f = fdopen(atoi(argv[1]), "r");  // 从命令行获取 fd
    while (fscanf(f, "%s%d", name, &score) != EOF)
        printf("name:%s, score:%d\n", name, score);
    fclose(f);
}
  • 关键点
  • 父进程通过 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):
    C
    1
    2
    3
    4
    dup2(fd[1], 1);   // stdout → 管道写端
    close(fd[0]);     // 关闭读端(不用)
    close(fd[1]);     // 原始写端可关闭(dup2 已引用)
    execvp(argv1[0], argv1);
    
  • 第二个子进程(执行 cmd2):
    C
    1
    2
    3
    4
    dup2(fd[0], 0);   // stdin ← 管道读端
    close(fd[0]);     // 原始读端关闭
    close(fd[1]);     // 写端关闭(不用)
    execvp(argv2[0], argv2);
    
  • 父进程
    • 关闭 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 文件仍保留在磁盘上,直到被显式删除(unlinkrm)。

这一区别使得 FIFO 非常适合用于不同用户、不同会话、甚至不同启动周期的进程间通信,例如守护进程与客户端工具之间的交互。

2. 创建与使用

PPT 给出了命名管道的创建命令和编程接口:

创建方式

  • 命令行创建
    Bash
    1
    2
    3
    mknod pipe0 p      # 使用 mknod,指定类型为 p(pipe)
    # 或更常用的方式:
    mkfifo pipe0       # 专用于创建 FIFO 的命令
    
  • 成功后,当前目录下将出现名为 pipe0 的文件。

  • 查看文件类型

    Bash
    ls -l pipe0
    

  • 输出示例:prw-r--r-- 1 user group 0 ... pipe0
  • 首字符 p 表示这是一个命名管道(FIFO)。

使用方式

命名管道的读写遵循“先开后通”原则,且默认行为与匿名管道一致(阻塞式、字节流、单向):

  • 发送者(写端)

    C
    1
    2
    3
    int fd = open("pipe0", O_WRONLY);  // 阻塞直到有读端打开
    write(fd, "Hello\n", 6);
    close(fd);
    

  • 接收者(读端)

    C
    1
    2
    3
    4
    5
    int fd = open("pipe0", O_RDONLY);  // 阻塞直到有写端打开
    char buf[256];
    read(fd, buf, sizeof(buf));
    printf("Received: %s", buf);
    close(fd);
    

关键行为说明(PPT 隐含要点): - 若只有写端打开而无读端,open(..., O_WRONLY) 会阻塞,直到另一个进程以 O_RDONLY 打开该 FIFO; - 反之亦然,若只有读端打开,open(..., O_RDONLY) 也会阻塞; - 一旦两端都打开,通信行为与匿名管道完全相同:支持 read/write、有容量限制、写满阻塞、读空阻塞、写端全关则读端返回 EOF。

编程创建(补充 PPT 未显式写出但隐含的内容)

  • C 语言中可通过 mkfifo() 创建:
    C
    #include <sys/stat.h>
    mkfifo("pipe0", 0666);  // 权限受 umask 影响
    

典型应用场景

  • 客户端向守护进程发送控制命令(如 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)