欢迎光临
我们一直在努力

【Linux 系统篇(三十二)】通信(二):命名管道详解

在这里插入图片描述

大家好,欢迎来到 huangjin007_ 的博客
⭐
个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)

总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇


Linux 系统篇(三十二) —— 命名管道详解

文章目录

  • Linux 系统篇(三十二) —— 命名管道详解
    • 一、为什么有了匿名管道,还需要命名管道?
    • 二、什么是命名管道?
      • 2.1 给管道一个名字
      • 2.2 命名管道能解决什么问题?
    • 三、如何创建命名管道?
      • 3.1 通过命令行创建
      • 3.2 通过程序创建
    • 四、匿名管道和命名管道有什么区别?
    • 五、命名管道最容易出问题的地方:`open()` 为什么会阻塞?
      • 5.1 以只读方式打开 FIFO
      • 5.2 以只写方式打开 FIFO
      • 5.3 如果使用 `O_NONBLOCK`,会发生什么?
    • 六、实战一:使用命名管道实现文件拷贝
      • 6.1 第一个程序:读取源文件,写入命名管道
      • 6.2 第二个程序:读取命名管道,写入目标文件
    • 七、实战二:使用命名管道实现 Server 和 Client 通信
      • 7.1 先设计公共代码 `comm.hpp`
      • 7.2 客户端 `client.cc`
      • 7.3 服务器 `server.cc`
      • 7.4 Makefile
    • 八、把整个过程串起来:命名管道到底是怎么工作的?
      • 8.1 命名管道通信的完整流程
    • 九、使用命名管道时,还有哪些问题需要注意?
      • 9.1 命名管道是文件,为什么不能像普通文件一样读写?
      • 9.2 命名管道中的数据会一直保存吗?
      • 9.3 为什么服务器有时收不到完整的一条消息?
    • 结语:

一、为什么有了匿名管道,还需要命名管道?

  在学习 Linux 进程间通信时,我们已经接触过匿名管道。通过 pipe() 创建一个管道,再配合 fork() 创建子进程,就可以让父子进程之间交换数据。

  例如,父进程读取一份数据,然后通过管道发送给子进程,子进程接收到数据后再进行处理。

  这种方式很好用,但随着问题逐渐复杂,我们会发现一个限制:

  匿名管道通常用于具有亲缘关系的进程之间通信。如果两个进程互不相关,它们应该怎样交换数据?

  先来看一个具体场景。

  假设我们现在有两个独立运行的程序:

  • server:负责接收消息。
  • client:负责发送消息。

  我们希望先启动服务器,再启动客户端。客户端输入一段文字,服务器就能接收到并显示出来。

  问题来了:这两个程序不是通过 fork() 创建出来的父子进程,它们没有共同的父进程,也没有提前继承对方的文件描述符。

  那么,之前学习的匿名管道还能直接使用吗?

  通常不行。匿名管道没有一个供两个独立程序共同查找的文件系统路径,双方无法仅仅通过一个共同的文件名找到并打开它。

  既然如此,我们能不能给管道一个名字,让两个互不相关的进程都能找到它?

  答案是可以的。

  这就是我们今天要学习的主角——命名管道(Named Pipe),也叫 FIFO。


二、什么是命名管道?

2.1 给管道一个名字

  匿名管道和命名管道的核心思想其实相同:都可以让一个进程写入数据,再让另一个进程读取数据。

  不同的是,命名管道在文件系统中有一个对应的名字,两个独立运行的进程可以通过这个名字找到同一个管道。

  例如,我们可以创建一个名为 fifo 的命名管道:

mkfifo fifo

  创建完成后,执行:

ls -l

  可能看到类似这样的结果:

在这里插入图片描述

  注意最前面的字符:

p

  它表示这是一个 FIFO 类型的文件,也就是命名管道。

  普通文件通常以 – 开头,目录通常以 d 开头,而命名管道以 p 开头。

  这里有一个非常重要的知识点:

  命名管道虽然在文件系统中有一个名字,但它并不是用来保存普通文件内容的。

  普通文件通常将数据持久地保存到文件中,而命名管道主要用于进程之间传递数据。数据被读取后,就会从管道的数据流中消耗掉,并不是一直保存在这个路径对应的文件里。

  可以把它理解成一个有名字的传送通道:

  • client 找到管道,向里面写入消息。
  • server 找到同一个管道,从里面读取消息。
  • 数据经过管道传递,而不是作为普通文件内容永久保存。

  因此,命名管道本质上是一种特殊类型的文件,其实际通信能力由 Linux 内核提供。

