南通公司做網(wǎng)站百度人工客服
送給大家一句話:
人要成長,必有原因,背后的努力與積累一定數(shù)倍于普通人。所以,關(guān)鍵還在于自己。 – 楊絳
從零開始認識進程間通信
- 1 為什么要進程間通信
- 2 進程如何通信
- 3 進程通信的常見方式
- 4 管道
- 4.1 什么是管道
- 4.2 管道通信的系統(tǒng)調(diào)用
- 4.3 小試牛刀
- 5 總結(jié)
- Thanks?(・ω・)ノ謝謝閱讀!!!
- 下一篇文章見!!!
1 為什么要進程間通信
以前我們學習的過程中,進程是具有獨立性的。但有些時候需要多個進程進行協(xié)同,這時候就需要進程間的通信來保證信息的互通。
就比如學校就分有教務(wù)處 , 學生處,教研組,班主任等部分。如果學校想要組織一場考試,就通知教務(wù)處安排好考場和監(jiān)考員,告訴教研組老師需要出卷子,等教務(wù)處與教研組完成對應(yīng)工作再告知學生處和班主任,然后通知學生進行考試,班主任和學生處做好考試監(jiān)督工作。
這里面就少不了溝通交流,傳遞信息。進程工作也是這樣:進程的協(xié)同工作需要一個前提提交——通信。通信就是傳遞數(shù)據(jù),控制相關(guān)信息
2 進程如何通信
首先 , 我們知道進程是具有獨立性的,一個進程的狀態(tài)不會影響其他進程的運行。而獨立性的本質(zhì)是進程 = 內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 代碼和數(shù)據(jù)
,內(nèi)核數(shù)據(jù)結(jié)構(gòu)PCB
是獨立的,代碼和數(shù)據(jù)
是獨立的 , 進程自然就是獨立的。
所以,進程間通信的成本的成本稍微高一些,因為進程本身是獨立的,兩個進程天然是無法進行數(shù)據(jù)共享的!
可是子進程建立的時候不是會拷貝(繼承)一份父進程的數(shù)據(jù)嗎,這不是進行通信嗎???
這就要我們明確區(qū)分兩個概念:能通信與可以一直通信是不一樣的。子進程繼承的父進程數(shù)據(jù)是只讀的,而且只進行一次。而一直通信是時不時就“打個電話”。
所以進程間通信的前提是:先讓不同的進程看到同一份(操作系統(tǒng))資源(“一段內(nèi)存”)。
- 首先,一定是某一個進程先需要通信,讓OS創(chuàng)建一個共享資源
- 那么OS必須通過對應(yīng)的系統(tǒng)調(diào)用來創(chuàng)建共享資源
- OS創(chuàng)建的共享資源的不同 , 系統(tǒng)調(diào)用接口的不同----就導(dǎo)致進程間通信會有不同的種類
3 進程通信的常見方式
一般通信有以下種類:
- 管道
- 匿名管道pipe
- 命名管道
- System V IPC 標準 (早期的本地通信)
- System V 消息隊列
- System V 共享內(nèi)存
- System V 信號量
- POSIX IPC 標準(現(xiàn)代版本)
- 消息隊列
- 共享內(nèi)存
- 信號量
- 互斥量
- 條件變量
- 讀寫鎖
今天來講解管道
早期的時候,程序員們面對通信的需求時,不想再單獨設(shè)計一個通信模塊,直接復(fù)用內(nèi)核級代碼,就產(chǎn)生了管道!!!管道分為命名管道和匿名管道。
4 管道
4.1 什么是管道
【Linux】 拿下 系統(tǒng) 基礎(chǔ)文件操作!!!
【Linux】開始了解重定向
這兩篇文章了我們講解了文件的底層,知道每一個進程都有對應(yīng)的文件管理結(jié)構(gòu)體,文件管理結(jié)構(gòu)體中有管理已經(jīng)打開文件的數(shù)組。數(shù)組下標為文件描述符,指向文件結(jié)構(gòu)體,而文件結(jié)構(gòu)體又會指向文件真正屬性inode。
當我們以不同方式打開文件時,只需要在內(nèi)存中加載一份數(shù)據(jù)(通過引用計數(shù)來管理),以讀寫方式打開,便會有兩個對應(yīng)的文件結(jié)構(gòu)體。他們共同使用一份代數(shù)據(jù),那自然就使用同一個內(nèi)核級緩沖區(qū)。
那么為了要通信,不用在寫一個新的模塊,直接建立一個子進程來通信多簡單。子進程會以父進程為模版進行寫時拷貝。
進行拷貝的只有進程對應(yīng)的結(jié)構(gòu)體,因為進程具有獨立性,而文件系統(tǒng)我們可沒提過什么對立性,所以文件管理數(shù)組進行淺拷貝,同樣指向原先的文件結(jié)構(gòu)體。
這時也就理解為什么父子進程會向同一塊顯示器終端打印數(shù)據(jù)了。
也理解為什么進程會默認打開012三個標準輸入輸出:因為所有進程都是bash的子進程,而bash打開了這三個文件,所以自然就打開了!!!
子進程要主動close(0 / 1 /2)不影響父進程繼續(xù)使用顯示器文件!只有引用計數(shù)(類似硬鏈接數(shù))歸零才會清理數(shù)據(jù)
今天我們進行進程間通信的前提——先讓不同的進程看到同一份(操作系統(tǒng))資源,不就解決了嗎!!!
文件的內(nèi)存緩沖區(qū)不就是兩個進程共享的一份資源嗎!而所謂的管道文件就是這個文件緩沖區(qū)!
但是呢,管道只允許進行單向通信(父->子 或 子->父),因為管道如果允許父子進程都可以寫,就會導(dǎo)致數(shù)據(jù)紊亂!進行通信的時候,每個進程關(guān)閉不需要的文件描述符,然后通過緩沖區(qū)來單向通信。一個進程把信息寫入緩沖區(qū),另一個進程從緩沖區(qū)讀取數(shù)據(jù),不需要刷新到硬盤,直接從內(nèi)存進行操作!
有個問題:父子既然要關(guān)閉不需要的fd那為什么曾經(jīng)還要打開呢?可以不關(guān)閉嗎?
如果父進程只打開讀寫的fd,那么子進程也就只能繼承讀寫的fd,這就壞事了,總得有人寫入吧!那為什么不直接以讀寫方式打開一個fd呢?這樣肯定不可以,子進程繼承后也具有讀寫,也壞事了!
所以不關(guān)閉是為了讓子進程可以繼承下去,到時候關(guān)閉不需要的就可以了!當然也可以不關(guān)閉,只要你不亂使用,所以為了排除風險,建議直接關(guān)閉
4.2 管道通信的系統(tǒng)調(diào)用
了解了管道是什么,我們就來看看關(guān)于管道的系統(tǒng)調(diào)用是什么吧?
通過手冊我們可以看到:
PIPE(2) Linux Programmer's Manual PIPE(2)NAMEpipe, pipe2 - create pipeSYNOPSIS#include <unistd.h>/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64; see NOTES */struct fd_pair {long fd[2];};struct fd_pair pipe();/* On all other architectures */int pipe(int pipefd[2]);#define _GNU_SOURCE /* See feature_test_macros(7) */#include <fcntl.h> /* Obtain O_* constant definitions */#include <unistd.h>int pipe2(int pipefd[2], int flags);
新的pipe(int pipefd[2])
是今天的主角(Ubuntu提供了新的選擇pipe2
),其實底層就是open
。
pipefd[2]
這是一個輸出型參數(shù),把以讀方式打開的文件描述符rfd
和以寫方式打開的文件描述符wfd
記錄下來!
和open
不同的是,這個系統(tǒng)調(diào)用不需要文件路徑和文件名,所以才叫匿名管道!
那么如果我們想要雙向通信呢???
干脆建兩個管道不就行了!
那為什么要進行單向通信呢?
因為這個管道的單向通信簡單,對代碼的復(fù)用率很高!
4.3 小試牛刀
接下來我們就來寫一個demo,來試試管道接口。
首先我們來搭建一個框架:
- 建立一個管道,得到對應(yīng)的文件描述符
- 創(chuàng)建子進程,關(guān)閉對應(yīng)文件
- 我們進行子進程寫入,父進程讀取
- 等待子進程退出,避免僵尸進程出現(xiàn)!
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/wait.h>
#include<cerrno>using namespace std;void SubProcessWrite(int wfd)
{
}
void FatherProcessRead(int rfd)
{
}
int main()
{//創(chuàng)建管道int pipefd[2];//[0] -> r | [1] -> wint n = pipe(pipefd);if(n != 0){perror("創(chuàng)建管道錯誤!\n");}cout << "pipefd[0] : " << pipefd[0] << " pipefd[1] :" << pipefd[1] << endl;//創(chuàng)建子進程//關(guān)閉對應(yīng)文件pid_t id = fork();if(id == 0){//子進程 -- writeclose(pipefd[0]);SubProcessWrite(pipefd[1]);//使用完都關(guān)閉close(pipefd[1]);exit(0);}//父進程 -- readclose(pipefd[1]);FatherProcessRead(pipefd[0]);//使用完都關(guān)閉close(pipefd[0]);pid_t rid = waitpid(id ,nullptr , 0 );if(rid > 0){cout << "wait child process done!" << endl;}return 0;
}
管道本質(zhì)也是文件,我們讀寫同樣使用write和read。
我們完善一下代碼:
- 子進程每隔一秒寫入一次數(shù)據(jù)
- 父進程每隔一秒讀取一次數(shù)據(jù)
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<string>
#include<sys/wait.h>
#include<cerrno>using namespace std;string GetOtherMessage()
{static int cnt = 0;string messageid = to_string(cnt);cnt++;pid_t self_id = getpid();string stringid = std::to_string(self_id);string message = "messageid : ";message += messageid;message += "my pid :";message += stringid;return message;
}void SubProcessWrite(int wfd)
{string messages = "father,I am your soon process !";while (true){string info = messages + GetOtherMessage();//寫入管道時沒有寫入'\0',沒有必要。在讀取時添加write(wfd , info.c_str() , info.size());sleep(1);}}
#define size 1024void FatherProcessRead(int rfd)
{char inbuffer[size];while (true){//注意傳入的參數(shù) , 讀取 rfd 內(nèi)的數(shù)據(jù)到inbuffer中,返回成功讀取的個數(shù)。ssize_t n = read(rfd , inbuffer , sizeof(inbuffer) - 1);if( n > 0 ) {inbuffer[n] = '\0';//添加'\0'cout << "father get message: " << inbuffer << endl;}}
}
int main()
{//創(chuàng)建管道int pipefd[2];//[0] -> r | [1] -> wint n = pipe(pipefd);if(n != 0){perror("創(chuàng)建管道錯誤!\n");}cout << "pipefd[0] : " << pipefd[0] << " pipefd[1] :" << pipefd[1] << endl;sleep(1);//創(chuàng)建子進程//關(guān)閉對應(yīng)文件pid_t id = fork();if(id == 0){cout << "子進程關(guān)閉不需要的fd , 準備開始發(fā)消息" << endl;sleep(1);//子進程 -- writeclose(pipefd[0]);SubProcessWrite(pipefd[1]);//使用完都關(guān)閉close(pipefd[1]);exit(0);}cout << "子進程關(guān)閉不需要的fd , 準備開始接收消息" << endl;sleep(1);//父進程 -- readclose(pipefd[1]);FatherProcessRead(pipefd[0]);//使用完都關(guān)閉close(pipefd[0]);pid_t rid = waitpid(id ,nullptr , 0 );if(rid > 0){cout << "wait child process done!" << endl;}return 0;
}
運行看看:
這樣驗證管道通信的可行性。
5 總結(jié)
管道的4種情況
- 如果管道內(nèi)部是空的 && write fd沒有關(guān)閉:
讀取條件不具備,讀取進程會被阻塞 – wait 等待條件具備(寫入了數(shù)據(jù)) - 管道別寫滿 && read fd 不讀且沒有關(guān)閉 :
管道被寫滿,寫進程會被阻塞,寫條件不具備-- wait 等待條件具備(讀取走一部分數(shù)據(jù)才能繼續(xù)寫) - 管道一直在被讀 && 寫端關(guān)閉了wfd:
讀端read的返回值會讀到 0 ,表示到了文件結(jié)尾!!!所以可以在讀取的時候進行一下判斷,為0就直接退出讀取! - rfd 直接關(guān)閉 , 寫端wfd一直在寫入:
首先管道只有一對讀寫端,讀端被關(guān)閉了,那么管道就不能稱之為管道了。OS系統(tǒng)也不會做這樣的無用功,直接把broken pipe
壞的管道 進行殺掉!會發(fā)送對應(yīng)的13號信號SIGPIPE
:
我們可以總結(jié)出管道的5 種特征:
- 匿名管道:只用來進行父子進程之間,因為他們可以看到同一資源
- 同步性:管道內(nèi)部自帶進程之間的同步機制!同步是多執(zhí)行流代碼的時候,具有明顯的順序性。父子進程的讀寫一定要同步進行,不然可能會出現(xiàn)并發(fā)讀取的情況,出現(xiàn)錯誤!
- 文件的生命周期是隨進程的:當一個文件沒有進程調(diào)用的時候,就會釋放掉!
- 管道在通信的時候,是面向字節(jié)流的:write 的次數(shù)和read的次數(shù)不是一一匹配的!
我們讓子進程瘋狂的寫,父進程也一直讀。子進程每 1 s寫一次,寫入時也向標準錯誤里進行打印(為了好觀察)。父進程每5s讀一次,并打印到顯示器:
可以看到,右側(cè)的子進程,左邊是父進程。子進程寫入好幾次的數(shù)據(jù),會被父進程一次讀取一大批!!! - 管道的通信模式,是一種特殊的半雙工模式:與之對應(yīng)的是全雙工模式,即雙方交流可以同時說話。半雙工是只能一方說話,一方聆聽,不能同時說(對講機模式)。
這里提一個概念,在管道讀寫是"原子"的,每個"原子"是 4096 bytes
。只有小于這個大小,就不會在讀寫時被其他人影響。如果大于一個原子的大小,就不能保證安全了。
下一篇文章我們進行管道的實戰(zhàn)——進程池項目!