当前位置: 首页 > ai >正文

Linux进程间通信--命名管道

目录

1、什么是命名管道

1.1 命名管道的创建和使用

 1.2、命名管道的工作原理

1.3、命名管道与匿名管道的区别 

2. 命名管道的特点及特殊场景

2.1 特点

2.2 四种特殊场景

3.日志类的模拟 

3.1可变参数的利用

3.2 time()函数和struct tm类的介绍

 3.3 日期类的实现


1、什么是命名管道

命名管道是一种在文件系统中存在的特殊文件类型,它允许不同进程通过文件名(即“命名”)来访问和进行通信。与匿名管道相比命名管道的最大特点是允许没有共同祖先(即没有血缘关系)的进程之间进行通信这使得命名管道在分布式系统和多进程应用中具有广泛的应用价值。

 之所以给管道起名字就是为了不同的进程之间利用管道名,找到管道文件进行进程通信,而不是局限于有亲缘关系的进程。

 比起匿名管道,命名管道也是内存级文件,在磁盘上都没有 Data block块,命名管道多了一个inode结构体.

1.1 命名管道的创建和使用

在 Linux 中,我们使用 mkfifo 函数来创建命名管道。该函数的原型如下:

#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

关于 mkfifo 函数

组成部分含义
返回值 int创建成功返回 0,失败返回 -1
参数1 const char *pathname创建命名管道文件时的路径+名字
参数2 mode_t mode创建命令管道文件时的权限

 对于参数1,既可以传递绝对路径 /home/xxx/namePipeCode/fifo,也可以传递相对路径 ./fifo,当然绝对路径更灵活,但也更长

对于参数2,mode_t 其实就是对 unsigned int 的封装,等价于 uint32_t,而 mode 就是创建命名管道时的初始权限,实际权限需要经过 umask 掩码计算

不难发现,mkfifo 和 mkdir 非常像,其实 mkfifo 可以直接在命令行中运行

创建一个名为 fifo 的命名管道文件

mkfifo fifo

 成功解锁了一种新的特殊类型文件:p 管道文件

 这个管道文件也非常特殊:大小为 0,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限,出现在文件系统中,只是单纯挂个名而已

可以直接在命令行中使用命名管道:

  • echo 可以进行数据写入,可以重定向至 fifo
  • cat 可以进行数据读取,同样也可以重定向于 fifo
  • 打开两个终端窗口(两个进程),即可进行通信

 当然也可以通过程实现两个独立进程 IPC

思路:创建 服务端 server 和 客户端 client 两个独立的进程,服务端 server 创建命名管道,并以的方式打开管道文件,客户端 client 以 的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端(服务端 读端 读取到 0 后也关闭并删除命令管道文件)

注意:

  • 当管道文件不存在时,文件会打开失败,因此为了确保正常通信,需要先运行服务端 server 创建管道文件
  • 服务端启动后,因为是读端,所以会阻塞等待 客户端(写端)写入数据
  • 通信结束后,需要服务端主动删除管道文件
unlink 命令管道文件名	//删除管道文件

服务端 Server.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>using namespace std;
int main()
{int n = mkfifo("pipe", 0664); // 创建管道if (n == -1){perror("mkfifo error");exit(1);}int fd = open("pipe", O_RDONLY | O_CREAT); // 打开管道文件,以读if (fd < 0){perror("open pipe error");exit(1);}cout << "读端打开成功" << endl;//读管道只有等写端打开才会,打开char buffer[1024] = {0};int m = read(fd, buffer, sizeof(buffer));if (m < 0){perror("read err");exit(1);}else if (m == 0){return 0;}else{cout << buffer << endl;}unlink("pipe");return 0;
}

客户端 client.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{int fd=open("pipe",O_WRONLY|O_CREAT);const char* s="hello,world";write(fd,s,sizeof(s));return 0;
}

makefile

.PHONY:all
all:Server ClientServer:server.ccg++ -o Server server.cc
Client:client.ccg++ -o Client client.cc.PHONY:clean
clean:rm -rf Server Client
 1.2、命名管道的工作原理

把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,管道文件对 struct file 结构体中的引用计数 ++,所以对于同一个文件,不同进程打开了,看到的就是同一个 。

  • 具体例子:显示器文件(stdout)只有一个吧,是不是所有进程都可以同时进行写入?
  • 同理,命名管道文件也是如此,先创建出文件,在文件系统中挂个名,然后让独立的进程以不同的方式打开同一个命名管道文件,比如进程 A 以只读的方式打开,进程 B 以只写的方式打开,那么此时进程 B 就可以向进程 A 写文件,即 IPC

 

