Linux:进程间通信 和 简单进程池

1.进程间通信介绍

我们以前学习进程的时候,进程之间的独立性都很强,即使对于父子进程来说,虽然父进程的数据可以给子进程共享,但是子进程也无法修改父进程的数据,并且因为写时拷贝,子进程也无法给父进程共享自己的数据。但是如果我们就是想要让进程与进程之间能够协同工作呢?这就需要进程间通信的知识。

1.1 基本概念

进程间通信(IPC,Inter‑Process Communication ),是指操作系统提供的、允许不同进程之间进行数据交换、信息传递、状态同步、资源协调的一系列机制与接口。由于每个进程拥有独立的虚拟地址空间,进程之间天然相互隔离,无法直接访问对方内存,因此必须依靠操作系统内核提供的特殊方式,实现跨进程的数据交互与协作。

因此我们可以说,进程间通信的本质, 就是让不同的进程能看到同一份资源,而这个资源大家姑且可以粗暴的认为就是内存块,而这个内存块是由操作系统通过系统调用提供的。

1.2 进程间通信的目的

1. 数据传输:一个进程将数据发送给另一个进程,完成信息传递;

2. 资源共享:多个进程共享同一部分资源,避免重复创建、节约系统开销;

3. 进程控制:一个进程控制另一个进程的执行,如暂停、唤醒、终止,或处理异常事件;

4. 通知事件:进程间互相发送信号,告知对方某类事件发生;

5. 同步互斥:协调多个进程的执行顺序,避免同时争抢临界资源,防止数据错乱、竞态条件。

2. 管道

管道是 Unix 中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个 "管道"。它是 Linux 最基础的进程间通信(IPC)方式 ,本质是内核维护的一块环形缓冲区 ,用于实现有亲缘关系进程间的单向数据传输。 管道分为匿名管道命名管道(FIFO)。

在我们讲解父子进程的时候,其实有一个场景就涉及了管道的知识:

这幅图中,父进程打开磁盘中的文件hello.txt,打开文件时内核会生成一个struct file结构体,该结构体通过inode记录文件属性,通过文件操作表提供读写接口,并将文件内容加载到文件内核缓冲区;父进程会在自身文件描述符表中记录该struct file的地址,对外返回文件描述符下标。当父进程创建子进程时,子进程会拷贝父进程的 PCB 及文件描述符表,子进程的文件描述符表同样指向同一个struct file结构体,内核不会为子进程新建该结构体,因此父子进程可共享该内核资源,父进程向文件内核缓冲区写入数据,子进程可从中读取,至此父子进程之间具备了进程间通信的条件。

这种基于文件系统、依靠内核缓冲区实现的通信方式,和管道底层原理完全一致。因此管道的本质是基于文件系统,进行内核级的进程间通信。

3. 匿名管道

3.1 基本概念

我们上面的讲解对于管道的底层原理,还是基于磁盘去做的,但是真正的对于真正的管道,我们先讨论匿名管道,实际上是完全不用依赖磁盘的,而是直接使用系统调用。

匿名管道是 Linux 中最基础的半双工、有亲缘关系进程间通信方式 ,通过 pipe() 系统调用创建,依托 Linux 文件系统架构实现,本质是内核开辟的一块环形内存缓冲区

因为匿名管道是只存在内核内存 里,不在磁盘、不在文件系统, 只能靠 pipe() 创建,只能靠文件描述符 访问。只有父子 / 亲缘进程能继承 fd 使用,外部进程完全找不到、用不了。那也就不需要文件名和文件的路径信息,因此才叫做匿名管道。

匿名管道通过 pipe 系统调用,在内核中创建内存级临时 inode、struct file 结构体与专属内核缓冲区全程无需磁盘参与 。父进程创建管道后调用 fork 生成子进程,子进程拷贝父进程的 PCB 与文件描述符表,父子进程通过文件描述符指向同一个内核缓冲区, 并且要注意的是,此时子进程也会拷贝一份struct file ;父进程向内核缓冲区写入数据,子进程从缓冲区读取数据,依靠文件系统模型完成内核级、单向的父子进程间通信,这就是匿名管道。