2.2 命名管道能解决什么问题?

  继续看 client 和 server。

  我们只需要让两个进程约定好同一个命名管道路径,就能分别打开它:

client ── write() ──> 命名管道 ── read() ──> server

  双方不需要是父子进程,也不需要通过 fork() 相互继承文件描述符。


三、如何创建命名管道?

  理解了命名管道的作用,接下来就要解决实际问题:怎样创建它?

  Linux 中主要有两种方式:使用命令行创建,以及在程序中调用 mkfifo()。

3.1 通过命令行创建

  最简单的方法就是使用 mkfifo 命令:

mkfifo fifo

  这条命令会在当前目录下创建一个名为 fifo 的命名管道。

  我们可以通过下面的命令查看:

ls -l fifo

  如果想删除它,可以执行:

rm fifo

  这里要注意,rm fifo 删除的是文件系统中的管道名称,而不是把已经传输过的数据清空。对于仍然打开着该 FIFO 的进程,已经打开的引用在最后一个引用关闭之前仍然可以存在。

3.2 通过程序创建

  实际开发中,我们通常需要让程序自动创建命名管道,因此要使用系统调用 mkfifo()。

它的函数原型是:

#include <sys/types.h>
#include <sys/stat.h>

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

两个参数分别表示:

  • filename:命名管道的路径。
  • mode:创建时设置的权限,例如 0644、0666。

返回值:

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno,可以使用 perror() 查看错误原因。

  下面来看一个简单的例子:

#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <cerrno>

int main()
{
// 在当前目录下创建一个名为 p2 的命名管道
if (mkfifo("p2", 0644) == –1)
{
// 如果管道已经存在,也会创建失败,errno 为 EEXIST
perror("mkfifo");
return 1;
}

printf("mkfifo success\\n");
return 0;
}

  如果创建成功,就能在目录中看到 p2。

  这里还有两个容易忽略的细节。

第一,mkfifo() 不负责打开管道。

  它只是在文件系统中创建命名管道。后续要使用它传输数据,还需要调用 open() 获取文件描述符。

第二,重复创建会失败。

  如果 p2 已经存在,mkfifo() 通常会返回 -1,并将 errno 设置为 EEXIST。实际程序中可以检查该错误,并判断已有路径是否确实是一个 FIFO,而不是直接认定创建失败就代表管道已经存在且可用。


四、匿名管道和命名管道有什么区别?

  现在我们已经知道,匿名管道和命名管道都可以传输数据,那么它们究竟有什么区别?

对比项匿名管道命名管道
创建方式 pipe() mkfifo()
打开方式 pipe() 创建时直接获得文件描述符 使用 open() 打开
是否有文件系统名称 通常没有可供双方共同打开的路径名 有
常见通信对象 父子进程等有亲缘关系的进程 可以是互不相关的进程
数据传输方式 内核提供的管道数据流 内核提供的 FIFO 数据流
是否直接保存普通文件内容 否 否
是否具有先进先出的特性 是 是

  匿名管道由 pipe() 创建并直接返回读写文件描述符;命名管道则先通过 mkfifo() 创建,再通过 open() 获取文件描述符。

  当两种管道都完成创建和打开后,它们的基本读写语义是相同的。

  例如,两者都可以使用 read() 读取数据,使用 write() 写入数据,也都可能因为管道为空、缓冲区已满或对端关闭而发生阻塞、结束或报错。


五、命名管道最容易出问题的地方:open() 为什么会阻塞?

  创建好命名管道之后,我们就可以使用 open() 打开它。

  但是,在真正编写程序之前,还有一个必须理解的问题:

  为什么有时候程序执行到 open() 就不动了,看起来像卡死了一样?

  这通常不是程序出错,而是命名管道本身的打开规则导致的。

5.1 以只读方式打开 FIFO

  假设服务器准备读取客户端发送的数据:

int fd = open("fifo", O_RDONLY);

  这里的 O_RDONLY 表示以只读方式打开。

  如果没有使用 O_NONBLOCK,也就是默认的阻塞模式,那么当 FIFO 还没有任何进程以写方式打开时,这个 open() 会一直等待。

  为什么?

  因为服务器的目的是读取数据,但目前还没有发送方建立写入连接。Linux 会让服务器等待,直到有进程以写方式打开同一个 FIFO。

可以把这个过程理解成:

