目录
一、命名管道的概念
命名管道(FIFO,First In First Out)是一种特殊的文件类型,用于在不同进程之间进行数据交换。它的设计允许一个进程将数据写入管道,而另一个进程从管道中读取数据。这种机制使得不同进程能够通过管道进行通信,而不需要直接共享内存或使用其他复杂的同步机制。
二、命名管道与匿名管道的区别
1、常规区别
-
命名管道:
- 命名管道是一个在文件系统中存在的特殊文件,具有一个路径名,允许不同进程通过该路径名进行数据通信。
- 可以通过
mkfifo()
系统调用创建,并且可以在文件系统中查看和操作。 - 命名管道在文件系统中具有可见性,可以通过路径名进行访问,任何进程只要知道管道的名称都可以进行读写操作。
- 适用于不同进程之间的通信,尤其是没有亲缘关系的进程(例如在不同的终端或不同的应用程序之间)。由于命名管道在文件系统中存在,适合需要跨多个会话或长期运行的进程通信的场景。
-
匿名管道:
- 匿名管道没有名称,通常用于具有亲缘关系的进程(例如父进程和子进程)之间的通信。
- 其创建通常通过
pipe()
系统调用实现,返回一对文件描述符(读写端)。 - 匿名管道在创建后不具有名称,无法在文件系统中访问。只能通过创建它的进程及其子进程进行通信,其他进程无法直接访问。
- 主要用于具有亲缘关系的进程之间的通信,如父进程和子进程。适合于短期的、临时的通信需求。常用于简单的数据传输,尤其是在创建子进程后立即进行数据传输时。
2、读写阻塞的区别
命名管道在管道读写阻塞的规则上与匿名管道相似,读取和写入操作均会在相应条件下发生阻塞。 如果一个进程尝试从空的命名管道中读取数据,它会被阻塞,直到有数据写入管道。同样,如果管道缓冲区已满,写入操作也会阻塞,直到有足够的空间。
但在创建管道文件完成后仍有区别。
-
匿名管道:
- 当使用pipe系统调用创建匿名管道成功时,管道的读写两端自动打开。
- 无需使用open函数对匿名管道文件进行读写打开的操作。直接调用write和read函数对匿名管道进行读写即可。
-
命名管道:
- 当成功创建FIFO文件后,管道的读写两端需要进程主动使用open()系统调用函数打开。
- 如果进程以只读的方式打开FIFO文件,open会阻塞,等待有进程以写的方式打开此FIFO文件才会成功返回(阻塞过程可以被信号打断)。如果使用open函数时,O_NONBLOCK标志被置位,则当没有写进程时,不进行阻塞等待,直接返回错误。
- 类似的,如果进程以只写的方式打开FIFO文件,open会阻塞,等待有进程以读的方式打开此FIFO文件才会成功返回(阻塞过程可以被信号打断)。如果使用open函数时,O_NONBLOCK标志被置位,则当没有读进程时,不进行阻塞等待,直接返回错误,错误码为ENXIO。
- 如果以读写的方式打开FIFO文件,则不会发生阻塞。但单个管道用于单向通信,最好不要在同一个进程中同时以读+写的方式打开!!!
3、存储位置和数据处理的区别
匿名管道
-
存储位置:
- 仅存在于内存中:匿名管道不涉及文件系统,它们完全在内存中实现。它们没有文件路径,因此不在文件系统中存在,而是作为内存中的数据缓冲区来进行通信。
- 独立于文件系统:匿名管道是进程间通信的一种机制,仅在创建它的进程及其子进程中存在。它们的生命周期通常由创建它们的进程控制,当进程结束时,匿名管道也随之消失。
-
数据处理:
- 数据在内存中的缓冲区中处理,读写操作都是通过内存进行的。写入的数据会在内存中缓冲,直到被读取。在此过程中,数据不会被写入到磁盘。
命名管道
-
存储位置:
- 文件系统中的特殊文件:命名管道通过
mkfifo()
系统调用创建,这会在文件系统中创建一个特殊的文件,这个文件代表命名管道。该文件存在于磁盘上的文件系统中,可以通过路径名进行访问。 - 元数据存储:命名管道的元数据(如管道的名字、权限等)存储在文件系统中,这使得它们在系统重启后仍然存在(即文件系统中的信息不会丢失)。
- 文件系统中的特殊文件:命名管道通过
-
数据处理:
- 内存中的数据流:虽然命名管道的路径名和元数据存储在磁盘上,但实际的数据流经管道的过程中会在内存中进行缓冲。写入的数据首先会存储在内存中的缓冲区中,直到被读取。在此过程中,数据不会被写入到磁盘——即文件的内容大小永远为0!
三、命名管道的创建与使用
1、在命令行中命名管道的创建与使用
1. 创建命名管道
使用 mkfifo
命令来创建一个命名管道。mkfifo
命令会在文件系统中创建一个特殊的文件,代表命名管道。例如:
mkfifo /pathname/fifo
其中 /pathname/
是命名管道的路径,fifo是命名管道的名称。例如:
这会在 当前目录下创建一个名为 fifo
的命名管道。
可以看出fifo是一种特殊的管道文件。
2. 使用命名管道
命名管道的使用可以通过标准输入/输出操作进行。
以下是一个简单的示例,展示如何使用命名管道在两个进程之间进行通信。
步骤:
-
在一个终端中,启动一个读取进程:
cat /pathname/myfifo
这个进程会等待从管道中读取数据。
-
在另一个终端中,启动一个写入进程:
echo "Hello from the other side!" > /pathname/myfifo
这条命令将数据写入到管道中,读取进程会立即接收到数据并输出。
3. 清理
如果不再需要命名管道,可以使用 rm
命令删除它:
rm /tmp/myfifo
这会从文件系统中删除 /tmp/myfifo
命名管道文件。
2、在编程中命名管道的创建与使用
1、认识mkfifo系统调用函数
函数原型
在 C 语言中,
mkfifo()
的函数原型定义在<sys/stat.h>
头文件中,如下所示:int mkfifo(const char *pathname, mode_t mode);
参数
pathname
: 指定要创建的命名管道的路径和文件名。路径可以是绝对路径或相对路径。mode
: 指定命名管道的权限。这个参数是一个八进制数,用于设置文件的权限。例如,0666
代表所有用户都有读写权限。返回值
- 成功时返回
0
。- 失败时返回
-1
,并设置errno
来指示错误类型。常见错误
EEXIST
: 指定的路径已经存在,且不是一个 FIFO 文件。ENOSPC
: 文件系统没有足够的空间来创建文件。EACCES
: 没有权限创建文件。
2、示例代码
以下是一个简单的示例程序,演示如何使用 mkfifo() 创建命名管道,并用 open(), write(), 和 read() 操作它,需要注意的是:以下是三个不同的程序,运行时即为三个不同的进程,必须要先创建好命名管道才能对其进行读写!!!
(1)、创建命名管道
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "/tmp/myfifo"
int main() {
// 创建命名管道
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
// 成功创建后,程序会在此停留
printf("FIFO %s created successfully.\n", FIFO_NAME);
return 0;
}
(2)、写入数据到命名管道
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "/tmp/myfifo"
int main() {
int fd;
const char *message = "Hello from the writer!";
// 打开命名管道
fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 写入数据
if (write(fd, message, sizeof(message)) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
printf("Message written to FIFO: %s\n", message);
// 关闭文件描述符
close(fd);
return 0;
}
(3)、读取数据从命名管道
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "/tmp/myfifo"
int main() {
int fd;
char buffer[100];
// 打开命名管道
fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 读取数据
if (read(fd, buffer, sizeof(buffer)) == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("Message read from FIFO: %s\n", buffer);
// 关闭文件描述符
close(fd);
return 0;
}
四、命名管道的应用—模拟简易的应用端与客户端通信
场景模拟:客户端向服务端发送信息,服务端接收信息后对信息进行处理。
场景实现:
1、客户端与服务端分别为两个不同的进程,当服务端开始运行时需要创建一个通信管道作为信息传输的媒介。
2、客户端通过向管道中写入信息,服务端从管道中读取信息,服务端拿到信息后再对信息进行处理,进而实现不同进程间的通信。
需要注意的是,当程序退出时,我们应该主动删除命名管道文件,防止资源浪费,在这里介绍一个函数unlink():
unlink 函数用于删除文件系统中的文件或链接。在 Linux 系统中,unlink 是一个系统调用,它从文件系统中删除指定路径的文件名。如果这个文件名是该文件的最后一个名字(即没有其他硬链接指向这个文件数据),文件数据会被实际删除;如果还有其他文件名引用这个文件的数据,文件数据会保留,直到所有引用它的文件名都被删除。
函数原型
#include <unistd.h> int unlink(const char *pathname);
参数
pathname
: 要删除的文件的路径。返回值
成功时返回
0
。失败时返回
-1
并设置errno
以指示错误类型。常见错误
ENOENT
: 指定的文件不存在。
EACCES
: 权限被拒绝,无法删除文件。
EPERM
: 操作不允许,如尝试删除一个目录而不是普通文件(虽然unlink
只对文件有效,删除目录应该使用rmdir
)。用法示例
如何使用 unlink删除一个文件:
#include <iostream> #include <unistd.h> #include <cerrno> #include <cstring> void deleteFile(const char* filepath) { if (unlink(filepath) == -1) { std::cerr << "Error deleting file: " << std::strerror(errno) << std::endl; } else { std::cout << "File deleted successfully." << std::endl; } } int main() { const char* filePath = "./myfifo"; deleteFile(filePath); return 0; }
备注
unlink只能删除文件,而不能删除目录。如果你需要删除目录,请使用rmdir。
unlink对于 FIFO(命名管道)和常规文件的行为相同。当 FIFO 文件没有任何进程打开时,它会被删除。
使用场景
- 在程序结束时或在不再需要时删除临时文件。
- 删除命名管道(FIFO),如在 IPC(进程间通信)中创建的 FIFO 文件。
确保在适当的时候调用 unlink,例如在程序退出时,如果程序中使用了 mkfifo创建 FIFO 文件时,应主动调用unlink函数删除fifo文件,以避免留下不必要的文件。
头文件:namedpipe.hpp
#pragma once
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cstring>
#define PATHNAME "./myfifo"
#define READER 0
#define WRITER 1
#define LIMIT 0666
#define BUFFER_SIZE 256
class NamePipe
{
private:
std::string _path;
int _fd;
int _identity;
public:
NamePipe(const std::string& path, int identity)
:_path(path), _identity(identity), _fd(-1)
{
if(_identity == READER){
if(mkfifo(PATHNAME, LIMIT) == -1){
perror("create fifo fail!");
exit(-1);
}
}
}
~NamePipe(){
if (_identity == READER)
{
int res = unlink(_path.c_str());
if (res != 0)
{
perror("unlink fail!");
}
}
Close();
}
void OpenForRead()
{
if((_fd = open(_path.c_str(), O_RDONLY, LIMIT)) == -1){
perror("open for read fail!");
exit(-1);
}
else{
std::cout << "客户端准备进行通信..." << std::endl;
}
}
int Read()
{
int r_num = 0;
char buffer[BUFFER_SIZE];
memset(buffer, 0, sizeof(buffer));
if((r_num = read(_fd, buffer, sizeof(buffer) - 1)) == -1){
perror("read fail!");
return -1;
}
else if(r_num == 0)
{
std::cout << "客户端关闭,通信终止..." << buffer << std::endl;
return 0;
}
else {
buffer[r_num] = '\0';
std::cout << "读取信息为:" << buffer << std::endl;
}
return r_num;
}
void OpenForWrite()
{
if((_fd = open(_path.c_str(), O_WRONLY, LIMIT)) == -1){
perror("open for write fail!");
exit(-1);
}
else{
std::cout << "服务端端准备进行通信..." << std::endl;
}
}
void Write(const std::string& buffer)
{
int w_num = 0;
if((w_num = write(_fd, buffer.c_str(), buffer.size())) == -1){
perror("read fail!");
exit(-1);
}
else{
std::cout << "写入信息成功!" << std::endl;
}
}
void Close()
{
close(_fd);
}
};
makefile:
.PHONY:all
all:server client
server:server.cc
g++ server.cc -o server
client:client.cc
g++ client.cc -o client
.PHONY:clean
clean:
rm -f client server
server.cc
#include "namedpipe.hpp"
int main()
{
NamePipe fifo(PATHNAME, READER);
fifo.OpenForRead();
while (true)
{
int ret = fifo.Read();
if (ret == 0)
{
std::cout << "Client quit, Server Too!" << std::endl;
break;
}
else if (ret == -1)
{
std::cout << "ReadNamedPipe Fail!" << std::endl;
break;
}
}
return 0;
}
client.cc:
#include "namedpipe.hpp"
int main()
{
NamePipe fifo(PATHNAME, WRITER);
fifo.OpenForWrite();
std::string buffer;
while(true)
{
std::cout << "请输入信息:";
std::getline(std::cin, buffer);
fifo.Write(buffer);
}
return 0;
}