【Linux】进程间通信——命名管道


文章目录
- 1. 命名管道介绍
- 2. 简单使用命名管道
- 创建命名管道
- 写入数据到命名管道
- 从命名管道读取数据
- 示例
- 删除命名管道
- 3. 命名管道原理
- 4. 命名管道代码使用
- 命名管道的打开规则
- 5. 结语
1. 命名管道介绍
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用命名管道来做这项工作。
在Linux系统中,命名管道(也称为FIFO
,First In First Out
)是一种特殊的文件类型,它允许进程间进行通信。与匿名管道不同,命名管道存在于文件系统中,并且可以被任何有适当权限的进程访问。命名管道提供了一种方法,使得不相关的进程能够通过预先定义好的路径来交换数据。
2. 简单使用命名管道
创建命名管道
可以通过mkfifo
命令或mknod
命令来创建命名管道。使用mkfifo
更为常见和简单:
mkfifo /path/to/your/fifo
这里,/path/to/your/fifo
是你要创建的命名管道的路径。创建后,这个路径将作为一个特殊类型的文件存在,其类型为p
(pipe);如下图所示,在当前路径下创建名为mypipe
的管道文件。
写入数据到命名管道
一个进程可以打开命名管道并写入数据。例如,使用echo
命令向命名管道写入文本:
echo "Hello, FIFO!" > /path/to/your/fifo
需要注意的是,如果此时没有其他进程正在读取该命名管道,则上述命令将会阻塞,直到有读者出现。
从命名管道读取数据
另一个进程可以从命名管道中读取数据。这通常也是通过标准输入输出重定向完成的:
cat < /path/to/your/fifo
同样地,如果此时没有进程向命名管道写入数据,那么执行读操作的命令也会处于等待状态。
示例
假设我们想要创建一个简单的场景,其中一个shell会话发送消息给另一个shell会话。首先,在第一个终端窗口创建命名管道:
mkfifo myfifo
然后,在第二个终端窗口启动读取过程:
cat < myfifo
回到第一个终端窗口,向命名管道写入一些内容:
echo "This is a test message" > myfifo
这时,第二个终端应该显示了刚刚写入的消息:“This is a test message”。
删除命名管道
一旦不再需要命名管道,可以直接使用rm
命令删除它:
rm /path/to/your/fifo
记住,命名管道必须在没有任何进程打开的情况下才能被成功删除。
命名管道非常适合用于那些需要跨多个会话或用户之间的简单IPC(进程间通信)的情况。对于更复杂的需求,可能需要考虑其他的IPC机制如消息队列、共享内存等。
3. 命名管道原理
与匿名管道类似,命名管道也是操作系统提供的可以共享的资源,不同的是命名管道是一个特殊的文件,记录在磁盘上也有自己的文件描述符; 但是它与普通文件又有不同,命名管道文件的内容不需要刷新到磁盘中,因为它仅需要进行通信即可,不需要耗费时间空间去将内容保存在磁盘中,所以它的文件大小一直是0。
此外由于命名管道是保存在磁盘上的文件,可以通过路径+文件名
标识唯一性,使得它可以不同于匿名管道,在没有亲缘关系的进程之间也能进行通信。
4. 命名管道代码使用
- 命名管道也可以从程序⾥创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
与创建文件类似,其中,filename是需要创建的命名管道的路径已以及文件名,mode 是管道的权限,一般设置为0600,表示只有拥有者可读可写。如果创建成功,返回0;否则返回-1。
- 删除命名管道相关函数有:
int unlink(const char *filename);
删除一个命名管道。filename参数指定管道的路径名。如果删除成功,返回0;否则返回-1。
- 命名管道创建成功就可以当作文件使用:
【打开文件】:
int open(const char *pathname, int flags, mode_t mode);
- pathname: 文件路径。
- flags: 打开标志(如 O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND 等)。
- mode: 文件权限(当文件被创建时)。
- 返回值: 成功时返回文件描述符,失败时返回 -1 并设置 errno。
【关闭文件】
int close(int fd);
- fd: 文件描述符。
- 返回值: 成功时返回 0,失败时返回 -1 并设置 errno。
【文件读写】
ssize_t read(int fd, void *buf, size_t count);
-
从文件描述符读取数据。
-
fd: 文件描述符。
-
buf: 缓冲区,用于存储读取的数据。
-
count: 要读取的字节数。
-
返回值: 成功时返回读取的字节数,到达文件末尾时返回 0,失败时返回 -1 并设置 errno。
ssize_t write(int fd, const void *buf, size_t count);
-
向文件描述符写入数据。
-
fd: 文件描述符。
-
buf: 缓冲区,包含要写入的数据。
-
count: 要写入的字节数。
-
返回值: 成功时返回写入的字节数,失败时返回 -1 并设置 errno。
基于上述函数我们就可以通过创建命名管道来实现不同进程间通信:
- 首先我们需要创建命名管道:
//1.创建命名管道
class InitFifo{
public:
InitFifo()
{
umask(0);
if(mkfifo(gpipeFile.c_str(),gmode)<0)//gpipeFile和gmode是定义的全局变量const std::string gpipeFile = "./fifo";const mode_t gmode = 0600;
std::cout<<"创建命名管道失败..."<<std::endl;
else
std::cout<<"创建命名管道成功..."<<std::endl;
}
~InitFifo()
{
if(unlink(gpipeFile.c_str())<0)
std::cout<<"删除命名管道失败..."<<std::endl;
else
std::cout<<"删除命名管道成功..."<<std::endl;
}
};
- 创建命名管道完成后,需要往命名管道写入内容:
先将需要使用的函数封装在一个Client类中
//2.使用命名管道发送信息
class Client{
public:
Client():_fd(-1)
{}
//打开命名管道文件
bool OpenFifoForWrite()
{
_fd = ::open(gpipeFile.c_str(), O_WRONLY);//以只写方式打开
if(_fd < 0)
{
std::cout<<"打开命名管道文件失败..."<<std::endl;
return false;
}
return true;
}
//写入命名管道文件内容
bool WriteFifo(const std::string& s)
{
if(_fd > 0)
{
ssize_t n = ::write(_fd,s.c_str(),s.size());
if(n < 0)
{
std::cout<<"写入命名管道失败..."<<std::endl;
return false;
}
return true;
}
std::cout<<"命名管道不存在..."<<std::endl;
return false;
}
//关闭文件
bool CloseFifo()
{
if(_fd > 0)
{
if(::close(_fd)< 0)
{
std::cout<<"关闭命名管道文件失败..."<<std::endl;
return false;
}
return true;
}
std::cout<<"文件不存在..."<<std::endl;
return false;
}
private:
int _fd;
};
使用Client类封装好相关函数后,就可以定义对象来使用
int main()
{
Client c;//定义Client对象
c.OpenFifoForWrite();//打开文件
//写入内容
std:: string message;
while(true)
{
std::cout<<"Please Enter# ";
std::getline(std::cin,message);//从标准输入中读取内容发送给命名管道
c.WriteFifo(message);
}
c.CloseFifo();//关闭文件
return 0;
}
- 从命名管道读取内容:
先将需要使用的函数封装在一个Server类中
//3.使用命名管道接收信息
class Server{
public:
Server():_fd(-1)
{ }
//打开命名管道文件
bool OpenFifoForRead()
{
_fd = ::open(gpipeFile.c_str(), O_RDONLY);//以只读方式打开
if(_fd < 0)
{
std::cout<<"打开命名管道文件失败..."<<std::endl;
return false;
}
return true;
}
//读取命名管道文件内容
std::string ReadFifo()
{
if(_fd > 0)
{
char buffer[gsize];
size_t n = ::read(_fd,&buffer,gsize-1);
if(n > 0)
{
buffer[n] = 0;
return buffer;
}
else{
std::cout<<"没有内容读取..."<<std::endl;
}
}
return "";
}
//关闭文件
bool CloseFifo()
{
if(_fd > 0)
{
if(::close(_fd)< 0)
{
std::cout<<"关闭命名管道文件失败..."<<std::endl;
return false;
}
return true;
}
std::cout<<"文件不存在..."<<std::endl;
return false;
}
private:
int _fd;
};
封装完成后,就可以定义Server对象使用
int main()
{
InitFifo i;//初始化命名管道
Server s;//定义Server对象
s.OpenFifoForRead();//打开文件
//读取内容
std:: string message;
while(true)
{
message = s.ReadFifo();
if(message == "")//读到空,说明没有内容或者写端关闭,直接跳出循环
break;
std::cout<<message<<std::endl;
}
s.CloseFifo();//关闭文件
return 0;
}
运行结果如下:
- 先打开Server可执行程序,再打开Client可执行程序
先打开Server可执行程序,再打开Client可执行程序,就能实现进程间通信:
- 先打开Client可执行程序,再打开Server可执行程序
如果先打开Client可执行程序,因为命名管道是在Server可执行程序中初始化的,所以无法实现通信,结果如下:
- 先关闭Client可执行程序:
Client也就是写端关闭后,读端就会读到0,这时候就要主动设置退出循环,退出程序,否则Server程序会陷入死循环。
因为在Server程序中设置了读到空就跳出循环,所以先关闭Client程序后,Server程序也会成功退出:
while(true)
{
message = s.ReadFifo();
if(message == "")
break;//跳出死循环
std::cout<<message<<std::endl;
}
结果如下:
- 先关闭Server可执行程序:
读端也就是Server端关闭后,写端的进程会收到信号进而终止程序。
- 如果先打开Server可执行程序,Client可执行程序还未打开:
Server进程会阻塞在打开命名管道文件那里直到Client可执行程序打开
命名管道的打开规则
• 如果当前打开操作是为读⽽打开FIFO时
◦ O_NONBLOCK disable(阻塞模式):阻塞直到有相应进程为写⽽打开该FIFO
◦ O_NONBLOCK enable(非阻塞模式):⽴刻返回成功
• 如果当前打开操作是为写⽽打开FIFO时
◦ O_NONBLOCK disable(阻塞模式):阻塞直到有相应进程为读⽽打开该FIFO
◦ O_NONBLOCK enable(非阻塞模式):⽴刻返回失败,错误码为ENXIO
上述命名管道的使用由先打开Server可执行程序,Client可执行程序还未打开,Server进程会阻塞在打开命名管道文件说明当前命名管道是使用阻塞模式打开的
5. 结语
匿名管道由pipe
函数创建并打开。而命名管道由mkfifo
函数创建,打开⽤open
。FIFO
(命名管道)与pipe
(匿名管道)之间唯⼀的区别在它们创建与打开的⽅式不同,⼀但这些⼯作完成之后,它们具有相同的语义。以上就是命名管道有关的所有内容啦~ 完结撒花~ 🥳🎉🎉