1. 程式人生 > >fork()+pipe() --> 父子程序間通過管道通訊

fork()+pipe() --> 父子程序間通過管道通訊

1.fork()函式:建立新程序

標頭檔案:#include <unistd.h>

             #include<sys/types.h>

函式原型:pid_t forkvoid);

返回值:一個是子程序返回0,第二個是父程序的返回值大於0,錯誤返回-1

功能:建立一個新的程序。(pid_t 是一個巨集定義,其實質是int 被定義在#include<sys/types.h>中

來看一下fork之後,發生了什麼事情。

由fork建立的新程序被稱為子程序(child process)。該函式被呼叫一次,但返回兩次兩次返回的區別是子程序的返回值是

0,而父程序的返回值則是新程序(子程序)的程序 id。將子程序id返回給父程序的理由是:因為一個程序的子程序可以多於一個,沒有一個函式使一個程序可以獲得其所有子程序的程序id。對子程序來說,之所以fork返回0給它,是因為它隨時可以呼叫getpid()來獲取自己的pid;也可以呼叫getppid()來獲取父程序的id。(程序id為0的總是由交換程序使用,所以一個子程序的程序id不可能為0)。

fork之後,作業系統會複製一個與父程序完全相同的子程序。雖說是父子關係,但是在作業系統看來,他們更像兄弟關係,這2個程序共享程式碼空間,但是資料空間是互相獨立的,子程序資料空間中的內容是父程序的完整拷貝,指令指標也完全相同,子程序擁有父程序當前執行到的位置(兩程序的程式計數器pc值相同。也就是說,子程序是從fork返回處開始執行的),但有一點不同,如果fork成功,子程序中fork的返回值是0,父程序中fork的返回值是子程序的程序號,如果fork不成功,父程序會返回錯誤。

可以這樣想象,2個程序一直同時執行,而且步調一致,在fork之後,他們分別作不同的工作,也就是分岔了。

 注意:fork()函式主要是以父程序為藍本複製一個程序,其ID號和父程序的ID號不同。對於結果fork出來的子程序的父程序ID號是執行fork()函式的程序的ID號;

例如:

父程序, fork返回值是:17025, ID:17024  ,父程序ID:16879

子程序, fork返回值是:0, ID:17025  ,父程序ID:17024

使用管道有一些限制: 
兩個程序通過一個管道只能實現單向通訊,如果有時候也需要子程序寫父程序讀,就必須另開一個管道。
管道的讀寫端通過開啟的檔案描述符來傳遞,因此要通訊的兩個程序必須從它們的公共祖先那裡繼承管道檔案描述符。也就是需要通過fork傳遞檔案描述符使兩個程序都能訪問同一管道,它們才能通訊。 

使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設定O_NONBLOCK標誌): 
1. 如果所有指向管道寫端的檔案描述符都關閉了(管道寫端的引用計數等於0),而仍然有程序從管道的讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會返回0,就像讀到檔案末尾一樣。 
2. 如果有指向管道寫端的檔案描述符沒關閉(管道寫端的引用計數大於0),而持有管道寫端的程序也沒有向管道中寫資料,這時有程序從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會阻塞,直到管道中有資料可讀了才讀取資料並返回。 
3. 如果所有指向管道讀端的檔案描述符都關閉了(管道讀端的引用計數等於0),這時有程序向管道的寫端write,那麼該程序會收到訊號SIGPIPE,通常會導致程序異常終止。
4. 如果有指向管道讀端的檔案描述符沒關閉(管道讀端的引用計數大於0),而持有管道讀端的程序也沒有從管道中讀資料,這時有程序向管道寫端寫資料,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入資料並返回。 



2.pipe()函式:建立管道

 標頭檔案: #include<unistd.h>
函式原型:int pipe(int filedes[2]);
函式說明:pipe()會建立管道,並將檔案描述詞由引數filedes陣列返回。
              filedes[0]為管道里的讀取端
              filedes[1]則為管道的寫入端。
返回值: 若成功則返回零,否則返回-1,錯誤原因存於errno中。

錯誤程式碼: 

         EMFILE 程序已用完檔案描述詞最大量
         ENFILE 系統已無檔案描述詞可用。
         EFAULT 引數 filedes 陣列地址不合法。

