1.?匿名管道的限制
匿名管道存在以下核心限制:
- 僅限親緣關系進程:只能用于父子進程等有血緣關系的進程間通信(如通過?
fork()
?創建的子進程)。 - 單向通信:數據只能單向流動(一端寫,另一端讀),雙向通信需創建兩個管道。
- 臨時性:存在于內存中,進程結束后自動銷毀。
- 緩沖區有限:大小固定(通常為一個內存頁,如4KB),易寫滿阻塞。
引入命名管道的原因:
為解決匿名管道的局限性,命名管道允許任意進程(無論是否有親緣關系)通過文件系統路徑訪問,實現跨進程通信。
2.?什么是命名管道
命名管道(Named Pipe/FIFO)是一種特殊的文件類型,特點包括:
- 文件系統可見:通過路徑名(如?
/tmp/myfifo
)標識,任何進程可訪問。 - 遵循FIFO原則:數據按寫入順序讀取,嚴格保持先進先出。
- 突破親緣限制:不相關進程可通過路徑名打開同一管道通信。
- 雙向通信支持:部分場景下支持讀寫雙向操作(需顯式設計)。
示例:命名管道在文件系統中顯示為特殊文件(權限位帶?
p
,如?prw-r--r--
)。
3.?如何創建命名管道
方法一:命令行創建
mkfifo <路徑名> # 例如:mkfifo /tmp/my_pipe
生成一個具名管道文件,權限默認受?umask
?影響。
示例:
方法二:程序內創建
使用?mkfifo()
?函數:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功返回0,失敗返回-1
- 參數:
pathname
:管道路徑(如?/tmp/my_pipe
)。mode
:權限標志(如?0666
?表示所有用戶可讀寫)。
- 后續操作:
- 需用?
open()
?打開管道(讀模式?O_RDONLY
?或寫模式?O_WRONLY
)。 - 默認阻塞行為:讀端打開時寫端阻塞,反之亦然;可通過?
O_NONBLOCK
?設為非阻塞。
- 需用?
刪除管道:
- 命令行:
rm <路徑名>
?或?unlink <路徑名>
。- 程序內:
unlink(pathname)
。
4.?匿名管道和命名管道的區別
特性 | 匿名管道 | 命名管道 | 證據來源 |
---|---|---|---|
創建方式 | pipe(fd) ?一步創建并打開 | mkfifo() ?創建 +?open() ?打開 | |
進程關系要求 | 必須具有親緣關系(如父子進程) | 任意進程均可訪問 | |
持久性 | 隨進程結束銷毀 | 文件系統持久,需手動刪除 | |
通信方向 | 僅單向 | 可支持雙向通信 | |
性能 | 略快(無文件系統操作) | 稍慢(涉及磁盤索引節點) | |
使用場景 | 短期親緣進程通信 | 長期/跨進程通信(如C/S架構) |
關鍵補充
- 語義一致性:打開后兩者操作方式相同(如?
read()
/write()
)。 - 網絡支持:命名管道可跨機器通信,匿名管道僅限本地。
- 阻塞行為:兩者均受緩沖區影響,但命名管道可通過?
O_NONBLOCK
?靈活控制阻塞。
5. 命名管道的打開規則
一、為讀而打開 FIFO(O_RDONLY
)
O_NONBLOCK
?未設置(默認阻塞)- 行為:調用?
open()
?會阻塞當前進程,直到有另一個進程為寫而打開同一 FIFO。 - 原理:內核需確保存在數據生產者,否則讀操作無意義。
- 行為:調用?
"open以只讀方式打開FIFO時,要阻塞到某個進程為寫而打開此FIFO"?。
"若沒有指定O_NONBLOCK,只讀 open 要阻塞到某個其他進程為寫而打開此 FIFO"?。
O_NONBLOCK
?設置(非阻塞)- 行為:
open()
?立即成功返回(返回文件描述符),無論是否有寫端打開。 - 后續注意:此時若管道無數據,
read()
?可能返回 0(EOF)或?EAGAIN
?錯誤(見下文讀寫規則)。
- 行為:
"先以只讀方式打開,如果沒有進程已經為寫而打開一個FIFO,只讀 open() 成功,并且 open() 不阻塞"?。
"若指定了O_NONBLOCK,則只讀 open 立即返回"?。
二、為寫而打開 FIFO(O_WRONLY
)
O_NONBLOCK
?未設置(默認阻塞)- 行為:調用?
open()
?會阻塞當前進程,直到有另一個進程為讀而打開同一 FIFO。 - 原理:內核需確保存在數據消費者,否則寫操作可能無限等待。
- 行為:調用?
"open以只寫方式打開FIFO時,要阻塞到某個進程為讀而打開此FIFO"?。
"只寫 open 要阻塞到某個其他進程為讀而打開它"?。
O_NONBLOCK
?設置(非阻塞)- 行為:若無讀端已打開,
open()
?立即失敗,返回?-1
?并設置錯誤碼?ENXIO
(表示設備不存在)。 - 行為:若已有讀端打開,則?
open()
?成功。
- 行為:若無讀端已打開,
"先以只寫方式打開,如果沒有進程已經為讀而打開一個FIFO,只寫 open() 將出錯返回 -1"?。
"若指定了O_NONBLOCK,則只寫 open 將出錯返回 -1 如果沒有進程已經為讀而打開該 FIFO,其errno置ENXIO"?。
三、關鍵補充與深度解析
O_RDWR
(讀寫模式)的特殊性- 行為:以?
O_RDWR
?模式打開時?永不阻塞,因進程自身已同時打開讀寫端?。 - 風險:可能導致自我死鎖(如寫滿后讀阻塞),實踐中極少使用。
- 行為:以?
讀寫操作的阻塞行為(與?
open
?獨立)操作 O_NONBLOCK
?未設置O_NONBLOCK
?設置read()
?空管道阻塞直到有數據寫入 立即返回? EAGAIN
(或空數據)write()
?滿管道阻塞直到有空間 部分寫入或返回? EAGAIN
管道斷裂與信號處理
- 寫端關閉:讀端?
read()
?返回 0(EOF),不阻塞?。 - 讀端關閉:寫端?
write()
?觸發?SIGPIPE
?信號(默認終止進程),錯誤碼?EPIPE
?。
- 寫端關閉:讀端?
原子性與?
PIPE_BUF
- 規則:寫入 ≤?
PIPE_BUF
?字節的數據保證原子性(不與其他進程交織)。 - 典型值:Linux 中?
PIPE_BUF
?為 4096 字節(一頁大小)。
- 規則:寫入 ≤?
?四、內核實現原理(選讀)
- 阻塞的本質
- 進程休眠在 FIFO inode 的等待隊列中,由另一端打開或數據變動時喚醒?。
- 示例:
// Linux 內核片段(讀打開阻塞邏輯) if (PIPE_READERS(*inode)++ == 0) wait_for_partner(inode, &PIPE_WCOUNTER(*inode)); // 等待寫端
- 非阻塞的沖突處理
- 寫打開時若無讀端,內核直接返回?
ENXIO
?而非加入等待隊列?:
- 寫打開時若無讀端,內核直接返回?
"若命名管道讀端尚未打開,而 O_NONBLOCK=1,寫端打開失敗并釋放資源"?。
總結與建議
場景 | 打開模式 | O_NONBLOCK | 結果 |
---|---|---|---|
讀打開,無寫端存在 | O_RDONLY | 未設置 | 阻塞 |
讀打開,無寫端存在 | O_RDONLY | 設置 | 立即成功 |
寫打開,無讀端存在 | O_WRONLY | 未設置 | 阻塞 |
寫打開,無讀端存在 | O_WRONLY | 設置 | 立即失敗(ENXIO) |
讀寫打開 | O_RDWR | 任意 | 立即成功(不依賴外部進程) |
工程建議:
- 生產-消費模型:推薦讀端阻塞打開(確保寫端就緒),寫端非阻塞打開(快速失敗+重試邏輯)。
- 超時控制:若需阻塞但避免無限等待,結合?
select()
/poll()
?設置超時。 - 錯誤處理:始終檢查?
open()
?返回值和?errno
,尤其非阻塞模式。
6. 代碼示例
下面為了更好理解命名管道,我們直接來一段代碼,使用命名管道讓兩個無血緣關系的進程進行通信——一個進程寫一個進程讀。
這里client.cc和server.cc代表兩個沒有血緣關系的進程,在前面學習進程時我們知道,.cc文件跑起來就是一個進程,所以這里不多贅述。而我們命名管道的創建,以及打開管道文件進行操作的代碼則封裝在comm.hpp中。Makefile則是我們配置的自動化工具。
下面我們就來在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);}
測試
我們先定義兩個宏
#define PATH "."
#define FILENAME "fifo"
我們想要在當前路徑下創建一個fifo的管道文件
服務端:
#include "comm.hpp"int main()
{// 創建管道NamedFifo f(PATH, FILENAME);// 文件操作Fileoper reader(PATH, FILENAME);reader.OpenForRead();reader.Read();reader.Close();return 0;
}
客戶端:
#include "comm.hpp"int main()
{Fileoper Writer(PATH, FILENAME);Writer.OpenForWrite();Writer.Write();Writer.Close(); return 0;
}
運行測試:
可以看到成功實現了兩個沒有血緣關系的進程的單向通信
源碼:
comm.hpp:
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define PATH "."
#define FILENAME "fifo"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;
};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;}}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);}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};