目錄
一、命名管道定義
二、命名管道創建
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;
}
運行測試:
#?可以看到成功實現了兩個沒有血緣關系的進程的單向通信。