因为命名管道适用于独立的进程间 IPC,所以无论是读端和写端,进程 A、进程 B 为其分配的 fd 是一致的,都是 3

  • 如果是匿名管道,因为是依靠继承才看到同一文件的,所以读端和写端 fd 不一样

所以 命名管道 和 匿名管道 还是有区别的

1.3、命名管道与匿名管道的区别 
特性匿名管道(Unnamed Pipe)命名管道(Named Pipe)
进程关系只能在父子进程间使用可以在任何独立进程之间使用
创建方式使用 pipe() 创建使用 mkfifo() 创建,并指定路径
生命周期仅在进程期间存在在文件系统中持久存在,直到被删除
文件系统不存在文件名存在文件名,并且存储在文件系统中
使用方式通过文件描述符在父子进程间通信通过文件路径与文件描述符进行通信
描述符由操作系统自动分配给父子进程需要进程手动打开文件并获取文件描述符
阻塞行为管道满时写阻塞,管道空时读阻塞同样具有管道满时写阻塞,管道空时读阻塞

总结来说,命名管道是比匿名管道更加灵活的进程间通信方式,能够在没有直接关系的进程间传递数据,而匿名管道则适用于具有父子关系的进程。

2. 命名管道的特点及特殊场景

命名管道与匿名管道在许多方面具有相似性,下面回顾命名管道的一些主要特点及其特殊场景。

2.1 特点

命名管道的特点可以总结为以下几点:

  • 半双工通信:管道是单向的数据流通,意味着数据只能一个方向传输,要实现双向通信,通常需要两个管道。

  • 管道生命随进程而终止:命名管道在文件系统中有存在的时间,但它的生命周期由进程的创建和终止来决定,进程关闭时,管道与进程的通信结束。

  • 任意多个进程间通信:命名管道不像匿名管道那样只适用于父子进程,它支持任何两个进程间的通信,只要它们能访问同一个管道文件。

  • 流式数据传输服务:命名管道提供的是数据流式传输,它会将数据作为一个连续的流在进程间传递,而不是一次性传输整个文件内容。

  • 自带同步与互斥机制:管道的设计自动包含了同步与互斥机制,在数据传输时,它保证了写操作和读操作不会同时发生,避免了数据的竞争条件。

2.2 四种特殊场景

命名管道在使用过程中有一些特殊的场景:

  1. 管道为空时,读端阻塞,等待写端写入数据

    • 如果一个进程尝试读取管道,但管道当前没有数据,读操作会阻塞,直到有数据被写入管道。

  2. 管道为满时,写端阻塞,等待读端读取数据

    • 如果管道的缓冲区已满,写端会被阻塞,直到读端读取了部分数据,释放出空间,允许写入新的数据。

  3. 进程通信时,关闭读端,操作系统发出 13 号信号 SIGPIPE 终止写端进程

    • 当进程向管道中写数据,而另一个进程关闭了读端,写端会收到 SIGPIPE 信号,通知写端进程管道已不再可用,从而导致写端进程终止。

  4. 进程通信时,关闭写端,读端读取到 0 字节数据,可以借此判断终止读端

    • 当写端关闭时,读端会读取到 0 字节数据,表示写端已经终止。读端可以利用这个信号来结束其自身的读取过程。

3.日志类的模拟 

学习了命名管道,我们可以写出一个记录通信过程中日常的日志类。我们将管道的创建封装在一个类这种,这样一来就不用手动创建和删除了。日志类首先要有时间的,下面介绍相关的知识

3.1可变参数的利用

日志类像printf()函数一样,有可变参数部分,可以接受不同个数的参数。要想模拟出同样的效果,我们也要了解可变参数的解析过程。

在 C 语言中,处理可变参数的主要宏定义都在 stdarg.h 头文件中。这里介绍几种常见的宏,它们用于处理传入的可变参数。

1. va_list 类型

va_list 实际上是一个指向栈帧中可变参数部分的指针类型,它用于遍历函数中的可变参数。

作用:

va_list 用于存储访问可变参数所需的信息。在 C 语言中,参数的数量和类型通常是在编译时无法知道的,因此我们使用 va_list 来动态地访问这些参数。它需要配合使用

2. va_start

va_start 宏用于初始化 va_list 类型的变量,这个变量将用来访问传入的可变参数。

语法

va_start(va_list ap, last_fixed_arg);
  • apva_list 类型的变量,用来访问可变参数。

  • last_fixed_arg指向可变参数前面的一个固定参数。

作用:初始化 va_list,并将其指向第一个可变参数。因为函数的参数是从右向左依次入栈的,所以利用  last_fixed_arg,将其先取地址,取地址后加1.就可以得到可变参数的第一个变量了。

3. va_arg

va_arg 用来获取下一个可变参数,并指定它的类型。

语法