3.1.1 pipe 系统调用

pipe 是 Linux 系统中用于创建匿名管道 的核心系统调用,是实现亲缘进程(父子 / 兄弟进程)间内核级通信的基础机制。该调用由内核直接提供,全程基于内存缓冲区完成数据交互,不依赖磁盘文件,因此也被称为无名管道。

cpp 复制代码
#include <unistd.h>
int pipe(int fd[2]);

调用成功时返回 0,失败时返回 -1。参数 fd 是一个长度为 2 的整型数组,用于接收内核分配的两个文件描述符,其中:

  • fd[0] 固定作为管道读端,只能从管道中读取数据;
  • fd[1] 固定作为管道写端,只能向管道中写入数据。

当进程执行 pipe 调用时,内核会在内核空间中创建一块环形内存缓冲区,同时构建对应的管道管理结构,将读端与写端文件描述符关联到同一缓冲区。这两个文件描述符会被添加到调用进程的文件描述符表中,使进程具备对管道的读写能力。

3.2 通用通信分类概念

通用通信方式分为单工、半双工、全双工三类。

  1. 单工通信(Simplex)

**信息只能沿一个固定方向传输,永远不能反向。**就好比说是老师对于学生,在上课的时候是老师单向对学生进行信息输出,此时信息传输的方向是固定的。

  1. 半双工通信(Half‑duplex)

**同一时刻只能单向传输,但方向可以切换。**就好比是我们日常沟通交流,都是你一句我一句这样来回切换信息输出方向。

  1. 全双工通信(Full‑duplex)

**两个方向可以同时收发数据。**这就好比如说是在吵架的时候,大家都在互相问候,谁也听不进去谁的,都在声嘶力竭的给对方输出。

我们所提到的匿名管道实际就是半双工的通信模式的。

3.3 文件描述符角度理解管道

这张图就演示了 匿名管道 和 fork 结合的场景:

第一步,父进程创建匿名管道。父进程调用 pipe( ) 系统调用,由内核在内存中创建匿名管道缓冲区,同时在父进程的文件描述符表中分配两个专用文件描述符:fd[0]=3为管道读端,fd[1]=4为管道写端,分别对应管道的读取与写入接口;0、1、2 号文件描述符默认绑定标准输入、标准输出、标准错误终端设备,此时管道仅能被父进程访问。

第二步,父进程通过fork()创建子进程。子进程完整拷贝父进程的文件描述符表,因此子进程同样拥有管道读端fd[0]=3与写端fd[1]=4;父子进程共享内核中唯一的管道缓冲区,实现对同一通信资源的访问,为进程间数据交互奠定基础。此时父子进程均可对管道进行读写操作。

第三步,关闭冗余端口,建立单向半双工通信。父进程关闭自身管道读端fd[0],仅保留写端用于向管道写入数据;子进程关闭自身管道写端fd[1],仅保留读端用于读取管道数据。最终形成父进程写入、子进程读取的固定单向传输模式,完成父子进程间的匿名管道通信。

之所以在这里父子进程要各自关闭一个读端一个写端,是因为:首先,匿名管道为半双工通信 ,数据仅能从写端流向读端。父进程关闭读端、子进程关闭写端,可强制固定父进程写入、子进程读取的单向传输方向,避免父子进程双向读写产生数据错乱、覆盖的问题,保障数据有序交互。

其次,为规避进程永久阻塞。因为管道存在读写阻塞机制,当管道无数据时读端会阻塞等待;若存在任意打开的写端,读端将持续阻塞。子进程关闭自身写端后,所有管道写端将随父进程写入完成而全部关闭,子进程读取完毕后可正常结束,避免进程卡死。

同时,文件描述符属于内核有限资源,关闭冗余端口能够及时释放闲置句柄,减少系统资源占用,防止资源泄漏。最后,匿名管道生命周期依赖所有持有端口的进程关闭后才会销毁,规范关闭多余端口可精准管控管道生命周期,通信结束后内核可正常释放管道缓冲区内存。