服务器:我要打开管道,准备接收数据
|
v
等待写入端出现
|
v
客户端:打开同一个管道,准备发送数据
|
v
open() 完成

  注意,这里的阻塞发生在 open() 阶段,而不一定是 read() 阶段。

5.2 以只写方式打开 FIFO

  再来看客户端:

int fd = open("fifo", O_WRONLY);

  这里的 O_WRONLY 表示以只写方式打开。

  如果没有使用 O_NONBLOCK,而当前没有任何进程以读方式打开 FIFO,那么客户端的 open() 同样会阻塞。

  原因也很简单:如果没有读取端,发送方就还没有建立起正常的管道通信关系。

  因此,默认阻塞模式下有两条规则:

  • 读端打开 FIFO:等待写端打开。
  • 写端打开 FIFO:等待读端打开。

  这意味着,如果我们先启动服务器,再启动客户端,通常会看到这样的现象:

  • 服务器执行 open("fifo", O_RDONLY),暂时阻塞。
  • 客户端执行 open("fifo", O_WRONLY)。
  • 双方匹配成功,open() 返回。
  • 服务器可以开始读取,客户端可以开始写入。
  •   这也解释了为什么命名管道的两个程序可以分开启动,却仍然能够配合工作。

    5.3 如果使用 O_NONBLOCK,会发生什么?

      有时,我们不希望程序一直等待,而是希望 open() 立即返回。这时可以使用 O_NONBLOCK,即非阻塞模式。

    例如:

    int fd = open("fifo", O_RDONLY | O_NONBLOCK);

      对于 FIFO,非阻塞打开规则如下。

    打开方式是否有对端结果
    O_RDONLY 没有写端 阻塞等待
    O_RDONLY | O_NONBLOCK 没有写端 立即成功
    O_WRONLY 没有读端 阻塞等待
    O_WRONLY | O_NONBLOCK 没有读端 立即失败,errno 为 ENXIO

      为什么读端非阻塞打开时,即使没有写端也能成功?

      因为只读打开本身不需要等待写端建立连接。此时如果立即调用 read(),并且当前没有数据、也没有写端,通常会读到 0。这与普通的阻塞读取不同:如果仍然存在写端,只是暂时没有数据,那么非阻塞读取通常会返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。

    这两个场景要区分清楚:

    • 没有数据,但仍有写端: 暂时没有内容可读。
    • 没有数据,也没有写端: 当前没有发送方,read() 可以返回 0。

    六、实战一:使用命名管道实现文件拷贝

      理论讲完了,我们来解决一个实际问题。

      假设当前目录下有一个文件 abc,里面存放着一段文本。我们想将它复制为 abc.bak。

      平常复制文件,可以使用 cp 命令。那么,如果不直接让一个程序从源文件读取后写入目标文件,而是让两个独立程序通过命名管道完成数据传输,该怎么做?

      我们可以把任务拆分成两部分:

    • 程序 A:读取 abc,将读取到的数据写入 FIFO。
    • 程序 B:从 FIFO 读取数据,将数据写入 abc.bak。

      整体流程如下:

    在这里插入图片描述

    6.1 第一个程序:读取源文件,写入命名管道

      先实现程序 A。

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <cstdlib>
    #include <cstdio>
    #include <cerrno>

    #define ERR_EXIT(m) \\
    do \\
    { \\
    perror(m); \\
    exit(EXIT_FAILURE); \\
    } while (0)

    int main()
    {
    // 创建命名管道;如果已经存在,后面还可以继续使用
    if (mkfifo("tp", 0644) == –1 && errno != EEXIST)
    ERR_EXIT("mkfifo");

    // 打开源文件,只读
    int infd = open("abc", O_RDONLY);
    if (infd == –1)
    ERR_EXIT("open abc");

    // 打开 FIFO,只写;没有读端时会阻塞等待
    int outfd = open("tp", O_WRONLY);
    if (outfd == –1)
    ERR_EXIT("open tp");

    char buf[1024];
    ssize_t n;

    // 每次读取一部分数据,再写入 FIFO
    while ((n = read(infd, buf, sizeof(buf))) > 0)
    {
    ssize_t sent = 0;

    // write 可能只写入部分数据,因此循环写完当前数据块
    while (sent < n)
    {
    ssize_t ret = write(outfd, buf + sent, n – sent);
    if (ret == –1)
    {
    if (errno == EINTR)
    continue;
    ERR_EXIT("write tp");
    }
    sent += ret;
    }
    }

    // read 返回 -1 表示读取出错
    if (n == –1)
    ERR_EXIT("read abc");

    close(infd);
    close(outfd);

    return 0;
    }

      首先,open("abc", O_RDONLY) 打开源文件。接着,open("tp", O_WRONLY) 打开命名管道的写端。如果此时还没有程序打开读端,当前程序就会阻塞等待。

      进入循环后,read() 每次最多读取 1024 字节。读取多少,就向管道写入多少。文件很大也没有关系,不需要一次性将整个文件读入内存。

      这里特意处理了一个容易忽略的问题:write() 不一定一次就能写完要求的数据,因此要检查实际写入的字节数,并继续写入剩余部分。

    6.2 第二个程序:读取命名管道,写入目标文件

      程序 B 负责接收数据,并将数据写入 abc.bak。

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <cstdlib>
    #include <cstdio>
    #include <cerrno>

    #define ERR_EXIT(m) \\
    do \\
    { \\
    perror(m); \\
    exit(EXIT_FAILURE); \\
    } while (0)

    int main()
    {
    // 创建或复用已经存在的 FIFO
    if (mkfifo("tp", 0644) == –1 && errno != EEXIST)
    ERR_EXIT("mkfifo");

    // 创建目标文件;若已存在,则清空原内容
    int outfd = open("abc.bak",
    O_WRONLY | O_CREAT | O_TRUNC,
    0644);
    if (outfd == –1)
    ERR_EXIT("open abc.bak");

    // 打开 FIFO,只读;没有写端时会阻塞等待
    int infd = open("tp", O_RDONLY);
    if (infd == –1)
    ERR_EXIT("open tp");

    char buf[1024];
    ssize_t n;

    // 读取 FIFO,直到所有写端关闭且数据读完
    while ((n = read(infd, buf, sizeof(buf))) > 0)
    {
    ssize_t sent = 0;

    // 确保当前读到的数据全部写入目标文件
    while (sent < n)
    {
    ssize_t ret = write(outfd, buf + sent, n – sent);
    if (ret == –1)
    {
    if (errno == EINTR)
    continue;
    ERR_EXIT("write abc.bak");
    }
    sent += ret;
    }
    }

    if (n == –1)
    ERR_EXIT("read tp");

    close(infd);
    close(outfd);

    // 删除 FIFO 的文件系统名称
    if (unlink("tp") == –1)
    perror("unlink tp");

    return 0;
    }

    open() 的几个选项:

    • O_WRONLY:只写。
    • O_CREAT:目标文件不存在时创建它。
    • O_TRUNC:目标文件已存在时,将其原有内容截断为零长度。

      为什么程序 B 要等到 read() 返回 0 才结束?

      因为 FIFO 是数据流。只要还有写端打开,读取端就不能仅仅因为当前暂时没有数据,就认定通信结束。

      当所有写端都关闭,并且 FIFO 中剩余的数据已经读取完毕时,read() 才会返回 0,告诉程序已经到达当前数据流的末尾。


    七、实战二:使用命名管道实现 Server 和 Client 通信

      前面的文件拷贝让我们理解了命名管道的数据传递过程。但在实际开发中,更常见的学习场景是让两个程序持续交换信息。

      现在我们实现一个简单的通信程序:

    • server:服务器,负责接收并显示客户端消息。
    • client:客户端,负责从键盘读取输入,再发送给服务器。
    • fifo:双方共同使用的命名管道。

    最终效果类似这样:

    在这里插入图片描述

      这个例子仍然是单向通信:客户端发送,服务器接收。如果想让服务器也能主动回复客户端,可以再设计一个用于反向传输的 FIFO。

    7.1 先设计公共代码 comm.hpp

      我们先把创建管道、打开管道、读写数据等操作封装成类,避免客户端和服务器各自重复编写相同的代码。

    #pragma once

    #include <iostream>
    #include <string>
    #include <cerrno>
    #include <cstring>
    #include <cstdlib>

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>

    // 负责创建和删除命名管道
    class NamedFifo
    {
    public:
    NamedFifo(const std::string& path, const std::string& name)
    : _fifoname(path + "/" + name), _created(false)
    {
    // 如果管道已存在,不重复创建
    if (mkfifo(_fifoname.c_str(), 0666) == 0)
    {
    _created = true;
    std::cout << "mkfifo success: "
    << _fifoname << std::endl;
    }
    else if (errno == EEXIST)
    {
    // 已存在的路径必须确实是 FIFO
    struct stat st;
    if (lstat(_fifoname.c_str(), &st) == –1 ||
    !S_ISFIFO(st.st_mode))
    {
    std::cerr << "path exists but is not a FIFO"
    << std::endl;
    std::exit(EXIT_FAILURE);
    }

    std::cout << "FIFO already exists: "
    << _fifoname << std::endl;
    }
    else
    {
    perror("mkfifo");
    std::exit(EXIT_FAILURE);
    }
    }

    ~NamedFifo()
    {
    // 只删除本对象创建的 FIFO,避免误删别人已有的路径
    if (_created && unlink(_fifoname.c_str()) == –1)
    perror("unlink FIFO");
    }

    private:
    std::string _fifoname;
    bool _created;
    };

    // 负责打开、读取和写入命名管道
    class FileOper
    {
    public:
    FileOper(const std::string& path, const std::string& name)
    : _fifoname(path + "/" + name), _fd(–1)
    {
    }

    void OpenForRead()
    {
    // 阻塞模式下,等待写端打开 FIFO
    _fd = open(_fifoname.c_str(), O_RDONLY);
    if (_fd == –1)
    perror("open FIFO for read");
    else
    std::cout << "open FIFO for read success"
    << std::endl;
    }

    void OpenForWrite()
    {
    // 阻塞模式下,等待读端打开 FIFO
    _fd = open(_fifoname.c_str(), O_WRONLY);
    if (_fd == –1)
    perror("open FIFO for write");
    else
    std::cout << "open FIFO for write success"
    << std::endl;
    }

    void Read()
    {
    if (_fd == –1)
    {
    std::cerr << "FIFO is not open" << std::endl;
    return;
    }

    char buffer[1024];

    while (true)
    {
    // 留一个字节存放字符串结束符
    ssize_t n = read(_fd, buffer, sizeof(buffer) – 1);

    if (n > 0)
    {
    buffer[n] = '\\0';
    std::cout << "client says: " << buffer;
    std::cout.flush();
    }
    else if (n == 0)
    {
    // 所有写端关闭,且管道数据已经读完
    std::cout << "client closed the pipe"
    << std::endl;
    break;
    }
    else if (errno == EINTR)
    {
    // 被信号打断,继续读取
    continue;
    }
    else
    {
    perror("read FIFO");
    break;
    }
    }
    }

    void Write()
    {
    if (_fd == –1)
    {
    std::cerr << "FIFO is not open" << std::endl;
    return;
    }

    std::string message;

    while (true)
    {
    std::cout << "Please enter#: ";

    if (!std::getline(std::cin, message))
    break;

    if (message == "quit")
    break;

    // 加上换行符,方便服务器显示消息
    message += '\\n';

    // 保证当前消息全部写入 FIFO
    size_t sent = 0;
    while (sent < message.size())
    {
    ssize_t n = write(
    _fd,
    message.data() + sent,
    message.size() – sent
    );

    if (n == –1 && errno == EINTR)
    continue;

    if (n <= 0)
    {
    perror("write FIFO");
    return;
    }

    sent += static_cast<size_t>(n);
    }
    }
    }

    void Close()
    {
    if (_fd != –1)
    {
    close(_fd);
    _fd = –1;
    }
    }

    ~FileOper()
    {
    // 析构时兜底关闭文件描述符
    Close();
    }

    private:
    std::string _fifoname;
    int _fd;
    };

      这里使用了两个类,各自负责不同的工作。

      NamedFifo 负责管道的创建和清理。FileOper 负责打开管道以及通过文件描述符进行读写。

      将它们分开,是为了让职责更加清晰:管道的生命周期管理与数据传输操作不必混在一起。

      需要特别留意 NamedFifo 的析构函数:只有当前对象成功创建了 FIFO,才会尝试删除它。如果管道原本就存在,这个对象不会擅自删除它。

      不过,在多进程程序中,如果某个进程创建了 FIFO,另一个进程正在使用它,创建者退出时删除路径仍然可能影响后续进程重新打开它。因此,实际项目应当明确规定由谁负责创建和删除 FIFO,并协调好清理时机。

    7.2 客户端 client.cc

      客户端不需要创建管道。它只需要打开服务器已经准备好的 FIFO,然后不断读取用户输入并发送消息。

    #include "comm.hpp"

    int main()
    {
    // 创建文件操作对象,指定当前目录下的 fifo
    FileOper writerfifo(".", "fifo");

    // 打开 FIFO 写端;服务器需要先打开读端
    writerfifo.OpenForWrite();

    // 如果打开失败,就不要继续发送
    writerfifo.Write();

    // 关闭文件描述符
    writerfifo.Close();

    return 0;
    }

      客户端代码很短,因为实际读写逻辑已经封装在 FileOper 中。

      其中,"." 表示当前目录,"fifo" 表示管道名称,两者组合后得到 ./fifo。

      当客户端输入普通文本时,消息会被写入管道;输入 quit 时,客户端退出输入循环,关闭写端。

      这里的 quit 只是客户端自己约定的退出命令,并不会作为消息发送给服务器。

    7.3 服务器 server.cc

      服务器负责创建命名管道,并打开读端等待客户端消息。

    #include "comm.hpp"

    int main()
    {
    // 创建 FIFO;如果已经存在,则验证并复用
    NamedFifo fifo(".", "fifo");

    // 创建文件操作对象
    FileOper readerfifo(".", "fifo");

    // 打开读端,等待客户端打开写端
    readerfifo.OpenForRead();

    // 循环接收并显示客户端发送的消息
    readerfifo.Read();

    // 关闭文件描述符
    readerfifo.Close();

    return 0;
    }

      服务器启动后,先创建 fifo,再调用 OpenForRead()。

      如果客户端还没有启动,服务器会在打开读端时等待。客户端启动后,打开同一个 FIFO 的写端,双方就能建立通信关系。

      服务器接收到数据后,通过 Read() 输出消息。当所有写端关闭且管道中的数据已经读完时,read() 返回 0,服务器退出读取循环,随后关闭文件描述符。

      这里有一个需要明确的细节:服务器的 NamedFifo 对象在 main() 结束时析构。如果它创建了 FIFO,就会尝试删除 FIFO 的名称。因此,这份示例适合展示单个客户端完成一轮通信的基本过程,不是完整的多客户端服务器实现。

    7.4 Makefile

      我们可以通过 Makefile 一次编译客户端和服务器。

    .PHONY: all clean

    all: client server

    client: client.cc comm.hpp
    g++ -o $@ client.cc -std=c++11

    server: server.cc comm.hpp
    g++ -o $@ server.cc -std=c++11

    clean:
    rm -f client server

    这里有几个细节:

    • all 是默认目标,执行 make 时会编译 client 和 server。
    • $@ 表示当前目标名称,例如 client。
    • 将 comm.hpp 加入依赖列表后,修改头文件也会触发对应程序重新编译。
    • clean 用于删除编译生成的可执行文件,不会删除 FIFO。

    八、把整个过程串起来:命名管道到底是怎么工作的?

      前面分别学习了创建命名管道、打开规则、文件拷贝以及客户端和服务器通信。现在我们把这些知识点串联起来,形成一个完整的理解。

      假设服务器和客户端分别运行在两个终端中。

    8.1 命名管道通信的完整流程

    在这里插入图片描述


    九、使用命名管道时,还有哪些问题需要注意?

    9.1 命名管道是文件,为什么不能像普通文件一样读写?

      因为它属于特殊文件。

      普通文件的主要用途是保存数据,而 FIFO 的主要用途是提供进程间的数据传输通道。虽然它们都可以使用 open()、read()、write() 和 close() 等接口,但底层行为并不完全相同。

      例如,FIFO 通常不能像普通文件那样使用 lseek() 随意移动读写位置。

    9.2 命名管道中的数据会一直保存吗?

      不会。

      FIFO 中的数据由内核管理,读取端读走数据后,这些数据就从管道的数据流中消耗掉了。

      如果希望数据能够长期保存,就应该使用普通文件或其他持久化存储方式,而不是把 FIFO 当作文件仓库。

    9.3 为什么服务器有时收不到完整的一条消息?

      因为 FIFO 是字节流,而不是自动划分消息的队列。

      例如,客户端执行两次写入:

    write(fd, "Hello", 5);
    write(fd, "World", 5);

      服务器可能一次读取 10 个字节,也可能先读到 "Hello",再读到 "World",还可能在不同的读取操作中取得部分数据。

      因此,不能简单地认为:

    一次 write() = 一次 read()

      如果要传输具有明确结构的消息,就需要设计协议,而不能依赖每次读取都恰好对应一次写入。


    结语:

      今天的内容到这里就结束了,希望你能有所收获~

    干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux 系统篇(三十二)】通信(二):命名管道详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址