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