Linux -- 进程间通信【命名管道】
目录
一、命名管道定义
二、命名管道创建
1、指令
2、系统调用
3、删除
三、匿名管道和命名管道的区别
四、命名管道的打开规则
五、代码示例
1、comm.hpp
2、server.cc
3、client.cc
一、命名管道定义
# 匿名管道存在以下核心限制:
- 仅限亲缘关系进程:只能用于父子进程等有血缘关系的进程间通信(如通过
fork()
创建的子进程)。 - 单向通信:数据只能单向流动(一端写,另一端读),双向通信需创建两个管道。
- 临时性:存在于内存中,进程结束后自动销毁。
- 缓冲区有限:大小固定(通常为一个内存页,如4KB),易写满阻塞。
# 引入命名管道的原因:
为解决匿名管道的局限性,命名管道允许任意进程(无论是否有亲缘关系)通过文件系统路径访问,实现跨进程通信。
# 由于匿名管道的局限性,如果我们想让两个毫不相关的进程间进行通信,就需要使用我们的命名管道。
# 命名管道与匿名管道都是只存在于内存中的文件,并不会向磁盘刷新,唯一不同的是匿名管道是通过父子进程看到同一份资源,而命名管道是通过路径与文件名的方式找到同一份文件资源,因为我们知道路径具有唯一性。
# 我们可以使用FIFO
文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。
二、命名管道创建
1、指令
mkfifo <路径名> # 例如:mkfifo /tmp/my_pipe
# 生成一个具名管道文件,权限默认受 umask
影响。
# 并且我们可以直接通过其进行echo和cat两个进程间的通信:
2、系统调用
# 使用 mkfifo()
函数:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功返回0,失败返回-1
- 参数:
pathname
:管道路径/文件名(如/tmp/my_pipe
)。若以路径的方式给出,则将命名管道文件创建在pathname
路径下,若以文件名的方式给出,则将命名管道文件默认创建在当前路径下mode
:权限标志(如0666
表示所有用户可读写),它受默认掩码umask
的约束。
- 后续操作:
- 需用
open()
打开管道(读模式O_RDONLY
或写模式O_WRONLY
)。 - 默认阻塞行为:读端打开时写端阻塞,反之亦然;可通过
O_NONBLOCK
设为非阻塞。
- 需用
# 比如说我们可以通过该接口实现客户端client
与服务端server
间的通信。
3、删除
- 命令行:
rm <路径名>
或unlink <路径名>
。- 程序内:
unlink(pathname)
。
三、匿名管道和命名管道的区别
关键补充
- 语义一致性:打开后两者操作方式相同(如
read()
/write()
)。 - 网络支持:命名管道可跨机器通信,匿名管道仅限本地。
- 阻塞行为:两者均受缓冲区影响,但命名管道可通过
O_NONBLOCK
灵活控制阻塞。
四、命名管道的打开规则
五、代码示例
# 下面为了更好理解命名管道,我们直接来一段代码,使用命名管道让两个无血缘关系的进程进行通信——一个进程写一个进程读。
# 这里client.cc和server.cc代表两个没有血缘关系的进程,在前面学习进程时我们知道,.cc文件跑起来就是一个进程,所以这里不多赘述。而我们命名管道的创建,以及打开管道文件进行操作的代码则封装在comm.hpp中。Makefile则是我们配置的自动化工具。
1、comm.hpp
# 下面我们就来在comm.hpp中将代码封装起来,首先需要将命名管道创建,最后结束通信后还需要将管道回收,因为命名管道不会随进程的生命周期,所以需要我们手动回收。代码如下:
class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_filename = _path + "/" + _name;// 创建命名管道int n = mkfifo(_filename.c_str(), 0666);if(n < 0){std::cerr << "mkfifo failed" << std::endl;}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 回收命名管道int n = unlink(_filename.c_str());if(n < 0){std::cerr << "remove fifo failed" << std::endl;}else{std::cout << "remove fifo success" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};
# 由于我们要实现一个进程写,一个进程读的单向通信,所以我们先规定,让客户端client.cc进程来写,服务端server.cc进程来读,那么读写操作我们还需要再封装一个类,因为我们只要创建一个管道就行了。
# 如果都封装在一个类中,那么客户端和服务端都需要实例化出一个对象,才能对管道读写通信,但这样就会创建两个命名管道了,因为只要构造函数就会创建命名管道,而我们不需要两个命名管道,我们只需要创建一个命名管道,然后服务端和客户端分别以读写的方式打开这个管道文件就可以进行通信了,所以我们可以再封装一个类来实现对打开的命名管道进行操作。代码如下:
class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};
# 由于我们需要打开指定路径的管道文件,所以成员变量仍然需要和NamedFifo类一样,但是我们打开管道文件后,需要通过返回的文件描述符后续管理规管道文件,所以我们还需要一个成员变量_fd,来接收open返回的文件描述符。客户端需要从管道写入,服务端需要从管道读取,所以客户端以只写的方式打开管道文件,而服务端以只读的方式打开管道文件。但是打开之后我们客户端和服务端还需要对管道进行读写操作,所以我们还需要分别实现一个写函数和一个读函数。代码如下:
void Write(){std::string message;while(true){std::cout << "Please Enter#";std::getline(std::cin, message);write(_fd, message.c_str(), message.size());}}void Read(){while(true){char buffer[1024];ssize_t n = read(_fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;std::cout << "Client say#" << buffer << std::endl;}else if(n == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}
# 当然,通信结束之后我们需要关闭文件描述符。
void Close(){close(_fd);}
# 我们定义两个宏,想要在当前路径下创建一个fifo的管道文件:
#define PATH "."
#define FILENAME "fifo"
# 再定义一个错误退出的宏:
// 在 C 语言中,\(反斜杠)在这里的作用是续行符,用于将一行代码延续到下一行。
#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)
# 源码:
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define PATH "."
#define FILENAME "fifo"// 在 C 语言中,\(反斜杠)在这里的作用是续行符,用于将一行代码延续到下一行。
#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name):_path(path),_name(name){_fifoname = _path + "/" + _name;// 将文件默认掩码设置为0umask(0);// 新建管道int n = mkfifo(_fifoname.c_str(), 0666);if (n < 0){// std::cerr << "mkfifo error" << std::endl;// perror("mkfifo");// exit(1);ERR_EXIT("mkfifo");}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 删除管道文件int n = unlink(_fifoname.c_str());if (n == 0){std::cout << "remove fifo success" << std::endl;}else{// std::cout << "remove fifo failed" << std::endl;ERR_EXIT("unlink");}}private:std::string _path;std::string _name;std::string _fifoname;
};class FileOper
{
public:FileOper(const std::string &path, const std::string &name): _path(path), _name(name),_fd(-1){_fifoname = _path + "/" + _name;}void OpenForRead(){// 打开// write方没有执行open的时候,read方就要在open内部进行阻塞,直到有人把管道文件打开了,open才会返回_fd = open(_fifoname.c_str(), O_RDONLY); // 以读方式打开命名管道文件if (_fd < 0){// std::cerr << "open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cout << "open fifo success" << std::endl;}void OpenForWrite(){// write_fd = open(_fifoname.c_str(), O_WRONLY); // 以写方式打开命名管道文件if (_fd < 0){// std::cerr << "Open fifo error" << std::endl;// return;ERR_EXIT("open");}std::cerr << "Open fifo success" << std::endl;}void Write(){// 写入操作std::string message;int cnt = 1;pid_t id = getpid();while (true){std::cout << "Please Enter# ";std::getline(std::cin, message);message += ", message number: " + std::to_string(cnt++) + "[" + std::to_string(id) + "]";write(_fd, message.c_str(), message.size());}}void Read(){// 正常的readwhile (true){char buffer[1024];int number = read(_fd, buffer, sizeof(buffer) - 1);if (number > 0){// 读取成功buffer[number] = 0; // 字符串末尾置\0std::cout << "Client say# " << buffer << std::endl;}else if (number == 0){std::cout << "client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}void Close(){close(_fd);}~FileOper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
2、server.cc
#include "comm.hpp"int main()
{// 创建管道NamedFifo f(PATH, FILENAME);// 文件操作Fileoper reader(PATH, FILENAME);reader.OpenForRead();reader.Read();reader.Close();return 0;
}
3、client.cc
#include "comm.hpp"int main()
{Fileoper Writer(PATH, FILENAME);Writer.OpenForWrite();Writer.Write();Writer.Close(); return 0;
}
运行测试:
# 可以看到成功实现了两个没有血缘关系的进程的单向通信。