1. 初識進程間通信
1.1進程間通信的目的:
1、數據傳輸:一個進程需要將它的數據發送給另一個進程
2、資源共享:多個進程之間共享同樣的資源
3、通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止 時要通知父進程)4、進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另 一個進程的所有陷入和異常,并能夠及時知道它的狀態改變
1.2 為什么要有進程間通信
為了實現兩個或者多個進程實現數據層面的交互,因為進程獨立性的存在,導致進程通信的成本比較高? ?很多場景下需要多個進程協同工作來完成要求。如下:
- 這條命令首先使用?cat??讀取?log.txt?的內容,然后通過管道?(
|
) 將輸出傳遞給?grep?命令。grep?用于搜索指定的字符串。 - grep Hello
:?
?這個命令搜索包含 "Hello" 的行。
1.3進程間通信的方式
管道(通過文件系統通信)
- 匿名管道pipe
- 命名管道?
System V IPC (聚焦在本地通信)
- System V 消息隊列
- System V 共享內存
- System V 信號量
POSIX IPC (讓通信可以跨主機)
- 消息隊列
- 共享內存
- 信號量
- 互斥量
- 條件變量
- 讀寫鎖
注意:
- ?System V?標準需要重新構建操作系統代碼來實現進程通信,比較繁瑣。
- 在?System V?標準出現之前,而「管道通信」是直接復用現有操作系統的代碼
- 現在本地通信已經被網絡通信取代,所以進程間通信方式只重點介紹管道通信和共享內存通信
知識補充:
(1)進程間通信的本質:必須讓不同的進程看到同一份“資源”(資源:特定形式的內存空間)
(2)這個資源誰提供?一般是操作系統
- 為什么不是我們兩個進程中的一個呢?假設一個進程提供,這個資源屬于誰?
- 這個進程獨有,破壞進程獨立性,所以要借用第三方空間
(3)我們進程訪問這個空間,進行通信,本質就是訪問操作系統!
- 進程代表的就是用戶,資源從創建,使用(一般),釋放--系統調用接口!
2.匿名管道
2.1.什么是管道
進程可以通過 讀/寫 的方式打開同一個文件,操作系統會創建兩個不同的文件對象 file,但是文件對象 file 中的內核級緩沖區、操作方法集合等并不會額外創建,而是一個文件的文件對象的內核級緩沖區、操作方法集合等通過指針直接指向另一個文件的內核級緩沖區、操作方法集合等。
- 這樣以讀方式打開的文件和以寫方式打開的文件共用一個?內核級緩沖區
- 進程通信的前提是不同進程看到同一份共享資源
所以根據上述原理,父子進程可以看到同一份共享資源:被打開文件的內核級緩沖區。父進程向被打開文件的內核級緩沖區寫入,子進程從被打開文件的內核級緩沖區讀取,這樣就實現了進程通信!
- 這里也將被打開文件的內核級緩沖區稱為?「?管道文件」,而這種由文件系統提供公共資源的進程間通信,就叫做「?管道?」
注意:
此外,管道通信只支持單向通信,即只允許父進程傳輸數據給子進程,或者子進程傳輸數據給父進程。
- 當父進程要傳輸數據給子進程時,就可以只使用以寫方式打開的文件的管道文件,關閉以讀方式打開的文件,
- 同樣的,子進程只是用以讀方式打開的文件的管道文件,關閉掉以寫方式打開的文件。
- 父進程向以寫方式打開的文件的管道文件寫入,子進程再從以讀方式打開的文件的管道文件讀取,從而實現管道通信。如果是要子進程向父進程傳輸數據,同理即可。
管道特點總結:
- 一個進程將同一個文件打開兩次,一次以寫方式打開,另一次以讀方式打開。此時會創建兩個struct file,而文件的屬性會共用,不會額外創建
- 如果此時又創建了子進程,子進程會繼承父進程的文件描述符表,指向同一個文件,把父子進程都看到的文件,叫管道文件
管道只允許單向通信
管道里的內容不需要刷新到磁盤
2.2 創建匿名管道
匿名管道:沒有名字的文件(struct file)
匿名管道用于父子間通信,或者由一個父創建的兄弟進程(必須有“血緣“)之間進行通信
#include <unistd.h>
原型:int pipe(int fd[2]);功能:創建匿名管道
參數 fd:文件描述符數組,其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤代碼
使用如下:
int main()
{// 1. 創建管道int fds[2] = {0};int n = pipe(fds); // fds: 輸出型參數if(n != 0){std::cerr << "pipe error" << std::endl;return 1;}std::cout << "fds[0]: " << fds[0] << std::endl;std::cout << "fds[1]: " << fds[1] << std::endl;return 0;
}// 運行如下:
fds[0]: 3
fds[1]: 4
-
輸出型參數:文件的描述符數字帶出來,讓用戶使用-->3,4,因為0,1,2分別被stdin,stdout,stderr占用。
2.3 匿名管道通信案例(父子通信)
注意:匿名管道需要在創建子進程之前創建,因為只有這樣才能復制到管道的操作句柄,與具有親緣關系的進程實現訪問同一個管道通信
情況一:管道為空 && 管道正常(read 會阻塞【read 是一個系統調用】)
具體代碼演示如下:(子進程寫入,父進程讀取)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>// 父進程 -- 讀取
// 子進程 -- 寫入
void write(std::string &info, int cnt)
{info += std::to_string(getpid());info += ", cnt: ";info += std::to_string(cnt);info += ')';
}int main()
{// 1. 創建管道int fds[2] = {0};int n = pipe(fds); // fds: 輸出型參數if (n != 0){std::cerr << "pipe error" << std::endl;return 1;}// 2. 創建子進程pid_t id = fork();if (id < 0){std::cerr << "fork error" << std::endl;return 2;}else if (id == 0){// 子進程// 3. 關閉不需要的 fd, 關閉 readint cnt = 0;while (true){close(fds[0]);std::string message = "(hello linux, pid: ";write(message, cnt);::write(fds[1], message.c_str(), message.size());cnt++;sleep(2);}exit(0);}else{// 父進程// 3. 關閉不需要的 fd, 關閉 writeclose(fds[1]);char buffer[1024];while(true){ssize_t n = ::read(fds[0], buffer, 1024);if(n > 0){buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}}// 記錄退出信息pid_t rid = waitpid(id, nullptr, 0);std::cout << "father wait chile success" << rid << std::endl;}return 0;
}
從上面可以知道:
- 子進程寫入的信息是變化的信息
- 父進程打印信息的時間間隔和子進程一樣,那么子進程沒傳入信息的時候,父進程處于阻塞?--> (IPC 本質:先讓不同的進程,看到同一份資源,可以保護共享資源)
情況二:管道為滿 && 管道正常(write 會阻塞【write 是一個系統調用】)
如下對代碼做點修改(紅框內的代碼)
管道有上限,Ubuntu?-> 64 KB
如果我們讓父進程正常讀取,那么結果又是怎樣的呢?
當我們到 65536 個字節時,管道已滿,父進程讀取了管道數據,子進程會繼續進行寫入,然后進行繼續讀取,就有點數據溢出的感覺
情況三:管道寫端關閉?&& 讀端繼續(讀端讀到0,表示讀到文件結尾)
代碼修改如下:
else if (id == 0)
{int cnt = 0, total = 0;while (true){close(fds[0]);std::string message = "h";// fds[1]total += ::write(fds[1], message.c_str(), message.size());cnt++;std::cout << "total: " << total << std::endl; // 最后寫到 65536 個字節sleep(2);break; // 寫端關閉}exit(0);
}
else
{// 父進程// 3. 關閉不需要的 fd, 關閉 writeclose(fds[1]);char buffer[1024];while (true) {sleep(1);ssize_t n = ::read(fds[0], buffer, 1024);if (n > 0) {buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}else if (n == 0) {std::cout << "n: " << n << std::endl;std::cout << "child quit??? me too " << std::endl;break;}std::cout << std::endl;}pid_t rid = waitpid(id, nullptr, 0);std::cout << "father wait chile success" << rid << std::endl;
}
結論:如果寫端關閉,讀端讀完管道內部數據,再讀取就會讀取到返回值 0,表示對端關閉,也表示讀到文件結尾
情況四:管道寫端正常?&& 讀端關閉(OS 會直接殺掉寫入進程)
情況二:
如何殺死呢?
a. OS 會給 目標進程發送信號:13) SIGPIPE
b. 證明如下;
else if (id == 0)
{int cnt = 0, total = 0;while (true){close(fds[0]);std::string message = "h";// fds[1]total += ::write(fds[1], message.c_str(), message.size());cnt++;std::cout << "total: " << total << std::endl; // 最后寫到 65536 個字節sleep(2);}exit(0);
}
else
{close(fds[1]);char buffer[1024];while (true){sleep(1);ssize_t n = ::read(fds[0], buffer, 1024);if (n > 0){buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}else if (n == 0){std::cout << "n: " << n << std::endl;std::cout << "child quit??? me too " << std::endl;break;}close(fds[0]); // 讀端關閉break;std::cout << std::endl;}// 記錄退出信息int status = 0;pid_t rid = waitpid(id, &status, 0);std::cout << "father wait chile success: " << rid << " exit code: " <<((status << 8) & 0xFF) << ", exit sig: " << (status & 0x7F) << std::endl;
}
小結
🦋 管道讀寫規則
- 當沒有數據可讀時
- read 調用阻塞,即進程暫停執行,一直阻塞等待
- read 調用返回-1,errno值為EAGAIN。
- 當管道滿的時候
- write 調用阻塞,直到有進程讀走數據
- 調用返回-1,errno值為 EAGAIN
- 如果所有管道寫端對應的文件描述符被關閉,則read返回0
- 如果所有管道讀端對應的文件描述符被關閉,則write操作會產生信號
2.4 匿名管道特性
-
匿名管道:只用來進行具有血緣關系的進程之間,進行通信,常用于父子進程之間通信
-
管道文件的生命周期是隨進程的
-
管道內部,自帶進程之間同步的機制(多執行流執行代碼的時候,具有明顯的順序性)
?????4.管道文件在通信的時候,是面向字節流的。(寫的次數和讀取的次數不是一一匹配的)
-
管道的通信模式,是一種特殊的半雙工模式,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道