笔记 | 【Linux】两个毫无关系的进程怎么通信?命名管道 FIFO 从原理到 Server/Client 实战(学习笔记)

刷到一个挺有意思的话题,结合自己之前的经验,整理了一下核心要点。

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学方向:C++方向学爱好者 ⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

文章目录

三、FIFO 的 open() 规则比 read() 更早开始同步四、系统调用 mkfifo()五、用命名管道写一个最小 Server / Client 六、Client 一退出,Server 为什么会读到 0七、匿名管道和命名管道到底差在哪八、几个容易理解错的地方 总结

前言

前面学匿名管道时有一个很明显的限制:

父进程先 pipe()
↓
fork()
↓
子进程继承父进程的 fd
↓
父子进程看到同一条管道

这套方法很好用,但前提是两个进程之间能通过 fork() 建立文件描述符的继承关系。

如果现在有两个完全独立启动的程序:

./server
./client

它们没有父子关系,也没有谁能把自己已经打开的匿名管道 fd “继承”给另一方。

那怎么让它们找到同一份内核通信资源?

命名管道解决的正是“两个无亲缘关系进程怎样找到同一份通信资源”这个坑。


一、匿名管道缺的不是“管道”,而是一个双方都认识的入口

匿名管道没有可供无关进程重新打开的路径。

int pipefd[2];
pipe(pipefd);

pipe() 返回的 pipefd[0]、pipefd[1] 只对当前进程以及后来继承这些 fd 的进程有意义。

两个毫无关系的进程不可能提前了解对方进程里的某个 fd 是多少。

命名管道换了一种思路:

不再靠 fd 继承找到同一条管道,而是让两个进程约定同一个 pathname。

比如:

/tmp/myfifo

Server 和 Client 都去 open("/tmp/myfifo", ...),内核就能把它们连接到同一个 FIFO 对象上。

这里有个地方很容易理解偏。

命名管道确实在文件系统中拥有路径、目录项和文件类型,执行 ls -l 能看到它;但真正传输的数据并不会像普通文件一样写进磁盘文件东西。

这里记住:

路径存在于文件系统中
        ↓
用于让不同进程找到同一个 FIFO
        ↓
通信数据仍然在内核管道机制中流动

所以“有文件名”不等于“通信数据会落盘”。


二、先用命令把 FIFO 的行为跑出来

2.1 mkfifo

创建:

mkfifo fifo

查看:

ls -l fifo

命名管道的文件类型通常会显示为:

p

删除时直接:

rm fifo

或者:

unlink fifo

没必要写:

rm -rf fifo

fifo 不是目录,-r 在这里没有意义。

2.2 为什么 echo > fifo 会卡住

先执行:

echo "hello fifo" > fifo

如果此时没有任何进程以读方法打开 FIFO,Shell 会停在那里。

另开一个终端:

cat fifo

此时两边都会继续运行,cat 能读到:

hello fifo

这一步最值得观察的是:

写端 open(O_WRONLY)
        │
        │ 没有读端时等待
        ▼
读端 open(O_RDONLY)
        │
        ▼
双方建立通信关系


三、FIFO 的 open() 规则比 read() 更早开始同步

默认阻塞模式下:

打开方法另一端不存在时open(..., O_RDONLY)等待某个写端打开open(..., O_WRONLY)等待某个读端打开

也就是说,阻塞甚至可能发生在 open(),代码还没有走到 read() 或 write()。

如果使用 O_NONBLOCK,规则会变化:

O_RDONLY | O_NONBLOCK

可以立即打开。

而:

O_WRONLY | O_NONBLOCK

如果一个读端都不存在,会失败并得到 ENXIO。

排查 FIFO “程序一启动就卡住”时,先看 open() 有没有返回。

许多时候不是 read() 卡住,而是:

open(FIFO_FILE, O_RDONLY);

根本还没有返回。


四、系统调用 mkfifo()

程序中创建 FIFO:

#include
#include

int mkfifo(const char *pathname, mode_t mode);

成功返回:

0

失败返回:

-1

比如:

#include
#include
#include

const char* FIFO_FILE = "./myfifo";

int main()
{
    umask(0);

    if (mkfifo(FIFO_FILE, 0666) < 0)
    {
        if (errno != EEXIST)
        {
            perror("mkfifo");
            return 1;
        }
    }

    return 0;
}

Server 可能被重复启动,所以遇到 EEXIST 时不要先认定程序出错。

如果路径已经存在,还该确认它是不是我们期望的 FIFO,而不是同名普通文件。


五、用命名管道写一个最小 Server / Client

Server:读端
Client:写端

5.1 Server

```

include


include


include


include


include


include


include

const char* FIFO_FILE = "./myfifo";

int main()
{
umask(0);

if (mkfifo(FIFO_FILE, 0666) < 0 && errno != EEXIST)
{
perror("mkfifo");
return 1;
}

std::cout


以上就是这次整理的全部内容,希望对你有所启发。如果有不同见解,欢迎在评论区交流讨论。

评论 (0)

暂无评论