type va_arg(va_list ap, type);
  • ap:指向可变参数的 va_list 类型变量。

  • type:你期望的参数类型。

作用:返回 ap 中的下一个可变参数,并将 ap 指向下一个参数。根据类型,对指针强转为对应类型就可以得到参数,再配上while循环就可以将可变参数解析完毕。

4.va_end

va_end 宏用于结束对可变参数的访问。

语法

va_end(va_list ap);
  • apva_list 类型的变量。

作用:清理资源,结束访问。

 示例:计算多个数字的和

#include <stdio.h>
#include <stdarg.h>// 求和函数
int sum(int count, ...) {va_list args;          // 声明一个 va_list 类型的变量va_start(args, count); // 初始化 va_listint total = 0;for (int i = 0; i < count; i++) {total += va_arg(args, int); // 获取下一个参数}va_end(args); // 清理 va_listreturn total;
}int main() {printf("Sum: %d\n", sum(3, 1, 2, 3));  // 输出 6printf("Sum: %d\n", sum(5, 1, 2, 3, 4, 5));  // 输出 15return 0;
}
3.2 time()函数和struct tm类的介绍

日志需要记录时间的情况,我们介绍time()函数和struct tm结构体:

1. time() 函数

time() 是 C 标准库中的一个函数,主要用于获取当前系统时间。它返回的是自 1970年1月1日00:00:00 UTC 到当前时刻所经过的秒数,通常称为 Unix时间戳。这个时间戳是一个整数,单位是秒。

#include <time.h>time_t time(time_t *t);
  • 参数

    • t:一个指向 time_t 类型变量的指针。如果 t 不为 NULL,则将当前的时间戳存储到 *t 中;如果为 NULL,则不保存当前时间。

  • 返回值

    • 返回当前时间的时间戳,即从 1970 年 1 月 1 日到当前时间所经过的秒数。如果出现错误,返回 (time_t)(-1)

2. struct tm 结构体

struct tm 是一个结构体,用于表示某一时刻的日期和时间。它包含了年、月、日、小时、分钟、秒等信息。

定义(在 <time.h> 头文件中):

struct tm {int tm_sec;   // 秒 (0-59)int tm_min;   // 分钟 (0-59)int tm_hour;  // 小时 (0-23)int tm_mday;  // 一个月中的日期 (1-31)int tm_mon;   // 月份 (0-11),0表示1月,11表示12月int tm_year;  // 从1900年起的年份,1900年对应0int tm_wday;  // 一周中的天 (0-6),0表示星期日int tm_yday;  // 一年中的天数 (0-365),0表示1月1日int tm_isdst; // 夏令时标志(>0表示夏令时,0表示非夏令时,<0表示无法确定)
};

 3.localtime() 函数

localtime() 是 C 语言中的一个标准库函数,用于将 time_t 类型的时间戳(即自 1970 年 1 月 1 日以来的秒数)转换为本地时间。返回的时间是一个 struct tm 类型的结构体,它包含了具体的时间信息,如年、月、日、时、分、秒等。

#include <time.h>struct tm *localtime(const time_t *timep);
  • 参数

    • timep:指向 time_t 类型的指针,表示自 1970 年 1 月 1 日以来的秒数(即 Unix 时间戳)。

  • 返回值

    • 返回一个指向 struct tm 的指针。struct tm 中包含了本地时间的各个部分(如年、月、日、小时、分钟、秒等)。

    • 返回的结构体是静态的,因此每次调用 localtime() 都会覆盖上次的结果,所以不应该在多个地方同时使用它返回的指针。

#include <stdio.h>
#include <time.h>int main() {time_t current_time;struct tm *tm_info;// 获取当前时间戳current_time = time(NULL);// 将时间戳转换为本地时间tm_info = localtime(&current_time);// 输出格式化的本地时间printf("Current local time: %04d-%02d-%02d %02d:%02d:%02d\n",tm_info->tm_year + 1900,  // 年份(1900年后)tm_info->tm_mon + 1,      // 月份(1-12)tm_info->tm_mday,         // 日(1-31)tm_info->tm_hour,         // 时(0-23)tm_info->tm_min,          // 分(0-59)tm_info->tm_sec);         // 秒(0-59)return 0;
}
Current local time: 2025-07-11 13:45:30
 3.3 日期类的实现

Client客户端:

#include "log.hpp"
#include "comm.hpp"int main()
{int fd=open(FIFO_FILE,O_WRONLY);//打开管道文件if(fd<0){perror("open");exit(FIFO_OPEN_ERR);}cout<<"client open file done"<<endl;string s;while(true){cout<<"client please enter@ ";getline(cin,s);int id=write(fd,s.c_str(),s.size());}close(fd);return 0;
}

