Linux中的進程間通信之匿名管道解讀
一、基本概念
我們知道多個進程之間是互相獨立的,但是有時候我們需要將一個進程的數據傳遞到另一個進程,實現數據傳輸的效果,有的時候多個進程之間要共享同樣的資源,有的時候一個進程要對其他進程發(fā)送消息,實現通知事件,還有的時候一個進程要完全控制另一個進程的執(zhí)行,實現進程控制
因為進程間相互獨立,所以進程通信是有較高成本的
進程間通信的本質就是讓不同的進程看到同一份資源,這份資源一定是由操作系統(tǒng)提供的第三方空間,不能是某個進程的,因為這樣會破壞進程獨立性,我們進程訪問第三方空間本質上就是訪問操作系統(tǒng)
一般操作系統(tǒng)會有一個獨立的通信模塊,隸屬于文件系統(tǒng),它被制定者制定了兩個標準system V 和 posix ,其中system V 是本機內部進程間的通信,分為消息隊列、共享內存、信號量,posix 是網絡進程通信,分為消息隊列、共享內存、信號量、互斥量、條件變量、讀寫鎖
在進程間通信的規(guī)則指定之前,還沒有system V 和 posix 的時候,我們是通過管道進行進程間通信的,這是一種基于文件的通信方式
二、管道
1、溫故知新
我們在之前的學習命令行的過程中學習過管道,那里的管道與這里的管道是一致的,本質上就是一個管子,在兩頭位置處有兩種處理方式,在進入管道前處理一次,在管道中的內容就是已經被處理過一次的內容,然后離開管道后再處理一次,得出的結果就是一個數據被前面的命令處理一次的結果被后面的命令處理
當時學習的時候只浮于表面,實際上管道就是起到一個傳遞數據流的作用,兩邊為兩個進程,進程A發(fā)出的信息可以通過管道到達進程B,管道本身沒有處理數據的功能,只有傳遞數據的功能
2、實現方式
我們說管道是一個基于文件的通信方式,我們來看一下我們文件管理的內容
進程中的PCB中有一個struct files_struct
指針,指向結構體files_struct
,files_struct
結構體中存在一個文件描述符指針數組,指向對應的struct file
對象,每個struct file
都有inode
描述文件屬性,file_operators
定義操作文件的函數接口,文件緩沖區(qū)緩沖文件,硬盤當中的文件如果要加載到內存中需要先加載到文件緩沖區(qū),如果我們的管道文件在硬盤上,那么IO的速度將非常慢,不利于我們進行進程間的快速通信,那什么地方既速度快又能存放文件呢?答案就是內存
我們把寫入或者讀取硬盤的IO操作去掉,將管道文件保存在緩沖區(qū),其他進程再通過文件描述符讀取緩沖區(qū)的內容,就可以實現進程間的管道通信,這里的管道文件就是匿名管道
管道文件的存放問題我們解決了,下一個問題就是其他進程怎么通過文件描述符讀取緩沖區(qū)的內容
我們知道子進程被父進程創(chuàng)建后,如果不做修改,相當于是淺拷貝,父進程的PCB復制一份,files_struct
也復制一份,那么它們就同時指向已經同一個struct file
,如果父進程fd==3
以讀方式打開管道文件,fd==4
以寫方式打開管道文件,那么子進程也一樣,然后父進程close(3)
子進程close(4)
實現父寫子讀,父進程close(4)
子進程close(3)
實現父讀子寫
因為一個文件是沒法進行讀寫交替一起的,所以匿名管道其實是一種半雙工的通信方式,即單向通信,當然我們可以通過建立多個匿名管道來實現雙向通信
管道通信常用于父子進程通信,可用于兄弟進程、爺孫進程等有"血緣"的進程進行通信
3、匿名管道
#include <unistd.h> int pipe(int pipefd[2]); //pipefd:文件描述符數組,其中pipefd[0]表示讀端,pipefd[1]表示寫端,值為對應的文件描述符 //返回值:成功返回0,失敗返回錯誤代碼
在pipe函數中,int fd[2]
是一個輸出型參數
我們來實現一個父讀子寫這樣一個管道通信
#include <iostream> #include <cstdio> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <string> #include <cstring> #include <cstdlib> #define N 2 #define NUM 1024 using namespace std; void Writer(int wfd) { //定義要發(fā)送的字符串 string s = "this is your child"; //獲取當前進程的pid pid_t myid = getpid(); int number = 0; char buffer[NUM]; while(1) { //此處相當于buffer[0] = '\0';意思是將整個數組當做字符串用并且清空字符串 buffer[0] = 0; //將字符串、pid、以及計數器number按照"%s-%d-%d"格式寫到buffer當中 snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),myid,number++); //這里傳過來的wfd為對應的文件描述符,然后將buffer中的有效內容寫到管道文件緩沖區(qū)中 write(wfd,buffer,strlen(buffer)); sleep(5); } } void Reader(int rfd) { char buffer[NUM]; while(1) { //同上 buffer[0] = 0; //將文件描述符rfd讀取的內容存儲到buffer中,并返回讀取到的字符個數n ssize_t n = read(rfd,buffer,sizeof(buffer)); //如果有內容則打印出來 if(n > 0) { buffer[n] = 0; cout << "parent get a message[" << getpid() << "]# " << buffer << endl; } //沒有內容即讀取完成 else if(n == 0) { printf("parent read file done!\n"); break; } //其他情況就是有bug了 else break; } } int main() { //pipefd用來存放輸出型參數 int pipefd[N] = {0}; //成功驗證 int n = pipe(pipefd); if(n < 0) { return 1; } //創(chuàng)建子進程 pid_t id = fork(); //錯誤情況 if(id < 0) { return 2; } //子進程執(zhí)行段,把讀寫函數打包一下,寫到一個函數里,立體分明 else if(id == 0) { //child //子進程要寫不讀,關掉pipefd[0],寫pipefd[1],寫完再關掉pipefd[1],然后退出 close(pipefd[0]); Writer(pipefd[1]); close(pipefd[1]); exit(0); } //父進程執(zhí)行段 else{ //parent //父進程要讀不寫,關掉pipefd[1],讀pipefd[0],等待子進程結束再關掉pipefd[0] close(pipefd[1]); Reader(pipefd[0]); pid_t rid = waitpid(id,NULL,0); if(rid < 0) { return 3; } close(pipefd[0]); } return 0; }
這里父進程只在子進程寫入的時候才讀取,沒有出現子進程寫一半父進程就讀取的情況,所以父子進程直接是會進行協同的,有同步和互斥性
(一)管道中的四種情況
對管道中可能出現的四種情況做說明:
- 讀寫端正常,如果管道為空,讀端就要被阻塞(上面印證)
- 讀寫端正常,如果管道被寫滿,寫端就要被阻塞(在管道特性這里印證)
- 讀端正常,寫端關閉,讀端可以讀到0,表明讀到了文件結尾,不堵塞
- 寫端正常,讀端關閉,操作系統(tǒng)會殺死正在寫入的進程,用信號
SIGPIPE
,也就是kill -13
注釋掉main函數中子進程中的Writer函數,它會讀到文件結尾并打印done信息
寫端一秒寫入一次,讀端一秒讀一次,讀端讀5秒后退出讀模式,關閉讀端,然后靜待5秒,等待子進程結束,然后打印它的退出碼和收到的信號
int main() { //...... if(id < 0) { return 2; } else if(id == 0) { //child close(pipefd[0]); Writer(pipefd[1]); close(pipefd[1]); exit(0); } else{ //parent close(pipefd[1]); Reader(pipefd[0]); close(pipefd[0]); cout << "father close read fd: " << pipefd[0] << endl; sleep(5); int status = 0; pid_t rid = waitpid(id,&status,0); if(rid < 0) { return 3; } cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl; sleep(5); cout << "parent quit" << endl; } return 0; }
(二)管道的特性
//子進程一直寫 void Writer(int wfd) { string s = "this is your child"; pid_t myid = getpid(); int number = 0; char buffer[NUM]; while(1) { //buffer[0] = '\0'; buffer[0] = 0; snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),myid,number++); write(wfd,buffer,strlen(buffer)); } } //父進程5秒讀一次數據 void Reader(int rfd) { char buffer[NUM]; while(1) { sleep(5); buffer[0] = 0; ssize_t n = read(rfd,buffer,sizeof(buffer)); if(n > 0) { buffer[n] = 0; cout << "parent get a message[" << getpid() << "]# " << buffer << endl; } else if(n == 0) { printf("parent read file done!\n"); break; } else break; } }
我們發(fā)現它的讀取是雜亂無章的,說明管道是面向字節(jié)流的,這里與前面并不矛盾,有人說這里不是沒寫完就讀取嗎,你看這個句子一段一段的,其實這里是緩沖區(qū)寫滿了,寫不下了,寫入端堵塞導致的,在讀取端讀取之后寫入端才繼續(xù)寫入,正好也印證了上面的說法
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Ubuntu 17.04系統(tǒng)下源碼編譯安裝opencv的步驟詳解
這篇文章主要給大家介紹了在Ubuntu 17.04系統(tǒng)下源碼編譯安裝opencv的相關資料,文中將一步步的步驟介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面跟著小編來一起學習學習吧。2017-08-08