3.4 内核角度理解管道

该图从Linux 内核底层视角,展示了匿名管道实现进程间通信的核心结构与数据交互逻辑,清晰呈现了进程、文件结构、索引节点(inode)与管道数据缓冲区的关联关系。

进程 1 与进程 2 分别维护各自独立的file文件结构,结构体包含访问模式f_mode、文件偏移f_pos、标志位f_flags、引用计数f_count、索引节点指针f_inode、操作函数集f_op等内核成员。两个进程的file结构通过f_inode指针,指向内核中同一个管道专属的 inode 索引节点,inode 节点再关联管道的数据页缓冲区,由此实现两个进程对同一管道资源的共享。

其中,进程 1 通过自身file结构发起管道写操作 write ,将数据写入共享的数据页缓冲区;进程 2 通过自身file结构执行管道读操作 read,从同一数据页中读取数据。两个进程虽拥有独立的文件描述信息,但依托唯一的 inode 节点与公共数据缓冲区,完成数据的单向传递。

3.5 管道样例

接下来乃至以后,为了方便我们的文章中的代码编写以及日常练习,我们就要逐渐从 vim编辑器 转向 VScode了,下面对VScode做一个介绍。

3.5.1 VScode介绍

为了保证文章的连贯性,我将文章用链接的形式展示:

https://blog.csdn.net/2502_91842264/article/details/161192195?fromshare=blogdetail&sharetype=blogdetail&sharerId=161192195&sharerefer=PC&sharesource=2502_91842264&sharefrom=from_link

3.5.2 代码样例 1

首先我们来验证一下 pipe 系统调用:

我们先创建一个容量为 2 的数组用于传参,用于接收内核分配的两个文件描述符。接着使用 n 接收 pipe系统调用的返回值, 调用成功时返回 0,失败时返回 -1。然后我们看到,果然 pipefd0 和 pipefd1 接收到的是对应文件描述符表中的下标 3 和 4 。

当管道创建好了之后,我们fork创建子进程,再关闭父子进程各自对应的管道:

接下来直接展示后半段完整代码:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/wait.h> 
#include <stdlib.h>
#include <string.h>

