Linux:進程間通信-管道
前言:為什么需要進程間通信?
你有沒有想過,當你在電腦上同時打開瀏覽器、音樂播放器和文檔時,這些程序是如何協同工作的?比如,瀏覽器下載的文件,為什么能被文檔編輯器直接打開?音樂播放器的音量調節,為什么能影響系統全局的聲音輸出?這背后,其實都是進程間通信(IPC)在發揮作用。
進程作為操作系統中獨立運行的基本單位,彼此之間默認是隔離的——就像住在不同房間的人,沒有門也沒有窗,無法直接交流。但實際應用中,進程又必須協同工作:比如打印進程需要接收文檔進程的數據,視頻渲染進程需要獲取解碼進程的結果。這就要求我們打破這種隔離,建立進程間的"溝通渠道"。
今天這篇文章,我們就從最基礎的管道開始,一步步揭開Linux進程間通信的神秘面紗。你會發現,看似復雜的IPC機制,其實和現實生活中的通信場景有著驚人的相似之處。
一、進程間通信的基本概念
1.1 什么是進程間通信?
進程間通信(IPC,Inter-Process Communication) 指的是兩個或多個進程之間進行數據交換的過程。它的本質是讓彼此獨立的進程能夠共享數據,實現協同工作。
舉個生活中的例子:你在廚房做飯(進程A),需要客廳的家人幫忙遞一下鹽(進程B)。這里的"遞鹽"就是一次簡單的IPC——你和家人(兩個進程)通過語言(通信方式)交換了"需要鹽"這個數據。
在計算機中,進程間的"語言"有很多種,比如管道、消息隊列、共享內存等,我們今天重點討論最基礎也最常用的"管道"。
1.2 為什么需要進程間通信?
你可能會說:"進程各自干好自己的事就行了,為什么非要通信?"但實際場景中,進程間的協同是必不可少的,主要體現在這幾個方面:
- 數據傳輸:一個進程需要將數據發送給另一個進程。比如,輸入法進程需要把你輸入的文字發送給編輯器進程。
- 資源共享:多個進程需要共享同一份資源(比如文件、內存)。比如,多個瀏覽器標簽頁需要共享同一個緩存文件。
- 進程控制:一個進程需要控制另一個進程的行為。比如,任務管理器進程可以強制關閉無響應的程序進程。
- 事件通知:一個進程需要向其他進程通知某個事件的發生。比如,下載進程完成后,通知用戶進程彈出提示。
想想看,如果沒有IPC,你的電腦會變成什么樣?瀏覽器下載的文件無法保存到硬盤(需要與文件系統進程通信),播放音樂時無法調節音量(需要與音頻進程通信),甚至連復制粘貼功能都無法實現(需要剪貼板進程在多個程序間傳遞數據)。
1.3 進程間通信的成本為什么高?
既然IPC這么重要,為什么實現起來不簡單呢?這就要從進程的"獨立性"說起了。
進程的獨立性是操作系統設計的基本原則——每個進程都有自己獨立的內存空間、寄存器狀態和文件描述符表。這種隔離性保證了一個進程的崩潰不會影響其他進程,但也給通信帶來了麻煩:進程A的內存數據,進程B默認是看不到的。
就像兩個加密的保險箱,各自有獨立的密碼,不借助外部工具(比如鑰匙),里面的東西無法互通。要實現通信,就必須打破這種獨立性,建立共享資源——而創建和管理共享資源,必然會帶來系統開銷(比如內存分配、權限檢查)和復雜性(比如同步問題)。
舉個例子:如果進程A想給進程B發送數據,需要先把數據從A的用戶空間拷貝到內核空間的共享緩沖區,再由B從內核空間拷貝到自己的用戶空間(兩次拷貝)。這個過程比進程內部的數據訪問要慢得多,這就是通信的成本。
二、進程間通信的實現基礎
2.1 操作系統在IPC中扮演什么角色?
進程間通信不能靠進程自己"私下聯系",必須由操作系統作為"第三方協調者"。操作系統的作用主要有三個:
- 提供共享資源:比如創建管道、消息隊列等內核級資源,讓進程可以通過這些資源交換數據。
- 管理資源生命周期:負責創建、使用和釋放通信資源,避免資源泄露。
- 保證安全性和可控性:通過系統調用接口限制進程對資源的訪問,防止越權操作。
打個比方,操作系統就像一個中介:進程A和進程B想通信,先向中介申請一個"會議室"(共享資源),中介創建并管理這個會議室,A和B只能通過中介規定的方式進入會議室交流。
2.2 通信資源是如何管理的?
操作系統管理通信資源的核心原則是"先描述,再組織"。
- 描述:每個通信資源(比如管道)都會被內核用一個數據結構(如
struct pipe_inode_info
)描述,記錄資源的屬性(大小、權限)、狀態(是否被使用)和操作方法(讀、寫函數)。 - 組織:內核會把所有同類資源用鏈表或哈希表組織起來,方便查詢和管理。比如,所有管道會被放在一個全局鏈表中,操作系統可以通過遍歷鏈表找到某個特定管道。
這種管理方式就像圖書館的圖書管理:每本書(資源)都有一張卡片(描述結構),記錄書名、作者等信息;所有卡片按分類(組織方式)存放在卡片柜里,方便查找。
2.3 常見的IPC標準有哪些?
早期的Unix系統中,不同廠商實現的IPC機制各不相同,導致程序兼容性很差。后來行業逐漸形成了兩套主流標準:
- System V IPC:由AT&T貝爾實驗室提出,主要包括消息隊列、信號量和共享內存三種方式,適用于單機內的進程通信。
- POSIX IPC:由IEEE制定,兼容System V的部分功能,同時支持線程通信和網絡通信,接口更統一,現在應用更廣泛。
這兩套標準就像通信領域的"普通話",讓不同進程(甚至不同程序語言編寫的進程)能按照統一的規則交流。
三、管道:最古老的IPC方式
3.1 什么是管道?
管道(Pipe)是Unix系統中最古老的IPC方式,它的設計非常樸素:用內存中的文件緩沖區模擬"管道",讓一個進程往管道里寫數據,另一個進程從管道里讀數據。
你可以把管道想象成一根水管:一端進水(寫端),另一端出水(讀端),水(數據)在管內單向流動。這種單向性是管道的核心特征——就像現實中的水管,你不能同時從一端既進水又出水。
在Linux命令行中,你其實早就用過管道了。比如ps aux | grep "chrome"
這個命令,ps
進程的輸出通過|
(管道符號)傳遞給grep
進程,這里的|
就是一個匿名管道。
3.2 管道的實現原理
管道本質上是一個內存級文件,它有這些特點:
- 不在磁盤上存儲,數據只存在于內存緩沖區中。
- 遵循文件操作的接口(打開、讀、寫、關閉),但不需要刷新到磁盤。
- 通過文件描述符表讓進程訪問:一個描述符對應讀端,另一個對應寫端。
具體實現步驟如下:
- 創建管道:通過
pipe()
系統調用創建管道,內核會分配一個內存緩沖區,并返回兩個文件描述符:fd[0]
(讀端)和fd[1]
(寫端)。 - 創建子進程:通過
fork()
創建子進程,子進程會繼承父進程的文件描述符表,因此也能訪問同一個管道。 - 關閉無用端口:父進程關閉讀端(
fd[0]
),子進程關閉寫端(fd[1]
),形成單向通信信道(父寫子讀);或者反過來(父讀子寫)。 - 通信:父進程通過
write()
向fd[1]
寫數據,子進程通過read()
從fd[0]
讀數據。
舉個例子:父進程想給子進程發送"hello",步驟如下:
- 父進程調用
pipe(fd)
,得到fd[0]=3
(讀)、fd[1]=4
(寫)。 - 父進程
fork()
出子進程,子進程的fd
數組也是[3,4]
。 - 父進程
close(fd[0])
(關閉讀端),子進程close(fd[1])
(關閉寫端)。 - 父進程
write(fd[1], "hello", 5)
,子進程read(fd[0], buf, 5)
,最終buf
中就有"hello"。
3.3 管道的代碼實現
下面我們用C語言實現一個簡單的父子進程管道通信:父進程向子進程發送消息,子進程打印消息。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>int main() {int fd[2];// 1. 創建管道if (pipe(fd) == -1) {perror("pipe error");exit(1);}// 2. 創建子進程pid_t pid = fork();if (pid == -1) {perror("fork error");exit(1);}if (pid == 0) { // 子進程:讀數據close(fd[1]); // 關閉寫端char buf[1024];ssize_t len = read(fd[0], buf, sizeof(buf)-1);if (len > 0) {buf[len] = '\0';printf("子進程收到:%s\n", buf);}close(fd[0]); // 關閉讀端exit(0);} else { // 父進程:寫數據close(fd[0]); // 關閉讀端const char* msg = "你好,子進程!";write(fd[1], msg, strlen(msg));close(fd[1]); // 關閉寫端(觸發子進程讀結束)wait(NULL); // 等待子進程退出exit(0);}
}
編譯運行后,會輸出:子進程收到:你好,子進程!
。
這里有幾個關鍵點:
- 子進程必須關閉寫端,父進程必須關閉讀端,否則會導致阻塞(比如子進程讀完數據后,會一直等父進程寫更多數據)。
- 當寫端關閉后,讀端
read()
會返回0,表示數據已讀完(類似文件結束)。 - 管道的緩沖區大小是固定的(通常為64KB),如果寫端寫滿緩沖區,會阻塞直到讀端讀取數據釋放空間。
3.4 管道的五大特性
通過上面的例子,我們可以總結出管道的五個核心特性:
-
只能單向通信:管道是半雙工的,數據只能從一端到另一端。如果需要雙向通信,必須創建兩個管道。
(思考:為什么管道設計成單向的?其實是為了簡化實現——雙向通信需要更復雜的同步機制,而單向通信能滿足大部分場景。)
-
只能用于有血緣關系的進程:因為管道沒有名字,只能通過
fork()
繼承文件描述符的方式讓進程共享。父子進程、兄弟進程(同一個父進程創建)之間可以用管道通信,但兩個無關進程不行。 -
面向字節流:管道中的數據是連續的字節流,沒有消息邊界。比如,父進程分兩次寫"hello"和"world",子進程可能一次就讀到"helloworld"。
(注意:這意味著應用程序需要自己定義協議來區分消息,比如用換行符分隔,或固定消息長度。)
-
自帶同步機制:
- 讀端:如果管道為空,
read()
會阻塞,直到有數據寫入。 - 寫端:如果管道滿了,
write()
會阻塞,直到有數據被讀走。
- 讀端:如果管道為空,
-
生命周期隨進程:管道會在所有訪問它的進程都關閉文件描述符后,被內核自動銷毀。
3.5 管道的四種典型情況
管道通信中,讀寫端的狀態會直接影響通信行為,常見的四種情況需要特別注意:
情況 | 現象 | 原因 |
---|---|---|
讀寫端正常,管道為空 | 讀端阻塞 | 讀端等待寫端寫入數據 |
讀寫端正常,管道滿了 | 寫端阻塞 | 寫端等待讀端讀取數據釋放空間 |
讀端關閉,寫端繼續寫 | 寫端進程被殺死 | 操作系統發送SIGPIPE 信號終止寫進程(避免無效寫入) |
寫端關閉,讀端繼續讀 | 讀端讀到0(文件結束) | 寫端關閉后,管道中剩余數據讀完后,read() 返回0 |
比如,如果你在代碼中忘了關閉寫端,子進程的read()
會一直阻塞(以為還有數據要讀),導致程序卡死。這也是為什么我們強調"一定要關閉無用的文件描述符"。
四、命名管道:讓無關進程也能通信
4.1 匿名管道的局限性
匿名管道雖然簡單,但有個致命缺點:只能用于有血緣關系的進程。如果兩個完全無關的進程(比如瀏覽器和音樂播放器)想通信,匿名管道就無能為力了——因為它們無法共享文件描述符。
這就像兩個陌生人住在不同的小區,沒有共同的朋友(父進程)介紹,無法知道對方的地址(管道的文件描述符)。要解決這個問題,就需要一種"有名字"的管道——命名管道(FIFO)。
4.2 什么是命名管道?
命名管道(FIFO,First In First Out)和匿名管道的核心原理相同,但它有一個關鍵區別:命名管道有文件名和路徑,可以通過文件系統被所有進程訪問。
就像一個公共郵箱:任何知道郵箱地址(路徑)的人,都可以往里面放信(寫數據)或取信(讀數據),不需要彼此認識。
在Linux中,你可以用mkfifo
命令創建命名管道:
mkfifo myfifo # 創建一個名為myfifo的命名管道
創建后,你會在目錄中看到這個文件,類型為p
(管道):
ls -l myfifo
# 輸出:prw-r--r-- 1 user user 0 8月 21 10:00 myfifo
4.3 命名管道的使用方式
命名管道的使用步驟和文件操作類似,分為創建、打開、讀寫、關閉四個步驟:
-
創建:用
mkfifo
命令或mkfifo()
函數創建。#include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); // 參數:pathname(管道路徑)、mode(權限,如0666) // 返回值:0成功,-1失敗
-
打開:用
open()
函數打開,指定讀或寫模式。int fd = open("myfifo", O_RDONLY); // 只讀打開(讀端) // 或 int fd = open("myfifo", O_WRONLY); // 只寫打開(寫端)
-
讀寫:用
read()
和write()
函數操作,和匿名管道相同。 -
關閉:用
close()
關閉文件描述符。 -
刪除:用
unlink()
函數刪除管道文件(類似rm
命令)。
4.4 命名管道的代碼實現
下面我們實現兩個無關進程的通信:一個寫進程向命名管道發送消息,一個讀進程接收消息。
寫進程(writer.c):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>int main() {// 1. 創建命名管道(如果不存在)if (mkfifo("myfifo", 0666) == -1) {perror("mkfifo error");exit(1);}// 2. 打開管道(寫端)int fd = open("myfifo", O_WRONLY);if (fd == -1) {perror("open error");exit(1);}// 3. 發送消息const char* msg = "來自writer的消息:你好,reader!";write(fd, msg, strlen(msg));printf("發送成功\n");// 4. 關閉管道close(fd);// 5. 刪除管道(可選)unlink("myfifo");return 0;
}
讀進程(reader.c):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>int main() {// 1. 打開管道(讀端)int fd = open("myfifo", O_RDONLY);if (fd == -1) {perror("open error");exit(1);}// 2. 接收消息char buf[1024];ssize_t len = read(fd, buf, sizeof(buf)-1);if (len > 0) {buf[len] = '\0';printf("收到消息:%s\n", buf);}// 3. 關閉管道close(fd);return 0;
}
運行步驟:
- 編譯兩個程序:
gcc writer.c -o writer
、gcc reader.c -o reader
。 - 先啟動讀進程:
./reader
(會阻塞等待寫端打開)。 - 再啟動寫進程:
./writer
(發送消息后退出)。 - 讀進程會輸出:
收到消息:來自writer的消息:你好,reader!
。
4.5 命名管道與匿名管道的區別
特性 | 匿名管道 | 命名管道 |
---|---|---|
存在形式 | 內存中,無文件名 | 有文件名(在文件系統中可見) |
適用進程 | 有血緣關系(父子、兄弟) | 任意進程(只要知道路徑) |
創建方式 | pipe() 系統調用 | mkfifo() 函數或mkfifo 命令 |
打開方式 | 繼承文件描述符 | 通過open() 函數打開路徑 |
生命周期 | 隨進程(所有進程關閉后銷毀) | 隨文件(需用unlink() 刪除) |
本質上,命名管道只是比匿名管道多了一個"文件名",其他特性(單向通信、面向字節流、同步機制)完全相同。
五、基于管道的進程池設計
5.1 什么是進程池?
在實際開發中,我們經常需要創建多個子進程處理任務(比如服務器處理多個客戶端請求)。如果每次有任務才創建子進程,會帶來很大的開銷(創建進程需要分配內存、初始化PCB等)。
進程池就是一種優化方案:提前創建一批子進程,當有任務時,直接讓空閑的子進程處理,避免頻繁創建和銷毀進程。
就像餐廳的服務員團隊:開業前招聘好服務員(創建子進程),客人來了(任務)直接安排空閑服務員接待,不用等客人來了再臨時招聘。
5.2 基于管道的進程池通信模型
進程池的核心是父進程如何給子進程分配任務。我們可以用管道實現這種通信:
- 創建進程池:父進程創建N個子進程,為每個子進程創建一個管道(父寫子讀)。
- 子進程等待任務:每個子進程阻塞在管道的讀端,等待父進程發送任務。
- 父進程分配任務:父進程有任務時,選擇一個空閑子進程,通過對應的管道發送任務數據。
- 子進程處理任務:子進程收到任務后,執行任務,完成后繼續等待下一個任務。
這種模型的優點是:
- 父進程可以精確控制每個子進程的任務(通過不同管道)。
- 子進程專注于處理任務,不需要關心任務分配邏輯。
5.3 進程池代碼實現
下面我們實現一個簡單的進程池:父進程創建3個子進程,向它們發送不同的任務(打印不同的消息)。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <vector>
#include <string>
#include <cstring>// 任務結構體(這里簡化為字符串)
struct Task {std::string msg;
};// 子進程處理任務的函數
void handle_task(int read_fd) {while (true) {// 讀取任務char buf[1024];ssize_t len = read(read_fd, buf, sizeof(buf)-1);if (len <= 0) break; // 寫端關閉,退出buf[len] = '\0';printf("子進程[%d]處理任務:%s\n", getpid(), buf);}close(read_fd);exit(0);
}int main() {const int NUM_PROCESSES = 3; // 進程池大小std::vector<int> write_fds; // 保存每個子進程對應的寫端// 創建進程池for (int i = 0; i < NUM_PROCESSES; ++i) {int fd[2];if (pipe(fd) == -1) {perror("pipe error");exit(1);}pid_t pid = fork();if (pid == -1) {perror("fork error");exit(1);}if (pid == 0) { // 子進程close(fd[1]); // 關閉寫端handle_task(fd[0]);} else { // 父進程close(fd[0]); // 關閉讀端write_fds.push_back(fd[1]); // 保存寫端}}// 向子進程發送任務std::vector<Task> tasks = {{"任務1:打印日志"},{"任務2:處理數據"},{"任務3:發送網絡請求"},{"任務4:更新緩存"},{"任務5:生成報表"}};for (size_t i = 0; i < tasks.size(); ++i) {int fd = write_fds[i % NUM_PROCESSES]; // 輪詢分配任務write(fd, tasks[i].msg.c_str(), tasks[i].msg.size());sleep(1); // 間隔1秒發送}// 關閉所有寫端(觸發子進程退出)for (int fd : write_fds) {close(fd);}// 等待所有子進程退出for (int i = 0; i < NUM_PROCESSES; ++i) {wait(NULL);}return 0;
}
運行后,輸出類似:
子進程[1234]處理任務:任務1:打印日志
子進程[1235]處理任務:任務2:處理數據
子進程[1236]處理任務:任務3:發送網絡請求
子進程[1234]處理任務:任務4:更新緩存
子進程[1235]處理任務:任務5:生成報表
這個例子中,父進程通過輪詢的方式給3個子進程分配5個任務,子進程處理完后繼續等待新任務,直到父進程關閉寫端才退出。
5.4 進程池的優化方向
上面的簡單實現可以進一步優化:
- 動態擴容:當任務過多時,自動創建新的子進程;任務過少時,銷毀部分子進程。
- 任務優先級:給任務設置優先級,父進程按優先級分配。
- 結果返回:子進程處理完任務后,通過另一個管道將結果返回給父進程。
- 異常處理:子進程崩潰時,父進程能檢測到并重新創建子進程。
這些優化可以讓進程池更適應實際應用場景,比如高并發的服務器程序。
六、其他IPC方式簡介
除了管道,Linux還有其他常用的IPC方式,這里簡單介紹:
6.1 消息隊列
消息隊列是內核中的一個消息鏈表,進程可以向隊列中添加消息,也可以從隊列中讀取消息。每個消息都有類型,讀取時可以按類型篩選。
優點:可以實現雙向通信,消息有邊界(不需要自己定義協議)。
缺點:消息大小和隊列長度有限制,效率不如共享內存。
6.2 信號量
信號量不是用于傳遞數據,而是用于實現進程間的同步和互斥(比如控制多個進程對共享資源的訪問)。
比如,信號量可以比作停車場的車位計數器:進程要進入臨界區(停車場),需要先獲取信號量(車位);離開時釋放信號量(騰出車位)。
6.3 共享內存
共享內存是效率最高的IPC方式:操作系統在內存中開辟一塊區域,讓多個進程直接映射到自己的地址空間,進程可以直接讀寫這塊內存,不需要內核中轉。
優點:數據不需要拷貝,速度極快。
缺點:需要自己處理同步問題(比如用信號量防止同時寫入)。
七、總結與展望
進程間通信是操作系統中非常重要的概念,而管道作為最基礎的IPC方式,雖然簡單但應用廣泛。通過本文的學習,你應該掌握:
- 進程間通信的必要性和成本來源。
- 匿名管道的原理、實現和特性(單向通信、血緣關系限制)。
- 命名管道如何解決匿名管道的局限性,讓無關進程通信。
- 基于管道的進程池設計,理解如何高效管理多個子進程。
管道雖然好用,但在高并發、大數據量的場景下,可能需要更高效的方式(如共享內存)。下一篇文章,我們將深入探討共享內存的實現原理和使用技巧,敬請期待!
7.1、為什么管道的緩沖區大小是固定的?動態調整緩沖區大小有什么問題?
管道的緩沖區大小被設計為固定值(通常為64KB,不同內核版本可能略有差異),核心原因是簡化操作系統對管道的管理,并保證通信的穩定性和效率。具體來說:
-
固定大小便于內核管理
管道的緩沖區是內核維護的一塊連續內存。固定大小可以讓內核提前分配內存、設置邊界,避免頻繁的動態內存申請/釋放(比如用kmalloc
或vmalloc
)。動態調整需要內核實時計算所需空間、處理內存碎片,會增加系統開銷,降低通信效率。 -
避免進程通信的不確定性
如果緩沖區大小動態變化,進程無法預判寫入/讀取的邊界。比如,寫進程可能以為緩沖區足夠大而持續寫入,導致內存耗盡;讀進程也無法確定何時能讀完數據,容易引發阻塞或數據截斷。固定大小能讓進程明確通信的“上限”,便于設計可靠的讀寫邏輯。
動態調整緩沖區大小的主要問題:
- 同步復雜:緩沖區擴容/縮容時,正在進行的讀寫操作可能被打斷,需要內核額外加鎖保護,增加死鎖風險。
- 效率下降:動態內存分配(尤其是大內存)耗時較長,且可能因內存碎片導致分配失敗,影響管道的實時性。
- 接口不統一:用戶進程無法提前知曉緩沖區大小,難以設計兼容不同內核版本的代碼(不同系統動態調整策略可能不同)。
7.2、如何用兩個管道實現父子進程的雙向通信?
管道是單向通信的(“半雙工”),但通過創建兩個管道,可以讓父子進程實現雙向通信。核心思路是:
- 管道1:父進程寫,子進程讀(父→子方向)。
- 管道2:子進程寫,父進程讀(子→父方向)。
具體步驟(代碼示例):
-
創建兩個管道
用pipe()
創建兩個管道pipe1
和pipe2
,分別對應兩個方向的通信信道。int pipe1[2], pipe2[2]; pipe(pipe1); // pipe1[0]:讀端;pipe1[1]:寫端(父→子) pipe(pipe2); // pipe2[0]:讀端;pipe2[1]:寫端(子→父)
-
創建子進程并關閉無關端口
父子進程通過fork()
繼承管道的文件描述符后,需關閉不需要的端口,避免干擾:- 父進程:關閉
pipe1
的讀端(pipe1[0]
)和pipe2
的寫端(pipe2[1]
),保留pipe1[1]
(寫)和pipe2[0]
(讀)。 - 子進程:關閉
pipe1
的寫端(pipe1[1]
)和pipe2
的讀端(pipe2[0]
),保留pipe1[0]
(讀)和pipe2[1]
(寫)。
- 父進程:關閉
-
雙向通信
- 父進程通過
write(pipe1[1], ...)
向子進程發送數據,子進程通過read(pipe1[0], ...)
接收。 - 子進程通過
write(pipe2[1], ...)
向父進程回復數據,父進程通過read(pipe2[0], ...)
接收。
- 父進程通過
代碼片段示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main() {int pipe1[2], pipe2[2];pipe(pipe1); // 父→子pipe(pipe2); // 子→父pid_t pid = fork();if (pid == 0) { // 子進程close(pipe1[1]); // 關閉pipe1寫端close(pipe2[0]); // 關閉pipe2讀端// 接收父進程數據char buf[100];read(pipe1[0], buf, sizeof(buf));printf("子進程收到:%s\n", buf);// 向父進程回復const char* reply = "子進程已收到!";write(pipe2[1], reply, strlen(reply));close(pipe1[0]);close(pipe2[1]);} else { // 父進程close(pipe1[0]); // 關閉pipe1讀端close(pipe2[1]); // 關閉pipe2寫端// 向子進程發送數據const char* msg = "父進程:你好!";write(pipe1[1], msg, strlen(msg));// 接收子進程回復char buf[100];read(pipe2[0], buf, sizeof(buf));printf("父進程收到:%s\n", buf);close(pipe1[1]);close(pipe2[0]);}return 0;
}
運行后輸出:
子進程收到:父進程:你好!
父進程收到:子進程已收到!
7.3、命名管道在文件系統中可見,但數據不寫入磁盤,這是如何實現的?
命名管道(FIFO)在文件系統中可見(有路徑和文件名),但數據不寫入磁盤,核心原因是它本質是“內存級文件”,文件系統中的條目僅作為“標識”,不存儲實際數據。具體實現如下:
-
文件系統中的“標識”作用
命名管道通過mkfifo
創建時,內核會在文件系統中創建一個特殊的inode(索引節點),記錄管道的路徑、權限、創建者等元信息,但不分配磁盤數據塊。這個inode的作用是讓所有進程通過路徑找到同一個管道(類似“地址牌”),而非存儲數據。 -
數據存儲在內存緩沖區
命名管道的實際數據存儲在內核維護的內存緩沖區中(和匿名管道一樣)。當進程通過open
打開命名管道時,內核會將管道的內存緩沖區映射到進程的文件描述符表中,進程的read
/write
操作實際是讀寫這塊內存,而非磁盤。 -
不寫入磁盤的原因
命名管道設計的核心是“進程間臨時通信”,數據無需持久化。如果寫入磁盤,會帶來額外的I/O開銷(磁盤速度遠慢于內存),且通信結束后數據無用,反而浪費磁盤空間。內核通過將數據限制在內存中,既保證了通信效率,又避免了不必要的磁盤操作。
簡單說:命名管道在文件系統中的“可見性”只是為了讓進程找到它,而實際數據始終在內存中流轉,用完即棄,不會落地到磁盤。
歡迎在評論區留下你的答案和疑問,我們一起討論!