1.程式間通訊介紹
1.1 程式通訊的基本概念
在之前我們已經學習過程式地址空間。Linux 環境下,程式地址空間相互獨立,每個程式各自有不同的使用者地址空間。任何一個程式的全域性變數在另一個程式中都看不到,所以程式和程式之間不能相互訪問,要交換資料必須通過核心,在核心中開闢一塊緩衝區,程式1把資料從使用者空間拷到核心緩衝區,程式2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程式間通訊(IPC,Inter Process Communication)。
1.2 為什麼要程式間通訊
程式通訊主要有以下目的:
- 資料傳輸:一個程式需要將它的資料傳送給另一個程式。
- 資源共享:多個程式之間共享同樣的資源。
- 通知事件:一個程式需要向另一個或一組程式傳送訊息,通知它(它們)發生了某種事件(如程式終止時要通知父程式)。
- 程式控制:有些程式希望完全控制另一個程式的執行(如Debug程式),此時控制程式希望能夠攔截另一個程式的所有陷入和異常,並能夠及時知道它的狀態改變。
1.3 常見的程式通訊方式
在程式間完成資料傳遞需要藉助作業系統提供特殊的方法,如今常見的程式間通訊方式有:
① 管道 (分為匿名管道與命名管道)
② 訊號 (開銷最小)
③ 共享記憶體
2.管道
2.1管道簡介
管道是Unix中最古老的程式間通訊方式,我們把從一個程式連線到另一個程式的資料流叫做管道。
在Linux中,| 符號被用來代表管道。因為在Linux中,不同的命令,如ps,ls,grep等命令的本質都是可執行程式,| 前面的命令前面的命令通常會輸出大量的結果,這些結果將會交由 | 後面的命令繼續處理。
如下面這個命令就是將ps axj中含有PID的結果輸出:
2.2 管道的建立和應用
管道的本質是核心中一塊供不同程式進行讀寫的緩衝區,而外在的操作形式是通過檔案讀寫的方式進行。
#include <unistd.h>
功能:建立一無名管道
原型
int pipe(int fd[2]);
引數
fd:檔案描述符陣列,這是一個輸出型引數,呼叫該介面後,將會給fd[2]陣列分配兩個檔案描述符,兩個檔案描述符分別對應管道的讀寫兩端。其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤程式碼
我們先用一個簡單的例子來看一下管道的建立:
#include<iostream>
#include<unistd.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(-1==ret)
{
std::cout<<"管道建立失敗!"<<std::endl;
}
std::cout<<"fd[0]:"<<fd[0]<<std::endl<<"fd[1]:"<<fd[1]<<std::endl;
return 0;
}
執行後:
可以看到,此時fd[0]和fd[1]返回了兩個檔案描述符。這兩個檔案描述符分別分別對應管道的讀寫兩端。
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid < 0)
{
printf("fork error!");
}else if(pid == 0)
{
//child
close(fd[0]);
char str[100];
while(1)
{
printf("child:");
fgets(str, 100, stdin);
ssize_t len = strlen(str);
if(write(fd[1], str, len) != len)
{
perror("write to pipe");
exit(1);
}
memset(str, 0, len);
sleep(1);
}
}
//father
int count = 0;
close(fd[1]);
while(count < 10)
{
char str[100];
ssize_t s = read(fd[0], str, 100);
if(s < 0){
perror("read from pipe");
break;
}else{
printf("father:%s", str);
}
memset(str, 0, strlen(str));
}
return 0;
}
上面這段程式碼實現了子程式寫入管道,父程式讀出的過程。
2.3 管道的底層機制
管道是在有血緣關係的程式之間來通訊的,如父子程式,兄弟程式等。因此,應用匿名管道時一定會有fork函式的參與。
如下面這個簡化圖可以看到,
-
父程式先使用pipe函式建立管道,得到兩個檔案描述符 fd[0]、fd[1]指向管道的讀端和寫端。
-
父程式呼叫fork建立子程式,此時父子程式有相同的struct files_struct,父子程式指向的struct file又指向了同一片檔案緩衝區。(注意:這個表述並不嚴謹,我們下面馬上就會講到)
-
接下來父程式關閉寫端,子程式關閉讀端,就可以實現子程式向管道中寫,父程式讀。注意:管道的通訊是單向的!!!!
在 Linux 中,管道的實現並沒有使用專門的資料結構,而是藉助了檔案系統的file結構和VFS的索引節點inode。通過將兩個 file struct指向同一個臨時的 inode,而這個 VFS 索引節點又指向一個物理頁面而實現的。
如上圖所示,有兩個 file 資料結構,但它們定義檔案操作例程地址是不同的,其中一個是向管道中寫入資料的例程地址,而另一個是從管道中讀出資料的例程地址。
這樣,使用者程式的系統呼叫仍然是通常的檔案操作,而核心卻利用這種抽象機制實現了管道這一特殊操作。看待管道,就如同看待檔案一樣!管道的使用和檔案一致,迎合了“Linux一切皆檔案思想”。
2.4 管道讀寫規則
用阻塞的方式開啟管道(即預設情況下)
-
如果所有管道寫端對應的檔案描述符被關閉(管道寫端引用計數為 0),讀端在將管道中剩餘資料讀取後,再次read會返回0。(寫端關閉)
-
如果有指向管道寫端的檔案描述符沒關閉,且持有管道寫端的程式也沒有向管道中寫資料,這時有程式從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次 read 會阻塞。(讀完不寫)
-
如果所有指向管道讀端的檔案描述符都關閉了(管道讀端引用計數為 0),進行write操作會產生訊號SIGPIPE,進而可能導致write程式退出。(讀端關閉)
-
如果有指向管道讀端的檔案描述符沒關閉(管道讀端引用計數大於 0),且讀端程式並沒有向管道中讀程式,則當寫端程式寫滿後,會進入阻塞。(寫滿不讀)
2.5 管道的特點
- 只能用於具有共同祖先的程式(具有親緣關係的程式)之間進行通訊。
- 管道提供流式服務。
- 管道的生命週期隨程式,程式退出,管道釋放。
- 核心會對管道操作進行同步與互斥。
- 管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道
- 管道大小為65536 byte