呼叫pipe函式時在核心中開闢一塊緩衝區(稱為管道)用於通訊,它有一個讀端一個寫端,然後通過filedes引數傳出給使用者程式兩個檔案描述 符,filedes[0]指向管道的讀端,filedes[1]指向管道的寫端(很好記,就像0是標準輸入1是標準輸出一樣)。所以管道在使用者程式看起來 就像一個開啟的檔案,通過read(filedes[0]);或者write(filedes[1]);向這個檔案讀寫資料其實是在讀寫核心緩衝區。 

注意:int pipe(int filedes[2]) 中的兩個檔案描述符被強制規定filedes[0]只能指向管道的讀端,如果進行寫操作就會出現錯誤;同理filedes[1]只能指向管道的寫端,如果進行讀操作就會出現錯誤。

3.父子程序通過管道通訊

每個程序各自有不同的使用者地址空間,任何一個程序的全域性變數在另一個程序中都看不到,所以程序之間要交換資料必須通過核心,在核心中開闢一塊緩衝區,程序1把資料從使用者空間拷到核心緩衝區,程序2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程序間通訊(IPC,InterProcess Communication)。


開闢了管道之後如何實現兩個程序間的通訊呢?比如可以按下面的步驟通訊。


1. 父程序呼叫pipe開闢管道,得到兩個檔案描述符指向管道的兩端。 
2. 父程序呼叫fork建立子程序,那麼子程序也有兩個檔案描述符指向同一管道。 
3. 父程序關閉管道讀端,子程序關閉管道寫端。父程序可以往管道里寫,子程序可以從管道里讀,管道是用環形佇列實現的,資料從寫端流入從讀端流出,這樣就實現了程序間通訊。

之前一直沒明白為什麼在這裡父程序要關閉管道讀端,並且子程序要關閉管道寫端,想了很久終於想通了...,原因如下:因為上面的這個程式是要模擬父程序和子程序的管道讀寫操作,其中父程序用於向管道中寫入資料,子程序用於向管道中讀取資料,因此開始要關閉父程序的讀檔案描述符filedes[0], 以及關閉子程序的寫檔案描符filedes[1],這是為了模擬這個過程。

至於為什麼父程序關閉管道的讀檔案描述符filedes[0]後子程序還能讀取管道的資料,是因為系統維護的是一個檔案的檔案描述符表的計數,父子程序都各自有指向相同檔案的檔案描述符,當關閉一個檔案描述符時,相應計數減一,當這個計數減到0時,檔案就被關閉,因此雖然父程序關閉了其檔案描述符filedes[0],但是這個檔案的檔案描述符計數還沒等於0,所以子程序還可以讀取。也可以這麼理解,父程序和子程序都有各自的檔案描述符,因此雖然父程序中關閉了filedes[0],但是對子程序中的filedes[0]沒有影響。

檔案表中的每一項都會維護一個引用計數,標識該表項被多少個檔案描述符(fd)引用,在引用計數為0的時候,表項才會被刪除。所以呼叫close(fd)關閉子程序的檔案描述符,只會減少引用計數,但是不會使檔案表項被清除,所以父程序依舊可以訪問。

最後需要注意,在linuxpipe管道下,在寫端進行寫資料時,不需要關閉讀端的緩衝檔案(即不需要讀端的檔案描述符計數為0),但是在讀端進行讀資料時必須先關閉寫端的緩衝檔案(即寫端的檔案描述符計數為0)然後才能讀取資料。

簡單例子:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
int main(void){
  pid_t pid;
  int i=0;
  int result = -1;
  int fd[2],nbytes;
  char string[100];
  char readbuffer[80];
  int *write_fd = &fd[1];
  int *read_fd = &fd[0];
  printf("Please input data:");
  scanf("%s",string);
  result = pipe(fd);
  if(-1 == result)
   {
     perror("pipe");
     return -1;
   }
  
  pid=fork();
  if(-1 == pid) //此處為了驗證父子程序是否建立成功,如果未建立成功,則返回-1
   {
     perror("fork");
     return -1;
   }
  else if(0 == pid)
   {
     printf(“this is child %d\n”, getpid());
     close(*read_fd);
     result = write(*write_fd,string,strlen(string));
     return 0;
   }
  else
   {
           printf(“this is parent %d\n”, getpid());
     close(*write_fd);
     nbytes = read(*read_fd,readbuffer,sizeof(readbuffer)-1);
     printf("receive %d data:%s\n",nbytes,readbuffer);
   }
return 0;
}
執行結果: