Linux系統程式設計之匿名管道

烏有先生ii發表於2021-12-01

1.程式間通訊介紹

1.1 程式通訊的基本概念

在之前我們已經學習過程式地址空間。Linux 環境下,程式地址空間相互獨立,每個程式各自有不同的使用者地址空間。任何一個程式的全域性變數在另一個程式中都看不到,所以程式和程式之間不能相互訪問,要交換資料必須通過核心,在核心中開闢一塊緩衝區,程式1把資料從使用者空間拷到核心緩衝區,程式2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程式間通訊(IPC,Inter Process Communication)。

image-20210828230200713

1.2 為什麼要程式間通訊

程式通訊主要有以下目的:

  • 資料傳輸:一個程式需要將它的資料傳送給另一個程式。
  • 資源共享:多個程式之間共享同樣的資源。
  • 通知事件:一個程式需要向另一個或一組程式傳送訊息,通知它(它們)發生了某種事件(如程式終止時要通知父程式)。
  • 程式控制:有些程式希望完全控制另一個程式的執行(如Debug程式),此時控制程式希望能夠攔截另一個程式的所有陷入和異常,並能夠及時知道它的狀態改變。

1.3 常見的程式通訊方式

在程式間完成資料傳遞需要藉助作業系統提供特殊的方法,如今常見的程式間通訊方式有:

① 管道 (分為匿名管道與命名管道)
​ ② 訊號 (開銷最小)
​ ③ 共享記憶體

2.管道

2.1管道簡介

管道是Unix中最古老的程式間通訊方式,我們把從一個程式連線到另一個程式的資料流叫做管道。

在Linux中,| 符號被用來代表管道。因為在Linux中,不同的命令,如ps,ls,grep等命令的本質都是可執行程式,| 前面的命令前面的命令通常會輸出大量的結果,這些結果將會交由 | 後面的命令繼續處理。

如下面這個命令就是將ps axj中含有PID的結果輸出:

image-20210906114902298

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;
}

執行後:

image-20211027210811579

可以看到,此時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;
}

上面這段程式碼實現了子程式寫入管道,父程式讀出的過程。

image-20211028172104581

2.3 管道的底層機制

管道是在有血緣關係的程式之間來通訊的,如父子程式,兄弟程式等。因此,應用匿名管道時一定會有fork函式的參與。

如下面這個簡化圖可以看到,

  1. 父程式先使用pipe函式建立管道,得到兩個檔案描述符 fd[0]、fd[1]指向管道的讀端和寫端。

  2. 父程式呼叫fork建立子程式,此時父子程式有相同的struct files_struct,父子程式指向的struct file又指向了同一片檔案緩衝區。(注意:這個表述並不嚴謹,我們下面馬上就會講到)

  3. 接下來父程式關閉寫端,子程式關閉讀端,就可以實現子程式向管道中寫,父程式讀。注意:管道的通訊是單向的!!!!

img

在 Linux 中,管道的實現並沒有使用專門的資料結構,而是藉助了檔案系統的file結構和VFS的索引節點inode。通過將兩個 file struct指向同一個臨時的 inode,而這個 VFS 索引節點又指向一個物理頁面而實現的。

image-20211027211509305

如上圖所示,有兩個 file 資料結構,但它們定義檔案操作例程地址是不同的,其中一個是向管道中寫入資料的例程地址,而另一個是從管道中讀出資料的例程地址。
這樣,使用者程式的系統呼叫仍然是通常的檔案操作,而核心卻利用這種抽象機制實現了管道這一特殊操作。看待管道,就如同看待檔案一樣!管道的使用和檔案一致,迎合了“Linux一切皆檔案思想”。

2.4 管道讀寫規則

用阻塞的方式開啟管道(即預設情況下)

  1. 如果所有管道寫端對應的檔案描述符被關閉(管道寫端引用計數為 0),讀端在將管道中剩餘資料讀取後,再次read會返回0。(寫端關閉)

  2. 如果有指向管道寫端的檔案描述符沒關閉,且持有管道寫端的程式也沒有向管道中寫資料,這時有程式從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次 read 會阻塞。(讀完不寫)

  3. 如果所有指向管道讀端的檔案描述符都關閉了(管道讀端引用計數為 0),進行write操作會產生訊號SIGPIPE,進而可能導致write程式退出。(讀端關閉)

  4. 如果有指向管道讀端的檔案描述符沒關閉(管道讀端引用計數大於 0),且讀端程式並沒有向管道中讀程式,則當寫端程式寫滿後,會進入阻塞。(寫滿不讀)

2.5 管道的特點

  • 只能用於具有共同祖先的程式(具有親緣關係的程式)之間進行通訊。
  • 管道提供流式服務。
  • 管道的生命週期隨程式,程式退出,管道釋放。
  • 核心會對管道操作進行同步與互斥。
  • 管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道
  • 管道大小為65536 byte

相關文章