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 四种特殊场景
命名管道在使用过程中有一些特殊的场景:
-
管道为空时,读端阻塞,等待写端写入数据:
-
如果一个进程尝试读取管道,但管道当前没有数据,读操作会阻塞,直到有数据被写入管道。
-
-
管道为满时,写端阻塞,等待读端读取数据:
-
如果管道的缓冲区已满,写端会被阻塞,直到读端读取了部分数据,释放出空间,允许写入新的数据。
-
-
进程通信时,关闭读端,操作系统发出 13 号信号 SIGPIPE 终止写端进程:
-
当进程向管道中写数据,而另一个进程关闭了读端,写端会收到
SIGPIPE
信号,通知写端进程管道已不再可用,从而导致写端进程终止。
-
-
进程通信时,关闭写端,读端读取到 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);
-
ap
:va_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);
-
ap
:va_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(¤t_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
这样一来就是实现了在通信之间实现日志类。