目录
1、什么是命名管道
1.1 命名管道的创建和使用
1.2、命名管道的工作原理
1.3、命名管道与匿名管道的区别
2. 命名管道的特点及特殊场景
2.1 特点
2.2 四种特殊场景
3.日志类的模拟
3.1可变参数的利用
3.2 time()函数和struct tm类的介绍
3.3 日期类的实现
命名管道是一种在文件系统中存在的特殊文件类型,它允许不同进程通过文件名(即“命名”)来访问和进行通信。与匿名管道相比,命名管道的最大特点是允许没有共同祖先(即没有血缘关系)的进程之间进行通信。这使得命名管道在分布式系统和多进程应用中具有广泛的应用价值。
之所以给管道起名字就是为了不同的进程之间利用管道名,找到管道文件进行进程通信,而不是局限于有亲缘关系的进程。
比起匿名管道,命名管道也是内存级文件,在磁盘上都没有 Data block块,命名管道多了一个inode结构体.
在 Linux 中,我们使用 mkfifo
函数来创建命名管道。该函数的原型如下:
#include
#include
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
这个管道文件也非常特殊:大小为 0,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限,出现在文件系统中,只是单纯挂个名而已
可以直接在命令行中使用命名管道:
当然也可以通过程实现两个独立进程 IPC
思路:创建 服务端 server 和 客户端 client 两个独立的进程,服务端 server 创建命名管道,并以 读 的方式打开管道文件,客户端 client 以 写 的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端(服务端 读端 读取到 0 后也关闭并删除命令管道文件)
注意:
server
创建管道文件unlink 命令管道文件名 //删除管道文件
服务端 Server.cc
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
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 Client
Server:server.cc
g++ -o Server server.cc
Client:client.cc
g++ -o Client client.cc
.PHONY:clean
clean:
rm -rf Server Client
把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,管道文件对 struct file
结构体中的引用计数 ++
,所以对于同一个文件,不同进程打开了,看到的就是同一个 。
stdout
)只有一个吧,是不是所有进程都可以同时进行写入?A
以只读的方式打开,进程 B
以只写的方式打开,那么此时进程 B
就可以向进程 A
写文件,即 IPC
因为命名管道适用于独立的进程间 IPC
,所以无论是读端和写端,进程 A
、进程 B
为其分配的 fd
是一致的,都是 3
fd
不一样所以 命名管道 和 匿名管道 还是有区别的
特性 | 匿名管道(Unnamed Pipe) | 命名管道(Named Pipe) |
---|---|---|
进程关系 | 只能在父子进程间使用 | 可以在任何独立进程之间使用 |
创建方式 | 使用 pipe() 创建 |
使用 mkfifo() 创建,并指定路径 |
生命周期 | 仅在进程期间存在 | 在文件系统中持久存在,直到被删除 |
文件系统 | 不存在文件名 | 存在文件名,并且存储在文件系统中 |
使用方式 | 通过文件描述符在父子进程间通信 | 通过文件路径与文件描述符进行通信 |
描述符 | 由操作系统自动分配给父子进程 | 需要进程手动打开文件并获取文件描述符 |
阻塞行为 | 管道满时写阻塞,管道空时读阻塞 | 同样具有管道满时写阻塞,管道空时读阻塞 |
总结来说,命名管道是比匿名管道更加灵活的进程间通信方式,能够在没有直接关系的进程间传递数据,而匿名管道则适用于具有父子关系的进程。
命名管道与匿名管道在许多方面具有相似性,下面回顾命名管道的一些主要特点及其特殊场景。
命名管道的特点可以总结为以下几点:
半双工通信:管道是单向的数据流通,意味着数据只能一个方向传输,要实现双向通信,通常需要两个管道。
管道生命随进程而终止:命名管道在文件系统中有存在的时间,但它的生命周期由进程的创建和终止来决定,进程关闭时,管道与进程的通信结束。
任意多个进程间通信:命名管道不像匿名管道那样只适用于父子进程,它支持任何两个进程间的通信,只要它们能访问同一个管道文件。
流式数据传输服务:命名管道提供的是数据流式传输,它会将数据作为一个连续的流在进程间传递,而不是一次性传输整个文件内容。
自带同步与互斥机制:管道的设计自动包含了同步与互斥机制,在数据传输时,它保证了写操作和读操作不会同时发生,避免了数据的竞争条件。
命名管道在使用过程中有一些特殊的场景:
管道为空时,读端阻塞,等待写端写入数据:
如果一个进程尝试读取管道,但管道当前没有数据,读操作会阻塞,直到有数据被写入管道。
管道为满时,写端阻塞,等待读端读取数据:
如果管道的缓冲区已满,写端会被阻塞,直到读端读取了部分数据,释放出空间,允许写入新的数据。
进程通信时,关闭读端,操作系统发出 13 号信号 SIGPIPE 终止写端进程:
当进程向管道中写数据,而另一个进程关闭了读端,写端会收到 SIGPIPE
信号,通知写端进程管道已不再可用,从而导致写端进程终止。
进程通信时,关闭写端,读端读取到 0 字节数据,可以借此判断终止读端:
当写端关闭时,读端会读取到 0 字节数据,表示写端已经终止。读端可以利用这个信号来结束其自身的读取过程。
学习了命名管道,我们可以写出一个记录通信过程中日常的日志类。我们将管道的创建封装在一个类这种,这样一来就不用手动创建和删除了。日志类首先要有时间的,下面介绍相关的知识
日志类像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
#include
// 求和函数
int sum(int count, ...) {
va_list args; // 声明一个 va_list 类型的变量
va_start(args, count); // 初始化 va_list
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // 获取下一个参数
}
va_end(args); // 清理 va_list
return total;
}
int main() {
printf("Sum: %d\n", sum(3, 1, 2, 3)); // 输出 6
printf("Sum: %d\n", sum(5, 1, 2, 3, 4, 5)); // 输出 15
return 0;
}
日志需要记录时间的情况,我们介绍time()函数和struct tm结构体:
1. time()
函数
time()
是 C 标准库中的一个函数,主要用于获取当前系统时间。它返回的是自 1970年1月1日00:00:00 UTC 到当前时刻所经过的秒数,通常称为 Unix时间戳。这个时间戳是一个整数,单位是秒。
#include
time_t time(time_t *t);
参数:
t
:一个指向 time_t
类型变量的指针。如果 t
不为 NULL
,则将当前的时间戳存储到 *t
中;如果为 NULL
,则不保存当前时间。
返回值:
返回当前时间的时间戳,即从 1970 年 1 月 1 日到当前时间所经过的秒数。如果出现错误,返回 (time_t)(-1)
。
2. struct tm
结构体
struct tm
是一个结构体,用于表示某一时刻的日期和时间。它包含了年、月、日、小时、分钟、秒等信息。
定义(在
头文件中):
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年对应0
int 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
struct tm *localtime(const time_t *timep);
参数:
timep
:指向 time_t
类型的指针,表示自 1970 年 1 月 1 日以来的秒数(即 Unix 时间戳)。
返回值:
返回一个指向 struct tm
的指针。struct tm
中包含了本地时间的各个部分(如年、月、日、小时、分钟、秒等)。
返回的结构体是静态的,因此每次调用 localtime()
都会覆盖上次的结果,所以不应该在多个地方同时使用它返回的指针。
#include
#include
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
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"<
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
#include
#include
#include
#include
#include
#include
#include
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<
comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 client
server:server.cc
g++ -o server server.cc -std=c++11
client:client.cc
g++ -o client client.cc -std=c++11
.PHONY:clean
clean:
rm -rf client server myfifo
这样一来就是实现了在通信之间实现日志类。