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

執行結果:

 


 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章