ZeroOne AI
← 返回文章列表

Linux系统篇24——通信(二):匿名管道 vs 命名管道,进程之间到底怎么“传话”?

👁 3
分类:工业互联网

进程之间默认谁也看不见谁,操作系统给它们接了一根“水管”——管道。本文从“对讲机”和“公共信箱”讲起,把匿名管道与命名管道的底层原理、代码写法、常见坑和面试考点一次讲透。

项目背景:进程之间,默认谁也看不见谁

每个进程都有自己独立的“内存房间”——地址空间。你在进程 A 里写一个变量,进程 B 完全看不到,这是操作系统刻意做的安全隔离。如果谁都能乱闯别人的内存,系统早就乱了。

但现实中的进程必须协作:

怎么办?操作系统在它们之间接了一根“水管”——管道(Pipe)。

技术方案:管道 = 内核缓冲区 + 两个 fd

一句话核心:管道是内核里的一段缓冲区,一头写、一头读,数据先进先出(FIFO),像排队一样有序。

两种管道的本质区别:

创建命名管道

# 方式一:mkfifo(推荐)
mkfifo myfifo
# 指定权限创建
mkfifo -m 0666 myfifo

# 方式二:mknod,p 表示管道类型
mknod myfifo p

# 查看:首字符为 p 代表管道文件
ls -l myfifo
# prw-r--r-- 1 user user 0 Aug 19 10:00 myfifo

注意:命名管道是特殊文件,数据存在内核内存,磁盘大小始终为 0。

生活化类比

一句话记忆:匿名管道认“血缘”,命名管道认“名字”。

系统架构:管道底层原理

管道不是真的水管,真身是内核里的一个缓冲区(Linux 上是环形队列),外加两个文件描述符:

写端 fd[1]                    读端 fd[0]
进程A ────► [内核环形缓冲区] ────► 进程B

关键点:

实施过程

匿名管道:pipe() + fork() 经典组合

pipe() 一次性返回两个 fd:fd[0] 读端、fd[1] 写端。fork() 后父子共享这对 fd。为了让数据单向流动,要“各关一头”:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pipe(fd);               // ① 建水管
    pid_t pid = fork();     // ② 分裂:父子共享同一根水管

    if (pid == 0) {         // 子进程:负责写
        close(fd[0]);       // 关掉读端
        const char *msg = "hello from child";
        write(fd[1], msg, strlen(msg));
        close(fd[1]);       // 写完关写端
        return 0;
    }

    // 父进程:负责读
    close(fd[1]);           // 关掉写端
    char buf[64] = {0};
    int n = read(fd[0], buf, sizeof(buf) - 1);
    printf("父进程收到: %s (%d 字节)\n", buf, n);
    close(fd[0]);

    wait(NULL);             // 回收子进程,避免僵尸
    return 0;
}

编译运行:

gcc pipe_demo.c -o pipe_demo && ./pipe_demo
# 输出: 父进程收到: hello from child (17 字节)

为什么非要“关一头”?

匿名管道两大限制

命名管道:mkfifo() 靠名字接头

创建后在文件系统真实存在一个 p 类型文件,两个互不相识的进程只要都能访问该路径就能通信,不需要血缘关系。

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

int main() {
    const char *path = "/tmp/myfifo";
    mkfifo(path, 0666);     // 建一根“有名字”的水管

    pid_t pid = fork();
    if (pid == 0) {         // 子进程:写
        int fd = open(path, O_WRONLY);
        const char *msg = "hello from writer";
        write(fd, msg, strlen(msg));
        close(fd);
        return 0;
    }

    // 父进程:读
    int fd = open(path, O_RDONLY);
    char buf[64] = {0};
    int n = read(fd, buf, sizeof(buf) - 1);
    printf("父进程收到: %s (%d 字节)\n", buf, n);
    close(fd);

    wait(NULL);
    unlink(path);           // 用完拆掉水管
    return 0;
}

关键坑:open() 的阻塞

命名管道 open() 本身就会阻塞:

所以独立进程通信时要约定好“谁先开、开哪头”。

真正的用途:两个独立进程通信

writer.c(终端 1):

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *path = "/tmp/myfifo";
    mkfifo(path, 0666);
    int fd = open(path, O_WRONLY);  // 等读者出现才返回
    char buf[128];
    while (fgets(buf, sizeof(buf), stdin)) {
        write(fd, buf, strlen(buf));
    }
    close(fd);
    return 0;
}

reader.c(终端 2):

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *path = "/tmp/myfifo";
    int fd = open(path, O_RDONLY);  // 等写者出现才返回
    char buf[128];
    int n;
    while ((n = read(fd, buf, sizeof(buf))) > 0) {
        write(STDOUT_FILENO, buf, n);  // 打印到屏幕
    }
    close(fd);
    return 0;
}
# 终端 1
gcc writer.c -o writer && ./writer
# 终端 2
gcc reader.c -o reader && ./reader
# 终端 1 敲一行字回车,终端 2 立刻显示

注意:必须一端先 open 阻塞,再启动另一端,这就是“管道两侧不同步”的经典协作模型。

管道通信的四种情况

情况现象
读正常,写满write 阻塞
写正常,读空read 阻塞
写端全部关闭,读正常read 读完返回 0
读端全部关闭,写正常收到 SIGPIPE,写进程终止

阻塞、非阻塞与 SIGPIPE

管道读写默认阻塞模式:

场景写端行为读端行为
缓冲区有空位正常写入正常读出
缓冲区写满阻塞等待,直到有空间
没有数据可读阻塞等待,直到有数据
对端全部关闭收到 SIGPIPE,进程被杀read() 返回 0(EOF)

SIGPIPE 是必考题也是必踩的坑:没人读你还在写,系统直接发信号崩掉进程。经典例子:

$ yes | head -1
y

yes 疯狂输出,head 读完一行关闭读端,yes 立刻被 SIGPIPE 杀死。想避免被杀,忽略信号:

#include <signal.h>
signal(SIGPIPE, SIG_IGN);  // 忽略信号,write() 返回 -1 而不是崩溃

新手必踩的四个坑

应用价值:工业场景里的管道

管道是理解后续所有 IPC 的地基——文件描述符、阻塞、缓冲区、同步这些概念,在共享内存和 Socket 里全部还会再遇到。把这根水管玩明白,后面的路会顺很多。

SEO关键词

Linux 匿名管道、命名管道、mkfifo、pipe、进程间通信、IPC、嵌入式Linux、管道阻塞、SIGPIPE、Linux系统篇

评论(0