【Linux】深度探秘命名管道:Linux 进程通信的无声桥梁
1.什么是命名管道
在 Unix/Linux 系统中,管道(Pipe)是一种重要的进程间通信(IPC,Inter-Process Communication)机制。除了前面介绍的匿名管道(Anonymous Pipe),系统还提供了命名管道(Named Pipe),通常称为 FIFO(First In, First Out)。命名管道通过一个在文件系统中存在的路径名来标识,使得不相关的进程之间也能通过它进行通信。
命名管道是一种特殊类型的文件,它在文件系统中有一个明确的名称,可以被多个进程打开和访问。与匿名管道不同,命名管道不局限于具有亲缘关系的进程(如父子进程),任何具有访问权限的进程都可以通过命名管道进行通信。
我们可以把命名管道看成”挂名“的匿名管道,把匿名管道加入文件系统中,但仅仅是挂个名而已,目的是为了人其他进程也能看到也看到这个文件(文件系统中的文件可以被所有的进程看到)
注意:
命名管道虽然能在文件系统被看到,但是它是没有Data block的,也就它不会存储在磁盘中,是一个内存文件。所以命名管道这个特殊文件大小为0
1.2 命名管道的特点
- 持久性:命名管道存在于文件系统中,直到被删除。即使创建它的进程退出,命名管道仍然存在,等待其他进程的连接。
- 双向通信:虽然管道本质上是半双工的(单向),但通过两个命名管道可以实现全双工通信。
- 跨进程通信:命名管道允许不相关的进程之间进行通信,只需知道管道的路径即可。
- 无需父子关系:任何进程都可以打开命名管道进行读写,不需要继承关系。
2. 创建命名管道
创建命名管道有两种方法:
- 直接在命令行上创建。
- 在程序中创建。
在命令行创建:
mkfifo mypipe
效果如下:
ubuntu@VM-20-9-ubuntu:~/pipeTest/namePipe$ mkfifo mypipe
ubuntu@VM-20-9-ubuntu:~/pipeTest/namePipe$ ls -l
total 0
prw-rw-r-- 1 ubuntu ubuntu 0 Nov 15 19:56 filename
prw-rw-r-- 1 ubuntu ubuntu 0 Nov 15 20:21 mypipe
2.1 在C程序中创建命名管道
为了在C程序中创建命名管道,我们需要用到的函数也是mkfifo
。
mkfifo:
头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
格式:
int mkfifo(const char *pathname, mode_t mode);
参数介绍:
pathname
- 命名管道的路径名。
- 在文件系统中创建一个文件,代表命名管道。
mode
- 管道文件的权限(类似于文件的权限),是一个
mode_t
类型值。 - 常见值:
- 例如
0666
,允许所有用户读写。
- 例如
mode
会受到 进程的umask
设置 的影响。
为了防止mode的设置被umask影响,可以事先将umsak设置为0,umask(0)
.
返回值:
成功:0
失败:-1,并设置error
下面我来实现一个进程向另一个进程发送信息:
客户端:client.cc
- 管道文件的权限(类似于文件的权限),是一个
//客户端向服务端发送消息
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <fcntl.h>
#include "common.hpp"
using namespace std;
int main()
{
int mf = mkfifo(pipePath,md);
if(mf<0)
{
perror("mkfifo");
exit(1);
}
//打开管道
int fd = open(pipePath,O_WRONLY);//以写方式打开
if(fd < 0)
{
perror("open");
exit(1);
}
//char data[SIZE];
string data;
while(true)
{
//开始写入数据
cout<<"cilent message# ";
getline(cin,data);
if(data == "exit")
break;
ssize_t n = write(fd,data.c_str(),data.size());
if(n<0)
{
perror("write");
exit(1);
}
}
//退出客户端
cout<<"exit"<<endl;
close(fd);
return 0;
}
服务端:server.cc
//服务端接受客户端的信息
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <fcntl.h>
#include "common.hpp"
using namespace std;
int main()
{
//打开管道
int fd = open(pipePath,O_RDONLY);//以读的方法
if(fd < 0)
{
perror("open");
exit(1);
}
char data[SIZE];
while(true)
{
int n = read(fd,data,sizeof(data)-1);
if(n<0)
{
perror("read");
exit(1);
}
else if(n == 0)
{
//写端关闭
cout<<"写端关闭"<<endl;
break;
}
data[n] = 0;
cout<<"client say:"<<data<<endl;
}
close(fd);
if (unlink(pipePath) == -1) {
perror("unlink failed");
return 1;
}//关闭管道
return 0;
}
公共文件:common.hpp
#pragma once
#define SIZE 1024
//存储管道的路径+名字
const char* pipePath = "./myPipe";
//也可以用绝对路径,当然相对路径更简单
//设置mask
mode_t md = 0666;
3. 命名管道的工作原理
再次回到文件系统:当重复多次打开一个文件时,并不会费力的打开多次,而是在第一次的基础上对struct_file
结构体中的引用计数自增1,所以对于同一个文件,不同的进程打开了,看到的就是同一个。
这也是管道实现的本质:让不同的进程看到同一块空间。
因为命名管道适用于独立的进程IPC
,所以无论是读端还是写端,进程A
,进程B
为其分配的文件描述符都是3。
我们知道如果是匿名管道,因为是依靠继承才看到同一文件的,所以读写端的fd
是不一样的。
3.1 命名管道和匿名管道的区别与联系
3.1.1 命名管道与匿名管道的区别
- 匿名管道只能用于具有血缘关系的进程间通信;而命名管道就不一样了,无论有没有血缘关系都可以。
- 匿名管道是通过
pipe
函数创建出来了;而命名管道需要先通过mkfifo
函数创建,然后再通过open
打开使用。 - 当出现多条匿名管道时,可能会出现写端
fd
被重复继承的情况;而命名管道就不会出现这种情况。
3.1.2 命名管道和匿名管道的联系
- 两个都是属于管道,都是操作系统中最古老的进程通信方式,都自带有同步和异步机制,提供的都是流式数据传输
与匿名管道相同的也有在这4个场景下的处理
- 管道为空时,读端堵塞,等待写端写入数据。
- 管道已满时,写端堵塞,等待读端读取数据。
- 进程通信时,关闭读端,OS发出13号信息
SIGPIPE
终止写端进程。 - 进程通信时,关闭写端,读端读取到0字节数据,可以凭借这个特征来终止进程。
4.命名管道的简单应用
我们可以利用命名管道来实现一些简单的功能,加强我们对命名管道的理解。现在我们打算实现的文件拷贝小程序。
我们可以利用命名管道实现不同进程间IPC
,也就是一个进程读取文件中的内容然后写进管道当中,然后另一个进程在通过管道将数据读出保存到新的文件,如此一来就是实现了一个进程的文件拷贝功能。
这也是在网络上下载应用的方式,因为下载应用的本质就是下载文件,我们将服务器看作写端,自己的电脑看作读端,那么下载这个动作的本质就是IPC
,不过是在网络层面实现的。
公共区域common.hpp
:
#pragma once
#include <unistd.h>
#include <sys/types.h>
#include <cstdlib>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#define SIZE 1024
//打开拷贝的文件路径
const char* filePath = "./file.txt";
//要打开的管道路径
const char* pipePath = "./pipe";
//mask码设置
mode_t md = 0666;
服务端server.cc
/**
* 服务端通过命名管道将本地文件拷贝到客户端
* 先打开命名管道,再打开需要被拷贝的文件,将文件通过命名管道发送给另一个程序
*
*/
#include "common.hpp"
int main()
{
//创建命名管道
if(mkfifo(pipePath,md) == -1)
{
perror("mkfifo");
exit(1);
}
//打开管道
int fd = open(pipePath,O_WRONLY);
if(fd == -1)
{
perror("open");
exit(1);
}
//打开需要被拷贝的文件
FILE* fp = fopen(filePath,"r");
if(fp == nullptr)
{
perror("fread");
exit(1);
}
//将文件传给管道
char buf[SIZE];
size_t bytes = 0;
while(true)
{
size_t n = fread(buf,1,sizeof(buf),fp);
if(n>0)
{
//写入管道
ssize_t sst = write(fd,buf,n);
if(sst == -1)
{
perror("write");
close(fd);
fclose(fp);
exit(1);
}
bytes+=n;
}
// 检查是否到达文件末尾或出错
if (n < sizeof(buf)) {
if (feof(fp)) {
break; // 文件读取完毕
}
if (ferror(fp)) {
perror("fread");
fclose(fp);
close(fd);
exit(1);
}
}
}
std::cout<<"传递字节数"<<bytes<<std::endl;
fclose(fp);
close(fd);
unlink(pipePath);
return 0;
}
客户端client.cc
//客户端接受服务端的文件
/**
* 客户端接受服务端的文件
* 打开命名管道,开始读取服务端传递给客户端的信息
*/
#include "common.hpp"
int main()
{
//打开管道,读取管道数据
int fd = open(pipePath,O_RDONLY);
if(fd == -1)
{
perror("open");
exit(1);
}
//将读取的数据打印到显示屏
char buf[SIZE];
ssize_t n = read(fd,buf,sizeof(buf)-1);
if(n == -1)
{
perror("read");
exit(1);
}
std::cout<<"文件内容:\n"<<buf<<"读取字节数:"<<n<<std::endl;
close(fd);
return 0;
}
此时服务端是写端,客户端是读端,实现的是下载服务;当服务端是读端,客户端是写端是,实现的就是上传服务,搞两条管道就能实现模拟简单的数据双向传输服务。
注意:创建管道文件后,无论先启动读端还是启动写端,都要堵塞式的等待另一方进行交互。
5.总结
作为匿名管道的兄弟,命名管道具备匿名管道的大部分特性,使用方法也基本一致,不过二者在创建和打开方式上各有不同:匿名管道简单,但只能用于具有血缘关系进程间通信,命名管道虽麻烦些,但适用于所有进程间通信场景。
- 点赞
- 收藏
- 关注作者
评论(0)