int main()
{
   //1.创建管道
   int pipefd[2] = {0};
   int n = pipe(pipefd);
   if(n < 0)
   {
    perror("pipe");
    return 1;
   }
   printf("pipefd[0]:%d,  pipefd[1]:%d\n",pipefd[0],pipefd[1]);
   //sleep(1);
  
   //2.fork创建子进程
   pid_t id = fork();
   if(id == 0)//子进程去写
   {
    close(pipefd[0]);
    char *msg = "hello world!";
    int cnt = 10;
    char outbuffer[1024];
    while(cnt)
    {
        //格式化输出,指定输入到outbuffer中
        snprintf(outbuffer,sizeof(outbuffer),"father -> child # %s %d %d\n",msg,cnt--,getpid());
        write(pipefd[1],outbuffer,strlen(outbuffer));
        sleep(1);
    }

    exit(0);
   }
   //父进程去读
   close(pipefd[1]);
   char buffer[1024];

   char inbuffer[1024];
   while(1)
   {
        ssize_t n = read(pipefd[0],inbuffer,sizeof(inbuffer)-1);
        if(n > 0)//说明读取到了有效信息
        {
           inbuffer[n] = 0;//让数组中最后一位有效数字的后一位为 \0 
           printf("%s",inbuffer);
        }
        else if(n == 0)
        {
            //读到末尾,对方关闭写端
            printf("打印完毕,子进程已退出,管道关闭 \n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
   }
   pid_t rid = waitpid(id , NULL ,0);//等待子进程

   (void)rid; // 防止编译器出现未使用定义变量的告警
   return 0;
}

首先调用pipe()创建匿名管道,得到读端文件描述符pipefd[0]、写端文件描述符pipefd[1];随后通过fork()创建子进程,父子进程共享该管道。子进程作为写入端,主动关闭管道读端,通过snprintf拼接包含自定义文本、计数与进程 ID 的字符串,循环 10 次、每次间隔 1 秒将数据写入管道;父进程作为读取端,主动关闭管道写端,循环调用read系统调用阻塞读取管道数据,读到有效数据则格式化输出,当管道写端全部关闭时结束读取,最终调用waitpid()回收子进程资源,避免产生僵尸进程。

其中snprintf是 C 语言标准格式化输出函数,函数原型为

cpp 复制代码
//标准定义
#include <stdio.h>

int snprintf(char *str, size_t size, const char *format, ...);

//对应代码中的语句是
snprintf(outbuffer, sizeof(outbuffer), "father -> child # %s %d %d\n", msg, cnt, pid);

第一个参数str为格式化后字符串的存储缓冲区,第二个参数size限制缓冲区最大写入字节数,避免内存溢出,第三个参数format为格式化模板字符串,后续可变参数用于填充模板内占位符。本代码中该函数将文本、递减计数、进程 ID 拼接为完整字符串,同时在末尾添加换行符,使后续printf可自动刷新缓冲区实时输出内容。

read是 Linux 系统调用,原型为:

cpp 复制代码
//标准定义
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

//代码中对应的语句是
ssize_t n = read(pipefd[0], inbuffer, sizeof(inbuffer)-1);

用于从指定文件描述符读取数据到缓冲区,fd为读取对象的文件描述符,本代码中为管道读端pipefd[0]buf为接收数据的缓冲区;count为单次最大读取字节数。

其返回值n的变化直接对应管道通信状态,

1. > 0 :实际读到的字节数

2. = 0 :遇到 EOF(文件结束 / 管道写端关闭)

3. = -1 :读取失败,错误码保存在 errno

当管道内存在子进程写入的数据时,read读取到有效字节,n大于 0 的实际读取字节数 ;子进程完成 10 次写入后执行exit(0)退出,操作系统自动关闭子进程持有的管道写端,而父进程早已关闭自身的管道写端,此时管道所有写端全部关闭read读取不到任何数据,返回值n变为 0;若读取过程中出现文件描述符异常、权限错误等问题,n会返回 - 1,代表读取失败。正是基于n从大于 0 逐步变为 0 的状态变化,父进程可判断数据读取完成,退出读取循环,完整实现父子进程的单向数据通信。

最后执行的结果就如下图所示:

3.5.2.1 样例 1 总结

刚刚的代码中,我们对匿名管道做了一个小小的印证,从子进程中写数据,父进程确实是能通过管道看到子进程写入的数据,实现内容交互的场景,接下来我们就要对匿名管道做一点阶段性总结:

  1. 管道是只能单向通信,即单工通信。

  2. 匿名管道只能用来进行父子进程间的通信,因为是通过子进程能继承父进程的内核资源来实现的,也就是说管道通信是文件的功劳,但真正能让父子进程能够看到对方的数据,其实还是fork的功劳。

  3. 像我们平时在Xshell上直接输入的命令,其实也是兄弟进程之间通过匿名管道进行信息交互:

我们可以看到调用的三个 sleep 命令的父进程的 PID 都是一样的。

  1. 管道是面向字节流的。这句话的意思是说:管道只认一堆连续的字节,不管你发的是整数、字符串、结构体,它只管按顺序收发二进制字节,不自带消息边界。它传输连续无边界的字节序列,不区分消息,不自带数据格式,读写按字节顺序进行,需要用户自己划分消息边界。

具体是什么意思,我们可以用这段代码来说明:

我们可以看到,当在打印的时候让 cnt 逢三就睡眠一秒钟,父进程打印的结果就是三条三条打印,而不是像我们之前看到的一条一条打印的过程,但是对于子进程来说,写入的过程是一条一条写入的,只不过当 cnt = 3 的时候会睡眠一秒而已。这就说明了,如果没有用户自动设置边界,管道的读取就是一次性读完的,而不是根据写入的多少来调整。

用一个生活化的例子说明就是:假如我买了很多快递,收件地址都是菜鸟驿站。第一天快递员往菜鸟驿站入库了一件快递,第二天一次性入库了两件,第三天一次性入库了三件。虽然在三天里面入库的快递件数都不一样,但是我可以在第四天集中把它们统一都取出来。也就是说在这个过程里面,我不管快递员往菜鸟驿站里放了多少件快递,或者说以怎样的数量放入的,我只要在我需要的时候,就可以把快递所有一次性全部取出来。

在这段关系中,菜鸟驿站 = 管道缓冲区; 快递员往驿站放快递 = 进程 write 写管道 ; 去取快递 = 进程 read 读管道 ; 每一件快递 = 一个字节 ; 字节流 = 一堆连续快递,驿站根本不管你是分几次放的、每次放几件。

  1. 如果开启管道之后,父子进程都已经结束了,但是并没有手动关闭管道,那么此时管道就会被操作系统自动回收。

这是因为,调用 pipe() 创建管道时,内核会为管道的读端、写端分别创建对应的 struct file 文件对象,每个对象内部维护引用计数成员 f_count(部分内核版本命名为 ref_count),用于记录当前持有该文件对象的进程数量。

当父进程通过 fork() 创建子进程时,子进程会完整复制父进程的文件描述符表,父子进程的文件描述符将指向同一个管道对应的 struct file 对象,每一次复制都会使对应文件对象的引用计数 f_count ++ 。

进程正常或异常终止时,操作系统会自动执行进程退出清理逻辑:遍历该进程打开的所有文件描述符,对每个文件描述符关联的 struct file 对象,将其引用计数 f_count 递减。

对于管道场景,若父子进程均已结束,即便代码中未手动调用 close() 关闭管道文件描述符,内核也会通过进程退出隐式关闭所有管道 fd,使管道读端、写端对应的 struct file 引用计数全部归零。当引用计数为 0 时,内核判定当前无任何进程持有管道资源,会自动释放管道内核缓冲区、销毁管道 inode 与文件对象,完成管道资源的回收。

  1. 如果子进程写入的速度比较慢,那父进程就会阻塞等待,直到管道中有了有效数据,父进程才能继续读。

  2. 子进程如果写入的速度远远快于父进程,那写入的内容就会在管道中堆积,一旦管道被写满,子进程就会进入阻塞状态。

  3. 读端在持续读取,但是写端直接关闭,当读端读完管道中的剩余数据,再读就只能读取到空内容,那么 read 的返回值就一直是 0 ,表明该管道读到文件结尾。

  4. 如果写端一直在写,但是读端直接关闭,因为只写不读这是没有意义的,那么操作系统就会直接杀掉写端进程。

4. 进程池

因为前面提到了匿名管道,这里为了让大家能更好的熟悉和理解,所以需要讲解进程池的知识,但是其内容也不少,因此为了不影响文章的连贯性,我把进程池的内容放到了另外一篇文章,这是文章链接:

https://blog.csdn.net/2502_91842264/article/details/161091425?fromshare=blogdetail&sharetype=blogdetail&sharerId=161091425&sharerefer=PC&sharesource=2502_91842264&sharefrom=from_link

大家可以看完之后再接着看下面的内容。

5. 命名管道

5.1 基本概念

命名管道也叫 FIFO ,是 Linux 下一种半双工、面向字节流、持久化、可用于任意进程间通信的特殊文件类型。 它和前面提到的 匿名管道(pipe) 是亲兄弟,它的通信规则、阻塞逻辑与匿名管道完全一致,但突破了血缘限制,能力更强。

匿名管道只能用于有血缘关系的父子、兄弟进程之间通信,必须通过fork继承文件描述符使用,生命周期是临时的,进程退出后便随之消失,没有实体名称,仅存在于内存中;命名管道则支持任意无血缘的无关进程间通信,无需依赖fork,通过文件名即可完成打开通信,以特殊文件的形式持久存在于文件系统中,具备标识名称,任意知晓该名称的进程都能使用,简单来说,匿名管道是临时专用通道,命名管道则相当于公共电话亭。

如图所示当一个进程想要打开磁盘上的一个文件时,操作系统会为这个文件分配一个struct file结构体,用来存储文件的属性和代码数据。但是当多个进程要打开同一份文件时,每个进程调用一次 open,内核就新建一个独立的 struct file,这个结构体只维护该进程自己的读写偏移量、打开模式、标志位 ,不保存文件内容。文件内容、权限、大小、时间等属性,存在磁盘 inode 里 ,内核只会把它加载到内存中一份 inode 缓存,所有进程共享这一份,不会重复加载。

那这也就相当于,让多个进程,看到同一份资源,这就是管道的底层逻辑。那么说是多个进程打开同一份文件,怎么保证这几个进程打开的确实是同一份文件呢?只要打开文件的时候,用的是相同的路径+文件名,就能保证是同一个文件。既然需要路径+文件名,所以叫做命名管道。

创建方式有在命令行中创建和在程序中创建两种:

bash 复制代码
// 在命令行中创建
mkfifo [选项] 管道文件名

执行后会在当前目录生成名为 myfifo 的管道文件,通过 ls -l 查看,文件权限首位为 p,代表命名管道。该命令本质是底层调用 mkfifo 系统调用实现创建。

我们用两台机器来演示一下:

下面是程序中的 mkfifo 函数:

cpp 复制代码
#include <sys/stat.h>
#include <sys/types.h>

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

mkfifo 是 POSIX 标准的系统调用函数,用于在 C/C++ 程序中动态创建命名管道,需要包含指定头文件,是进程间通信编程的标准接口。pathname:要创建的命名管道文件路径 + 名称;mode:管道文件的访问权限,例如 0666(可读可写),权限会受系统权限掩码 umask 影响。

创建成功:返回 0;创建失败:返回 -1(例如文件已存在时会报错)。作用是:在指定路径创建一个命名管道文件,不占用磁盘空间,仅作为进程间通信的标识,使用方式与普通文件一致,通过 open/close/read/write 操作。

而命名管道的本质,就是用 mkfifo 创建一个专门用于进程间传输信息的特殊文件。 它看起来像文件、有文件名、能在磁盘看到,但不存真实数据、不占磁盘空间只作为 "通信通道标识",让无关进程通过它传递数据。

5.2 使用命名管道

在这里我们选择将对管道的管理封装成类的形式,因为管道的操作不是孤立的,它是围绕一个"资源实体"展开的,这个资源就是管道文件描述符。把这个文件描述符、路径、权限以及围绕它的一系列操作(增删查改)都封装在一个类里,把状态和行为绑在一起,代码逻辑会更内聚,用起来也更像在操作一个"管道对象",而不是散落的一堆函数调用。

另外,我们创建了两个文件去模拟客户端和服务端,一个是 Client ,另一个是 Server。其中通过命名管道来实现信息交互。我们让服务端去创建管道,而服务端不必且不能创建管道,因为这样的话就会存在两个管道,完全没必要。我们的目的,是要让客户端向管道内输入内容,服务端通过管道去获取客户端输入的信息。

首先看构造函数。我们初始化了三个成员:管道文件的路径 _commfile,默认就是 "./fifo";权限模式 _mode,默认 0666;文件描述符 _fd,先置成 -1。这个 _fd 很关键,后续的读写全靠它。

然后是 Build(),负责创建管道文件。它没有直接调 mkfifo,而是先调用 IsExists() 判断文件是否已存在。

IsExists()stat 系统调用来检查,没有直接用 open,因为命名管道用 open 打开时会阻塞,因为当你用 open 打开命名管道时,如果另一端还没有进程打开,open 会卡住不动,一直等,直到另一端也打开,才会继续往下执行,所以不适合用来做存在性判断。

这里的 stat 系统调用是 Linux 下非常重要的系统调用 ,作用是:不打开文件,直接获取文件的元信息(属性),比如文件大小、类型、权限、创建时间、修改时间等等。

cpp 复制代码
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);

如果获取成功:返回 0 。如果**获取失败:返回 -1。**这也可以让 stat 系统调用实现一个非常重要的功能:查看文件是否存在。

所以此时,如果 stat 返回 0,说明管道文件已经在那儿了,就直接返回;否则就调用 mkfifo 创建,权限用的就是构造时给的 0666,创建失败直接退出。这样一来,Build() 可以安全地重复调用,不会因为管道已存在而出错。

要注意的是这里IsExists函数内部的else语句分支里面,有一个非常重要的 errno = 0 的语句,之所以这么干,是因为 errno 是一个个全局变量,它不会自己重置。stat 在检查文件不存在时,虽然返回 -1 表示出错,但它同时会把 errno 设置为 ENOENT(通常值为 2)。如果放着不管,这个 errno 的值就会一直留在那里,成为过时的错误信息。

这个类的其他部分(比如 Buildmkfifo 失败后)会依赖 errno 去打印错误原因。如果不清理,万一后续某个操作成功了但没修改 errno,或者调试时看到了这个残留的 errno,就会误以为刚刚发生了错误,造成逻辑干扰。显式清 0 就是在告诉程序:我知道文件不存在,这不是异常,别把这次的"错误"记在账上。

然后是打开管道 Open(int mode)。这里根据传入的模式,决定是以只读(For_Read 对应 O_RDONLY)还是只写(For_Write 对应 O_WRONLY)方式打开管道文件,然后把拿到的文件描述符存到 _fd 里。如果打开失败,打印错误信息并退出。这个设计意味着一个 FIFO 对象只能以一种模式打开,要么读要么写,符合管道半双工的特点。

根据收发信息的部分,SendReceive 分别对应 writereadSend 接收一个 string 引用,直接把字符串的 c_str() 和数据长度 size() 写进 _fd,没有处理返回值(用 (void)n 忽略了),这是简明写法,避免VScode因为没有使用这个 n 而去做未使用的告警。

Receive 中先定义一个 128 字节的临时缓冲区,read 的时候故意留出一个字节,读取成功后手动在末尾补 \0,然后把缓冲区内容赋给传进来的 string 指针,同时返回实际读到的字节数。这里返回值的语义做了区分:读成功返回正数,对端关闭时 read 返回 0 它也返回 0,出错返回 -1,便于外部判断状态。

最后是 Delete(),调用的就是 unlink 删除命名管道文件。同样先检查是否存在,存在才删,不会盲目操作。

这里的 unlink 系统调用是 Linux 系统调用,作用是删除文件(包括命名管道文件) ,功能和命令行的 rm 几乎一样。

cpp 复制代码
#include <unistd.h>

int unlink(const char *pathname);

它的作用是删除文件系统中的文件 (普通文件、命名管道文件都可以删),本质是将文件的硬链接计数减 1,当计数变为 0 时,文件真正被删除。

最后是服务端和客户端的代码展示:

最后我们来看看测试效果:

最终我们实现了客户端通过管道向服务端写入信息的场景。

本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎各位读者的批评和指正。

相关推荐
AOwhisky6 小时前
Python 学习笔记(第十五期)——运维自动化(下·后篇):堡垒机实战——paramiko高阶篇
运维·python·学习·云原生·自动化·运维开发
重生的黑客6 小时前
Linux 进程优先级、切换与调度:从孤儿进程到 O(1) 调度模型
linux·运维·服务器·进程优先级·nice
Urbano7 小时前
夹克与商务单西口袋工序自动化技改研究报告
运维·自动化
骑上单车去旅行8 小时前
MD5校验对比脚本
linux·服务器·windows
平生幻8 小时前
Linux 常用命令
linux
期待着20138 小时前
docker 安装 ,在centos7.9
运维·docker·容器
踏月的造梦星球8 小时前
DM8 DSC 单机双实例部署
运维·开发语言·数据库
MXsoft6189 小时前
平台化是智能运维发展的必然趋势吗?
运维
ShirleyWang0129 小时前
让headlamp控制台能访问
linux·服务器·python·k8s·k3s
liuccn9 小时前
Linux 存储系统:LVM 与直接分区
linux·运维