Server服务器

 

#include "log.hpp"
#include "comm.hpp"int main()
{Init init;Log log;int fd = open(FIFO_FILE, O_RDONLY);if (fd < 0){perror("open");exit(FIFO_OPEN_ERR);}log("Debug", "server open file done, error string: %s, error code: %d", strerror(errno), errno);log("Info", "server open file done, error string: %s, error code: %d", strerror(errno), errno);log("Warning", "server open file done, error string: %s, error code: %d", strerror(errno), errno);log("Fatal", "server open file done, error string: %s, error code: %d", strerror(errno), errno);while (true){char buffer[1024];int n = read(fd, buffer, sizeof(buffer));if (n > 0){buffer[n] = '\0';cout << "client say@" << buffer << endl;}else if(n==0){log("Debug", "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);break;}else{log("Fatal", "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);}}return 0;
}

log.hpp

 

#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>using namespace std;#define SIZE 1024
#define LogFile "log.txt" //日志文件的名字class Log
{
public:Log(){printmethod="Screen";//默认打印在屏幕上面path="./mylog/";//默认路径}void Eable(string method){printmethod=method;}void operator()(const string method,const char *format,...){time_t t=time(nullptr);struct tm*ctime=localtime(&t);//返回一个指针char leftbuffer[SIZE];snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",method.c_str(),ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);va_list s;va_start(s,format);char rightbuffer[SIZE];vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);//解析va_arg();的作用va_end(s);char logtxt[SIZE*2];snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);Printlog(method,logtxt);}void Printlog(const string method,string logtxt){if(method=="Screen"){cout<<logtxt<<endl;}else{string s=path+method+".txt";//./mylog/init.int fd=open(s.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if(fd<0) return ;write(fd,logtxt.c_str(),logtxt.size());close(fd);}}private:string printmethod;string path;
};

comm.hpp

#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>using namespace std;#define FIFO_FILE "./myfifo"
#define MODE 0664 enum 
{FIFO_OPEN_ERR=1,FIFO_DELETE_ERR,FIFO_CREAT_ERR
};//像类一样设计class Init
{
public:Init(){int n=mkfifo(FIFO_FILE,MODE);if(n==-1){perror("open fifo");exit(FIFO_CREAT_ERR);}}~Init(){int m=unlink(FIFO_FILE);if(m==-1){perror("delete fifo");exit(FIFO_DELETE_ERR);}}
};

 makfile

.PHONY:all
all:server clientserver:server.cc	g++ -o server server.cc -std=c++11
client:client.ccg++ -o client client.cc  -std=c++11.PHONY:clean
clean:rm -rf client server myfifo

这样一来就是实现了在通信之间实现日志类。

http://www.xdnf.cn/news/15080.html

相关文章:

  • 深度学习入门教程(三)- 线性代数教程
  • react打包发到线上报错Minified React error #130
  • 如何快速掌握WeNet:从零到一的端到端语音识别学习指南
  • spring-ai RAG(Retrieval-Augmented Generation)
  • 上位机知识篇---网络通信端口
  • 线程邮箱(线程间通信的异步缓存机制)
  • OBB旋转框检测配置与训练全流程(基于 DOTA8 数据集)
  • 云原生周刊:镜像兼容性
  • 十、MyBatis的逆向工程
  • 美颜SDK贴纸引擎设计指南:动画、识别与适配的实现逻辑
  • 华为数据通信网络基础
  • 香港站群服务器8C/4C/2C/1C有什么区别
  • 使用you-get命令下载视频/音频/图像
  • 北京-4年功能测试2年空窗-报培训班学测开-第四十八天
  • 【世纪龙科技】几何G6新能源汽车结构原理教学软件
  • 60 美元玩转 Li-Fi —— 开源 OpenVLC 平台入门(附 BeagleBone Black 驱动简单解析)
  • 飞算Java AI:专为 Java 开发者打造的智能开发引擎
  • uniapp制作一个个人页面
  • C++11堆操作深度解析:std::is_heap与std::is_heap_until原理解析与实践
  • [Reverse1] Tales of the Arrow
  • intellij idea的重命名shift+f6不生效(快捷键被微软输入法占用)
  • 【数据库基础 1】MySQL环境部署及基本操作
  • TypeScript---泛型
  • (7)机器学习小白入门 YOLOv:机器学习模型训练详解
  • map数据结构在Golang中是无序的,并且键值对的查找效率较高的原因
  • Linux 命令:tail
  • 如何查看自己本地的公网IP地址?内网环境网络如何开通服务器公网ip提供互联网访问?
  • Lecture #20:Database Logging
  • 深度解析 DApp 开发:从技术架构到商业落地的全链路解决
  • Jenkins 分布式和并发构建