文章目錄
- 前置知識
- 一、管道的原理
- 二、管道的特性
- 三、管道的接口
- 四、使用管道實現簡單的進程池
- 解決進程池的一個小問題
前置知識
一個進程在創建時,會默認打開三個文件,分別是:stdin,stdout,stderr
進程中有一個維護進程所打開的文件的文件描述對象結構體struct files_struct
該文件描述對象結構體中包含一個fd_array,文件描述符表,這個文件描述符表存儲的是對應打開的文件的文件描述對象的地址。也就是說,每一個文件都有對應的文件對象,來記錄該文件的各種屬性struct file
。而進程對應的是文件描述對象,兩者不同。
fd_array中存儲的就是struct file*
類型。
默認打開的三個文件中,stdin,stdout,stderr對應的分別是鍵盤文件,顯示器文件,顯示器文件,占用了fd_array文件描述符表中的0,1,2下標。
所以,進程再次創建文件時,會默認從3號下標開始記錄。
一、管道的原理
父進程創建管道文件時,默認打開讀端和寫端,讀端的文件fd存在3號下標中,寫端文件存在4號下標中。
子進程被創建時會繼承父進程的管理文件的對象,所以子進程的fd_array的3號和4號下標也記錄了管道文件的讀寫端。
為了保證父子進程之間的通信,假設是父進程進行讀取,子進程進行寫入。
所以需要關閉父進程的寫端,關閉子進程的讀端。
子進程進行寫入,父進程進行讀取,就能實現通信了。
問題:為什么父進程不直接把要發送給子進程的數據保存一份,子進程在創建時就會繼承這份數據了。
這種通信方式不是不可以,但只能靜態通信。
實際上,在創建管道文件時,會創建兩個文件對象,它們存儲同一個inode
,指向同一塊緩沖區,這樣就能實現子進程通過寫端的struct file
和父進程的讀端的struct file
進而看到同一個文件緩沖區,也就是讓不同的進程看到同一份資源。
所以管道通信只能進行單向通信!!!
二、管道的特性
Linux中,管道的大小一般是4096字節(4KB)
管道的本質就是內存級文件。
- 1.進程之間使用管道通信,必須具有血緣關系。常用于父子關系。
- 2.管道通信只能進行單向通信。
- 3.管道是基于文件的,而文件是隨進程的,所以管道的生命周期隨進程。
- 4.這個管道文件,沒有路徑,沒有名字,更沒有inode,因為使用該管道文件,是由操作系統創建并管理的,而父子進程之間通過該管道進行通信的原因是繼承,所以該管道就叫做匿名管道。
- 5.父子進程是會進行進程協同,同步與互斥的。我的理解是:父子進程要向管道文件中讀寫內容,就要調用write和read系統調用,而該函數會進行阻塞地等待或讀取。
-
- 由此可知,管道的讀寫中有4種情況:
-
- 1.讀寫端正常,如果管道為空,讀端就要阻塞。
-
- 2.讀寫端正常,如果管道被寫滿了,寫端就要阻塞。
-
- 3.讀端正常讀,寫端關閉,讀端就會讀到0,表明讀到了文件結尾,不會被阻塞。
-
- 4.寫端正常寫,讀端關閉,寫端不會再寫了,沒有意義了,因為沒人讀。
操作系統所做的這一切,本質就是讓不同的進程看到同一份資源。
三、管道的接口
該系統接口的參數是一個數組,數組有兩個元素,記錄的就是打開的管道文件的讀端和寫端在fd_array中的位置。
所以我們只需要傳一個數組過去即可。
如果成功返回0,失敗返回-1,且錯誤碼被設置。
所以該參數叫做輸出型參數
因為會把用戶傳進來的參數進行設置修改,所以用戶可以再次使用該參數。
使用方法:
#define SIZE 2
int pipefd[SIZE] = {0};
int n = pipe(pipefd);
這是父進程申請管道文件,父進程需要讀取,所以關閉寫端
clode(pipefd[1]);
附帶的一個函數:
printf函數我們熟悉,向顯示器中打印格式化內容。
snprintf函數是printf函數的變形,本應該向顯示器文件中打印的內容,變成向str指針指向的文件中打印size大小的格式化內容。
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,cnt);
匿名管道的測試代碼
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cerrno>#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define SIZE 2
#define NUM 1024
using namespace std;// 1.先創建管道文件
// 2.創建子進程
// 3.子進程進行寫入,父進程進行讀取//向指定文件描述符對應文件寫入
void Write(int wfd)
{string s = "Hello , i am child";char buffer[NUM];//getline(cin,buffer);pid_t self = getpid();int cnt = 5;while(cnt--){buffer[0] = 0; // 告訴讀者我的buffer當作字符串來用snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,cnt); cout << buffer << endl;write(wfd,buffer,strlen(buffer));sleep(1);}}void Read(int rfd)
{char buffer[NUM];while(true){buffer[0] = 0;ssize_t n = read(rfd,buffer,sizeof(buffer));//n是讀取到的個數if(n > 0) {buffer[n] = '\0';cout << "father-" << getpid() << "get a message from child:[" << buffer << "]#" << endl;}else if(n == 0){cout << "father read file done!" << endl;break;}else break;sleep(1);}
}int main()
{int pipefd[SIZE] = {0};int n = pipe(pipefd);//成功返回0,失敗返回-1if (n < 0) // 管道創建失敗{perror("pipefd fail");return 1;}// 管道創建成功cout << "pipefd[0] : " << pipefd[0] << " pipefd[1] : " << pipefd[1] << endl; //創建子進程pid_t id = fork();if (id < 0){perror("fork fail");return 2;}// child : writeelse if (id == 0){//關閉讀端close(pipefd[0]);//寫入Write(pipefd[1]);//寫入完成關閉寫端close(pipefd[1]);exit(1);}// father : readclose(pipefd[1]);Read(pipefd[0]);int status = 0;pid_t rid = waitpid(id,&status,0); // 阻塞等待if(rid < 0)return 3;else if(rid > 0)cout << "wait child process success!" << endl;close(pipefd[0]);return 0;
}
四、使用管道實現簡單的進程池
進程池:一個父進程通過創建多個子進程,然后將不同的任務派發給不同的進程,從而提高工作效率。
相比于接到一個任務后,再創建子進程,然后再將該任務交給子進程去做。
進程池的方法是一次創建多個子進程來待命,只要有任務,就可以立即派發,多個任務也能實現并行。
而父進程與子進程實現通信的方式就是管道通信。
進程池代碼
解決進程池的一個小問題
在父進程創建子進程時,子進程會繼承父進程的struct files_struct,所以在創建第二個子進程時,由于它繼承了父進程的信息,導致第二個子進程有能力去修改父進程與第一個子進程進行通信的管道文件。
所以在父進程不斷創建子進程的過程中,子進程的fd_array空間被占用越來越多,意味著后面的子進程能修改前面的管道文件。
解決辦法,在父進程創建第二個子進程開始,把該子進程中指向第一個管道文件的寫端全部關閉。