1、管道
我們了解到進程是獨立的,但有時進程間需要進行通信。那么,如何實現(xiàn)進程間的通信呢?
進程間通過文件的內(nèi)核緩沖區(qū)實現(xiàn)資源共享,這個過程無需磁盤參與,因此設計了一種內(nèi)存級的文件來專門實現(xiàn)進程間通信,這種內(nèi)存級文件就是管道。管道是什么?
管道是unix中最古老的進程間通信形式,從一個進程連接到另一個進程的數(shù)據(jù)流稱為“管道”。管道的原理:
必須先打開文件,然后創(chuàng)建子進程,不能先創(chuàng)建子進程再打開文件。這個過程利用的是子進程會繼承父進程相關(guān)資源的特性。
為什么父進程在打開文件時必須以“讀寫”方式打開,不能只讀或只寫?因為父進程打開文件,創(chuàng)建子進程后,父子進程必須有一個寫,一個讀,不能兩個都讀或兩個都寫。管道不需要路徑,也就不需要名字,所以稱為匿名管道。
上面的操作只是讓父子進程看到了同一份資源,但還沒有實現(xiàn)通信。這個內(nèi)存資源由操作系統(tǒng)提供,因此進程間通信也應通過操作系統(tǒng)實現(xiàn),即調(diào)用系統(tǒng)調(diào)用。
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main(){ //1、創(chuàng)建管道 int fds[2] = {0}; int n = pipe(fds); if (n != 0) { cerr << "pipe error" << endl; return -1; } //2、創(chuàng)建子進程 pid_t id = fork(); if (id < 0) { cerr << "fork error" << endl; return -1; } else if (id == 0) { //子進程 //3、關(guān)閉不需要的fd close(fds[0]);//0是讀 int cnt = 0; while (true) { string message = "hello world, hello "; message += to_string(getpid()); message += ", "; message += to_string(cnt++); write(fds[1], message.c_str(), message.size()); sleep(1); } exit(0); } else { //父進程 close(fds[1]);//1是寫 char buffer[1024]; while (true) { ssize_t n = read(fds[0], buffer, 1024); if (n > 0) { buffer[n] = 0; cout << "father, message: " << buffer << endl; } } } return 0; }
子進程寫,父進程讀。看待父子進程就像看待文件一樣。在上面子進程sleep的過程中,父進程在做什么呢?在阻塞等待。父進程在讀完子進程的數(shù)據(jù)后,操作系統(tǒng)就不讓父進程讀了,讓其進入阻塞狀態(tài),等待子進程再次寫入。這是為了保護共享資源,防止子進程寫了一半父進程就讀,或者父進程讀了一半子進程就寫。這個過程是管道內(nèi)部自動完成的。
現(xiàn)象:
管道為空且管道正常,read會阻塞(read是一個系統(tǒng)調(diào)用)。管道為滿(管道資源是有限的)且管道正常,write會阻塞。管道寫端關(guān)閉且讀端繼續(xù),讀端讀到0,表示讀到文件結(jié)尾。管道讀端關(guān)閉且寫端繼續(xù),操作系統(tǒng)會終止寫入的進程。
特性:
面向字節(jié)流。不關(guān)心對面是如何寫的,按需讀取。用來進行具有血緣關(guān)系的進程進行IPC,常用于父子進程。文件的生命周期隨進程,管道也是。單向數(shù)據(jù)通信。管道自帶同步互斥等保護機制。
2、進程池退出
當關(guān)閉寫端,讀端讀到0,表示讀到文件結(jié)尾,則結(jié)束進程。即將父進程所有的讀端關(guān)閉,則相應的子進程就會結(jié)束,最后再由父進程等待回收。
void CleanProcessPool(){ //virsion1 for (auto &c : _channels) { c.Close(); } for (auto &c : _channels) { pid_t rid = waitpid(c.GetId(), nullptr, 0); if (rid > 0) { cout << "child " << rid << " exit" << endl; } } }
為什么要分開關(guān)閉讀端和等待子進程,不能關(guān)一個等一個嗎?
根據(jù)上面的分析,所有的子進程的file_struct都會指向第一個管道,越往后的子進程指向的管道越多。所以我們只是把master的file_struct中指向管道關(guān)閉,這個管道還有其他子進程的file_struct指向,因此讀端不會讀到0,子進程不會退出,就會一直阻塞。解決這個問題有兩種辦法:
1、倒著關(guān)閉 因為通過分析可知,越早創(chuàng)建的管道指向越多,最后一個管道只被指向一次,只要將最后一個進程關(guān)閉,則前面的所有管道被指向都會少1,因此倒著關(guān)閉就不會出現(xiàn)阻塞的問題。
//virsion2 for (int i = _channels.size()-1; i >= 0; i--) { _channels[i].Close(); pid_t rid = waitpid(_channels[i].GetId(), nullptr, 0); if (rid > 0) { cout << "child " << rid << " exit" << endl; } }
2、在子進程中關(guān)閉所有歷史fd 因為父進程的3號文件描述符總為空,子進程只有3號文件描述符指向管道。在這之前子進程繼承父進程對之前的管道的指向,所以只需要在子進程中把這些指向全部關(guān)掉就行。
// 3、建立通信信道 if (id == 0) { //關(guān)閉歷史fd for (auto &c : _channels) { c.Close(); } // 子進程 //close(pipefd[1]); //dup2(pipefd[0], 0); // 子進程從標準輸入讀取 //_work(); //exit(0); }
3、命名管道
我們知道,匿名管道的原理是讓父子進程看到同一份資源,而父子進程看到同一份資源,是因為子進程繼承了父進程的資源。所以不難得出,匿名管道兩端必須是父子進程。而如果我們想在任意進程之間建立管道呢?首先可以肯定的是這任意兩個進程之間也要能看到同一份資源,因為是任意進程之間,所以這個資源不能繼承而來,因此就牽扯出了命名管道。
命名管道的原理:為什么叫做命名管道,因為有名字,是真實存在的文件,既然是真實存在的文件,就一定有路徑+文件名,而路徑+文件名具有唯一性。這樣不同的進程可以用同一個文件系統(tǒng)路徑標志同一個資源,也就是不同的進程看到了同一個資源。命名管道和普通文件的區(qū)別:這么看來命名管道和普通文件好像除了創(chuàng)建方式不同外也沒多大區(qū)別,而普通文件好像也能實現(xiàn)進程間通信,但是普通文件有兩個問題,我們往普通文件中寫入的數(shù)據(jù)會被刷新到磁盤中保存,另外普通文件也沒有被特殊保護,也就是我們可以往里寫大量的數(shù)據(jù),在寫的過程中也有可能被其他進程讀,這兩個問題是命名管道需要重點處理的,所以命名管道和普通文件有很大的區(qū)別,是特殊設計的。這個命名管道,該由誰創(chuàng)建?公共資源:一般要讓指定的一個進程現(xiàn)行創(chuàng)建。一個進程創(chuàng)建&&使用,另一個進程獲取&&使用。
4、共享內(nèi)存
共享內(nèi)存區(qū)是最快的IPC形式。一旦這樣的內(nèi)存映射到共享它的進程的地址空間,這些進程間數(shù)據(jù)傳遞不再涉及到內(nèi)核,換句話說是進程不再通過執(zhí)行進入內(nèi)核的系統(tǒng)調(diào)用來傳遞彼此的數(shù)據(jù)。
共享內(nèi)存 = 共享內(nèi)存的內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 內(nèi)存塊。讓兩個進程通過各自的虛擬地址空間,映射同一塊物理內(nèi)存,叫做共享內(nèi)存。共享內(nèi)存的本質(zhì)還是讓不同的進程看到同一個資源。
IPC_CEEAT:單獨使用,如果shm不存在則創(chuàng)建,如果存在則獲取。保證調(diào)用進程就能拿到共享內(nèi)存。IPC_CEEAT | IPC_EXCL:組合使用,如果不存在則創(chuàng)建,如果存在則返回錯誤。只要成功,一定是新的共享內(nèi)存。key為什么必須要用戶傳入,為什么內(nèi)核自己不生成?
任意進程間是獨立的,由某一個進程內(nèi)生成key,其他的進程是拿不到的。理論上用戶可以隨意設置key,只要保證不沖突就可,為了保證key的唯一性有函數(shù)來減小沖突的概率。
定義全局的key,讓進程間通過絕對路徑都能看到,由某個進程設置進內(nèi)核中,則其他進程也能夠得到。所以在應用層面,不同進程看到同一份共享內(nèi)存是通過唯一路徑+項目ID來確定的,類似命名管道也是通過文件路徑+文件名來確定的。
在OS看來,由shmget函數(shù)創(chuàng)建的共享內(nèi)存是OS創(chuàng)建的,所以共享內(nèi)存的生命周期隨內(nèi)核。和文件不同,文件的生命周期隨進程。所以共享內(nèi)存一旦創(chuàng)建出來,要么由用戶主動釋放,要么OS重啟。
共享內(nèi)存的管理指令:
ipcs -m:查看共享內(nèi)存信息ipcrm -m shmid:刪除共享內(nèi)存shmid VS key:
shmid:僅供用戶使用的shm標識符(類似文件描述符fd)key:僅供內(nèi)核區(qū)分不同shm唯一性的標識符(類似文件地址)除了指令刪除shm,還可以通過函數(shù)刪除:
共享內(nèi)存也有權(quán)限。
| 共享內(nèi)存的特點:
不需要調(diào)用系統(tǒng)調(diào)用,通信速度快。讓兩個進程在各自的用戶空間共享內(nèi)存塊,是真正的共享資源,但是不像管道,共享內(nèi)存沒有任何保護。共享內(nèi)存的保護機制,需要用戶自己完成。
本篇文章的分享就到這里了,如果您覺得在本文有所收獲,還請留下您的三連支持哦~