Linux——匿名管道
- 什么是管道
- 匿名管道的底層原理
- 觀察匿名管道現象
- 讀寫端的幾種情況
- 寫端慢,讀端快
- 寫端快,讀端慢
- 管道的大小
- 寫端關閉,讀端一直讀
- 寫端一直寫,讀端關閉
我們之前一直用的是vim來編寫代碼,現在有了vscode這樣強大的編輯器,我們可以把我們的vim放一邊了,如果還有小伙伴還沒有配置好vscode的遠端,可以點擊這里:
https://blog.csdn.net/qq_67693066/article/details/136368891
我們今天進入管道的學習:
什么是管道
在計算機領域,管道(Pipeline)是一種將多個命令連接在一起以形成數據流的機制。它允許一個命令的輸出成為另一個命令的輸入,從而實現命令之間的數據傳遞和處理。
在 Unix/Linux 系統中,管道通常用豎線符號 | 表示。通過管道,可以將一個命令的輸出傳遞給另一個命令進行處理,從而構建復雜的數據處理流程。
例如,假設我們有兩個命令 command1 和 command2,我們可以使用管道將它們連接起來:
command1 | command2
這將會把 command1 的輸出作為 command2 的輸入,command2 將處理 command1 的輸出并生成最終的結果。
管道的優勢包括:
簡化復雜任務: 管道可以將多個簡單的命令組合成一個復雜的任務,使得任務的實現更加簡單和高效。
模塊化和可重用性: 通過將命令連接在一起,可以更好地組織代碼并提高代碼的可重用性。每個命令都可以專注于完成一個特定的任務。
減少臨時文件: 管道可以避免將數據存儲到臨時文件中,從而減少了文件 I/O 的開銷和磁盤空間的占用。
實時處理: 管道允許命令之間的實時數據傳遞,這對于需要連續處理數據的任務非常有用,比如日志處理、數據流分析等。
簡單來說,管道就是連接多個指令。我們之前也在頻繁使用管道:比如我們想統計當前登錄到系統的用戶數量。
who指令的結果作為wc -l的輸入。
匿名管道的底層原理
我們這里講的簡單一點,現在我們有一個進程,它自身會被以讀和寫的方式分別打開一次:
然后這個讀和寫都會往一個緩沖區輸入輸出數據:
這個時候父進程創建子進程,子進程發生淺拷貝,指向沒有發生變化:
這里注意一下,管道一般是單向的,所以我們現在想讓父進程讀,讓子進程寫:
這樣形成了一個單向通道,這個就是一個基本的匿名管道。
匿名管道(Anonymous Pipe)是一種用于進程間通信的機制,特別是在 Unix 和類 Unix 系統中。它允許一個進程將輸出發送到另一個進程的輸入,從而實現進程間的數據傳輸。
以下是匿名管道的一些關鍵特點:
單向通信:匿名管道是單向的,只能支持單向數據流。它只能用于單一方向的通信,通常是父進程到子進程或者相反。
創建:匿名管道通過調用系統調用 pipe() 來創建。這個系統調用創建了一個管道,返回兩個文件描述符,其中一個用于讀取管道,另一個用于寫入管道。
父子進程通信:通常,匿名管道用于父子進程之間的通信。在創建子進程后,父進程可以將數據寫入管道,而子進程則可以從管道中讀取這些數據。
半雙工:匿名管道是半雙工的,意味著數據只能在一個方向上流動。如果需要雙向通信,則需要創建兩個管道,或者使用其他的進程間通信機制,比如命名管道或套接字。
進程同步:匿名管道通常用于進程間的同步和協作。一個進程可能會阻塞在讀取管道上,直到另一個進程寫入數據到管道中為止。
匿名管道在 Unix 系統中被廣泛應用,特別是在 shell 編程和進程間通信方面。它提供了一種簡單而有效的方式,允許不同進程之間進行數據交換和協作
我也有專門創建管道的函數pipe:
我們可以來試一下:
#include<iostream>
#include<unistd.h>
#include<cassert>
using namespace std;int main()
{int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);cout<<"pipefd[0]"<<"--->"<<pipefd[0]<<"pipefd[1]"<<"--->"<<pipefd[1]<<endl;return 0;
}
運行:
這里我們發現pipefd[0]指代的是3,而我們的pipefd[1]指代的是4。其實也很好理解,因為0,1,2被標準輸入,標準輸出,標準錯誤占了。所以從3開始。
同時,如果我么查手冊會看到這樣一段話:
這段話的主要意思是pipefd[0]是讀端,而pipefd[1]是寫端。這為我們以后哪個開哪個關提供了依據。
觀察匿名管道現象
我們先搭建架子來觀察我們匿名管道的現象:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");}if(id ==0){//子進程要做的事exit(0);}//父進程要做的事//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
現在我們想讓子進程寫,父進程讀,我們把相應用不到的管道關閉:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我們讓子進程寫入一些東西,然后讓父進程來讀,看看行不行:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 10;while(cnt){//緩沖區char message[MAX];//向緩沖區里寫snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道寫write(pipefd[1],message,strlen(message));sleep(1);}exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我們看到父進程真的拿到了子進程寫的東西,這就是一個最基本的管道的應用。
讀寫端的幾種情況
寫端慢,讀端快
我們模擬一下,寫端慢,讀端快的情況
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 10;while(cnt){//緩沖區char message[MAX];//向緩沖區里寫snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道寫write(pipefd[1],message,strlen(message));sleep(100); //模擬寫端慢}exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我們發現父進程處于一個休眠的狀態,很明顯,它是在等待我們的子進程進行寫入。
這里我們可以得出匿名管道具有同步機制,讀端和寫端是協同工作的。
寫端快,讀端慢
我們調換一下,讓寫端快,讀端快:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 10000;while(cnt){//緩沖區char message[MAX];//向緩沖區里寫snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道寫write(pipefd[1],message,strlen(message));cout<<"writing......"<<endl;}exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
執行:
過了2秒之后:
數據一瞬間出來了。
這里我們可以得出匿名管道是面向字節流的,它沒有硬性規定我寫一條你必須馬上讀一條,而是以字節流的形式讀或寫。
管道的大小
我們可以寫一段代碼來測試我們管道的大小:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 0;while(1){// //緩沖區// char message[MAX];// //向緩沖區里寫// snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);// cnt--;// //向管道寫// write(pipefd[1],message,strlen(message));// cout<<"writing......"<<endl;char c = 'a';write(pipefd[1], &c, 1);cnt++;cout << "write ....: " << cnt << endl;}exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){// sleep(2); //睡眠2秒// ssize_t n = read(pipefd[0],buffer,sizeof(buffer));// if(n > 0)// {// cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;// }}//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我們發現最后結果是65536,折合下來也就是64kb左右的大小。
我們也可以用指令來查看管道大小:ulimit -a:
我們查看的管道大小為512 * 8 = 4kb,好像比我們看到的小。這個其實不是真正的大小。
寫端關閉,讀端一直讀
我們現在讓寫段寫一段時間后直接關閉,但是讀端沒有關閉:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 0;while(1){//緩沖區char message[MAX];//向緩沖區里寫snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt++;//向管道寫write(pipefd[1],message,strlen(message));//跳出if(cnt > 3) break;// char c = 'a';// write(pipefd[1], &c, 1);// cnt++;// cout << "write ....: " << cnt << endl;}//關閉寫端close(pipefd[1]);exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){//sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}cout<<"father return value:"<< n << endl;sleep(1);}//回收子進程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
這樣表示:寫端關閉,讀端一直讀取, 讀端會讀到read返回值為0, 表示讀到文件結尾。
同時注意,進程退出,管道自動關閉。
寫端一直寫,讀端關閉
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//創建子進程pid_t id = fork();//子進程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子進程要做的事close(pipefd[0]); //關閉讀的通道//向管道寫入int cnt = 0;while(true){//緩沖區char message[MAX];//向緩沖區里寫snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt++;//向管道寫write(pipefd[1],message,strlen(message));sleep(1);//跳出//if(cnt > 3) break;// char c = 'a';// write(pipefd[1], &c, 1);// cnt++;// cout << "write ....: " << cnt << endl;sleep(1);}//關閉寫端//close(pipefd[1]);exit(0);}//父進程要做的事close(pipefd[1]); //關閉寫的通道//從管道中讀取數據char buffer[MAX];while(true){//sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}cout<<"father return value:"<< n << endl;sleep(1);//直接跳出break;}//關閉讀端close(pipefd[0]);sleep(5);int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id){cout << "wait success, child exit sig: " << (status&0x7F) << endl;}// //回收子進程// pid_t rid = waitpid(id,nullptr,0);// if(rid == id)// {// cout<<"wait success"<<endl;// }return 0;
}
我們得到一下它的信號:
我們查一下13號信號:
13號信號是:SIGPIPE:
SIGPIPE 是在進程向一個已經被關閉的管道(或者其他的類似的通信方式)寫入數據時,內核向該進程發送的信號。這個信號的默認行為是終止進程。
常見的場景是,一個進程向另一個進程通過管道發送數據,但接收數據的進程提前退出,導致寫入數據的進程嘗試往已關閉的管道寫入數據。在這種情況下,內核會發送 SIGPIPE 信號給寫入數據的進程,通知它目標進程已經退出,不再接收數據。
所以我們才有上述現象。
總結一下管道有4種情況:
管道的4種情況
- 正常情況,如果管道沒有數據了,讀端必須等待,直到有數據為止(寫端寫入數據了)
- 正常情況,如果管道被寫滿了,寫端必須等待,直到有空間為止(讀端讀走數據)
- 寫端關閉,讀端一直讀取, 讀端會讀到read返回值為0, 表示讀到文件結尾
- 讀端關閉,寫端一直寫入,OS會直接殺掉寫端進程,通過想目標進程發送SIGPIPE(13)信號,終止目標進程
5種特性:
管道的5種特性
- 匿名管道,可以允許具有血緣關系的進程之間進行進程間通信,常用與父子,僅限于此
- 匿名管道,默認給讀寫端要提供同步機制 — 了解現象就行
- 面向字節流的 — 了解現象就行
- 管道的生命周期是隨進程的
- 管道是單向通信的,半雙工通信的一種特殊情況