黃山網(wǎng)站建設(shè)推廣網(wǎng)絡(luò)輿情監(jiān)測(cè)系統(tǒng)
🌈歡迎來(lái)到Linux專(zhuān)欄~~進(jìn)程通信
- (???(??? )🐣,我是Scort
- 目前狀態(tài):大三非科班啃C++中
- 🌍博客主頁(yè):張小姐的貓~江湖背景
- 快上車(chē)🚘,握好方向盤(pán)跟我有一起打天下嘞!
- 送給自己的一句雞湯🤔:
- 🔥真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心
- 作者水平很有限,如果發(fā)現(xiàn)錯(cuò)誤,可在評(píng)論區(qū)指正,感謝🙏
- 🎉🎉歡迎持續(xù)關(guān)注!
文章目錄
- 🌈歡迎來(lái)到Linux專(zhuān)欄~~進(jìn)程通信
- 一. 進(jìn)程間通信介紹
- 二. 管道
- 🌍匿名管道
- 😎匿名管道原理
- 😎創(chuàng)建匿名管道pipe
- 😎demo代碼
- 😎匿名管道通信的4種情況
- ?讀阻塞:寫(xiě)快,讀慢
- ?寫(xiě)阻塞:寫(xiě)慢,讀快
- ?寫(xiě)端關(guān)閉
- ?讀端關(guān)閉
- 😎管道的大小
- 🌍命名管道
- 🎨創(chuàng)建命名管道
- 🎨基于命名管道通信
- 🌍 pipe vs fifo
- 三. System V標(biāo)準(zhǔn)下的進(jìn)程間通信方式
- 🌈共享內(nèi)存
- 💦共享內(nèi)存的建立
- 💛 創(chuàng)建共享內(nèi)存
- 💛 控制共享內(nèi)存
- 💛 掛接和去關(guān)聯(lián)
- 💛 shmid 和 key
- 💦共享內(nèi)存的進(jìn)程間通信
- 💦共享內(nèi)存與管道進(jìn)行對(duì)比
- 💦共享內(nèi)存歸屬誰(shuí)
- 💦共享內(nèi)存的特征
- 🌈消息隊(duì)列(了解)
- 📢寫(xiě)在最后
一. 進(jìn)程間通信介紹
進(jìn)程之間會(huì)存在特定的協(xié)同工作的場(chǎng)景:
- 數(shù)據(jù)傳輸:一個(gè)進(jìn)程要把自己的數(shù)據(jù)交給另一個(gè)進(jìn)程,讓其繼續(xù)進(jìn)行處理
- 資源共享:多個(gè)進(jìn)程之間共享同樣的資源。
- 通知事件:一個(gè)進(jìn)程需要向另一個(gè)或一組進(jìn)程發(fā)送消息,通知它(它們)發(fā)生了某種事件(如進(jìn)程終止時(shí)要通知父進(jìn)程)。
- 進(jìn)程控制:有些進(jìn)程希望完全控制另一個(gè)進(jìn)程的執(zhí)行(如Debug進(jìn)程),此時(shí)控制進(jìn)程希望能夠攔截另一個(gè)進(jìn)程的所有陷入和異常,并能夠及時(shí)知道它的狀態(tài)改變
進(jìn)程間通信的本質(zhì)就是,讓不同的進(jìn)程看到同一份資源
進(jìn)程是具有獨(dú)立性的。虛擬地址空間+頁(yè)表 保證了進(jìn)程運(yùn)行的獨(dú)立性(進(jìn)程內(nèi)核數(shù)據(jù)結(jié)構(gòu)+進(jìn)程代碼和數(shù)據(jù))
進(jìn)程通信的前提,首先需要讓不同的進(jìn)程看到同一份“內(nèi)存”(特定的結(jié)構(gòu)組織)
- 這塊內(nèi)存應(yīng)該屬于誰(shuí)呢?為了維持進(jìn)程獨(dú)立性,它一定不屬于進(jìn)程A或B,它屬于操作系統(tǒng)。
綜上,進(jìn)程間通信的前提就是:由OS參與,提供一份所有通信進(jìn)程都能看到的公共資源。
進(jìn)程間通信的發(fā)展
- 管道
- 匿名管道pipe
- 命名管道pipe
- System V標(biāo)準(zhǔn) 進(jìn)程間通信
- System V 消息隊(duì)列
- System V 共享內(nèi)存
- System V 信號(hào)量
- POSIX標(biāo)準(zhǔn) 進(jìn)程間通信(多線程詳談)
- 消息隊(duì)列
- 共享內(nèi)存
- 信號(hào)量
- 互斥量
- 條件變量
- 讀寫(xiě)鎖
二. 管道
什么是管道?
- 有入口,有出口,都是單向傳輸資源的(數(shù)據(jù))
所以計(jì)算機(jī)領(lǐng)域設(shè)計(jì)者,設(shè)計(jì)了一種單向通信的方式 —— 管道
🌍匿名管道
眾所周知,父子進(jìn)程是兩個(gè)獨(dú)立進(jìn)程,父子通信也是進(jìn)程間通信的一種,基于父子間進(jìn)程通信就是匿名管道。我們首先要對(duì)匿名管道有一個(gè)宏觀的認(rèn)識(shí)
父進(jìn)程創(chuàng)建子進(jìn)程,子進(jìn)程需要以父進(jìn)程為模板創(chuàng)建自己的files_struct
,而不是與父進(jìn)程共用;但是struct file這個(gè)結(jié)構(gòu)體就不會(huì)拷貝,因?yàn)榇蜷_(kāi)文件也與創(chuàng)建進(jìn)程無(wú)關(guān)(文件的數(shù)據(jù)不用拷貝)
- 因?yàn)樽筮吺沁M(jìn)程相關(guān)數(shù)據(jù)結(jié)構(gòu),右邊是文件相關(guān)結(jié)構(gòu)
😎匿名管道原理
- 父進(jìn)程創(chuàng)建管道,對(duì)同一文件分別以讀&寫(xiě)方式打開(kāi)
-
父進(jìn)程
fork
創(chuàng)建子進(jìn)程
-
因?yàn)楣艿朗且粋€(gè)只能單向通信的信道,父子進(jìn)程需要關(guān)閉對(duì)應(yīng)讀寫(xiě)端,至于誰(shuí)關(guān)閉誰(shuí),取決于通信方向。
于是,通過(guò)子進(jìn)程繼承父進(jìn)程資源的特性,雙方進(jìn)程看到了同一份資源。
😎創(chuàng)建匿名管道pipe
pipe
誰(shuí)調(diào)用就讓以讀寫(xiě)方式打開(kāi)一個(gè)文件(內(nèi)存級(jí)文件)
#include <unistd.h>
int pipe(int pipefd[2]);
- 參數(shù)
pipefd
:輸出型參數(shù)!通過(guò)這個(gè)參數(shù)拿到兩個(gè)打開(kāi)的fd - 返回值:成功返回0;失敗返回-1
數(shù)組pipefd
用于返回兩個(gè)指向管道讀端和寫(xiě)端的文件描述符:
數(shù)組元素 | 含義 |
---|---|
pipefd[0]~嘴巴 | 管道讀端的文件描述符 |
pipefd[1] ~ 鋼筆 | 管道寫(xiě)端的文件描述符 |
此處提取查一下要用到的函數(shù)
man2
是獲得系統(tǒng)(linux內(nèi)核)調(diào)用的用法;man 3
是獲得標(biāo)準(zhǔn)庫(kù)(標(biāo)準(zhǔn)C語(yǔ)言庫(kù)、glibc)函數(shù)的文檔
//linux中用man可以查哦
#include <unistd.h>
pid_t fork(void);#include <unistd.h>
int close(int fd);#include <stdlib.h>
void exit(int status);
下面按照之前講的原理進(jìn)行逐一操作:①創(chuàng)建管道 ②父進(jìn)程創(chuàng)建子進(jìn)程 ③關(guān)閉對(duì)應(yīng)的讀寫(xiě)端,形成單向信道
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd); //失敗返回-1assert(n != -1); //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl; //3cout<< "pipefd[1]" << pipefd[1] << endl; //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);exit(0);}//父進(jìn)程//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);return 0;
}
在此基礎(chǔ)上,我們就要進(jìn)行通信了,實(shí)際上就是對(duì)某個(gè)文件進(jìn)行寫(xiě)入,因?yàn)楣艿酪彩俏募?#xff0c;下面提提前查看要用到的函數(shù)
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
- 返回寫(xiě)入的字節(jié)數(shù)
- 零表示未寫(xiě)入任何內(nèi)容,這里意味著對(duì)端進(jìn)程關(guān)閉文件描述符#include <unistd.h>
unsigned int sleep(unsigned int seconds);
😎demo代碼
簡(jiǎn)單實(shí)現(xiàn)了管道通信的demo版本:
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd); //失敗返回-1assert(n != -1); //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl; //3cout<< "pipefd[1]" << pipefd[1] << endl; //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程 - 讀//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);char buffer[1024];while(1){size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因?yàn)閞ead是系統(tǒng)調(diào)用,沒(méi)有/0,此處給加上cout<<"child get a message["<< getpid() << "] 爸爸對(duì)你說(shuō)" << buffer << endl;}}//close(pipefd[0]);exit(0);}//父進(jìn)程 - 寫(xiě)//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);string message = "我是父進(jìn)程,我正在給你發(fā)消息";int count = 0; //計(jì)算發(fā)送次數(shù)char send_buffer[1024];while(true){//3.2構(gòu)建一個(gè)變化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3寫(xiě)入write(pipefd[1], send_buffer, strlen(send_buffer));//此處strlen不能+1//3.4 故意sleepsleep(1);}pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}
此處有個(gè)問(wèn)題:為什么不定義一個(gè)全局的buffer來(lái)進(jìn)行通信呢?
- 因?yàn)橛?strong>寫(xiě)時(shí)拷貝的存在,無(wú)法更改通信!
上面的方法就是把數(shù)據(jù)交給管道,讓對(duì)方通過(guò)管道進(jìn)行讀取
😎匿名管道通信的4種情況
之前父子進(jìn)程同時(shí)向顯示器中寫(xiě)入的時(shí)候,二者會(huì)互斥 —— 缺乏訪問(wèn)控制
而對(duì)于管道進(jìn)行讀取的時(shí)候,父進(jìn)程如果寫(xiě)的慢,子進(jìn)程就會(huì)等待讀取 —— 這就是說(shuō)明管道具有訪問(wèn)控制
?讀阻塞:寫(xiě)快,讀慢
父進(jìn)程瘋狂的進(jìn)行寫(xiě)入,子進(jìn)程隔10秒才讀取,子進(jìn)程會(huì)把這10秒內(nèi)父進(jìn)程寫(xiě)入的所有數(shù)據(jù)都一次性的打印出來(lái)!
代碼如非就是在父進(jìn)程添加了打印conut,子進(jìn)程sleep(10),可以自行的在demo代碼上添加
父進(jìn)程寫(xiě)了1220次,子進(jìn)程一次就給你讀完了,讀寫(xiě)之間沒(méi)有關(guān)系,這就叫做流式的服務(wù)。
也就是管道是面向字節(jié)流的,也就是只有字節(jié)的概念,究竟讀成什么樣也無(wú)法保證,甚至可能讀出亂碼,所以父子進(jìn)程通信也是需要制定協(xié)議的,但這個(gè)我們網(wǎng)絡(luò)再細(xì)說(shuō)。。
?寫(xiě)阻塞:寫(xiě)慢,讀快
管道沒(méi)有數(shù)據(jù)的時(shí)候,讀端必須等待:父進(jìn)程每隔2秒才進(jìn)行寫(xiě)入,子進(jìn)程瘋狂的讀取
?寫(xiě)端關(guān)閉
父進(jìn)程寫(xiě)入10秒,后把寫(xiě)端fd關(guān)閉,讀端會(huì)怎么樣?
- 寫(xiě)入的一方,fd沒(méi)有關(guān)閉,如果有數(shù)據(jù)就讀,沒(méi)有數(shù)據(jù)就等
- 寫(xiě)入的一方,fd關(guān)閉了,讀取的一方,
read
會(huì)返回0
,表示讀到了文件結(jié)尾,退出讀端
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd); //失敗返回-1assert(n != -1); //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl; //3cout<< "pipefd[1]" << pipefd[1] << endl; //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程 - 讀//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);char buffer[1024*8];while(1){//sleep(10);//20秒讀一次//寫(xiě)入的一方,fd沒(méi)有關(guān)閉,如果有數(shù)據(jù)就讀,沒(méi)有數(shù)據(jù)就等//寫(xiě)入的一方,fd關(guān)閉了,讀取的一方,read會(huì)返回0,表示讀到了文件結(jié)尾size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因?yàn)閞ead是系統(tǒng)調(diào)用,沒(méi)有/0,此處給加上cout<<"child get a message["<< getpid() << "] 爸爸對(duì)你說(shuō)" << buffer << endl;}else if (s == 0){cout << "write quit(father), me quit!!!" <<endl;break;}}//close(pipefd[0]);exit(0);}//父進(jìn)程 - 寫(xiě)//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);string message = "我是父進(jìn)程,我正在給你發(fā)消息";int count = 0; //計(jì)算發(fā)送次數(shù)char send_buffer[1024*8];while(true){//3.2構(gòu)建一個(gè)變化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3寫(xiě)入write(pipefd[1], send_buffer, strlen(send_buffer));//此處strlen不能+1//3.4 故意sleepsleep(1);cout<< count <<endl;if(count == 5){cout<< "父進(jìn)程寫(xiě)端退出" << endl;break;}}close(pipefd[1]);//關(guān)閉讀端pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}
運(yùn)行結(jié)果如下:
?讀端關(guān)閉
讀端關(guān)閉,寫(xiě)端繼續(xù)寫(xiě)入,直到OS終止寫(xiě)進(jìn)程
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe創(chuàng)建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork創(chuàng)建子進(jìn)程if (id == 0){//childclose(fd[0]); //子進(jìn)程關(guān)閉讀端//子進(jìn)程向管道寫(xiě)入數(shù)據(jù)const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子進(jìn)程寫(xiě)入完畢,關(guān)閉文件exit(0);}//fatherclose(fd[1]); //父進(jìn)程關(guān)閉寫(xiě)端close(fd[0]); //父進(jìn)程直接關(guān)閉讀端(導(dǎo)致子進(jìn)程被操作系統(tǒng)殺掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); //打印子進(jìn)程收到的信號(hào)return 0;
}
運(yùn)行結(jié)果顯示,子進(jìn)程退出時(shí)收到的是13號(hào)信號(hào)
通過(guò)kill -l命令可以查看13對(duì)應(yīng)的具體信號(hào)
由此可知,當(dāng)發(fā)生情況四時(shí),操作系統(tǒng)向子進(jìn)程發(fā)送的是SIGPIPE
信號(hào)將子進(jìn)程終止的。
🐋總結(jié)上述的4中場(chǎng)景:
- 寫(xiě)快,讀慢,寫(xiě)滿(mǎn)了不能再寫(xiě)了
- 寫(xiě)慢,讀快,管道沒(méi)有數(shù)據(jù)的時(shí)候,讀端必須等待
- 寫(xiě)關(guān),讀取的一方,
read
會(huì)返回0
,表示讀到了文件結(jié)尾,退出讀端 - 讀關(guān),寫(xiě)繼續(xù)寫(xiě),OS終止寫(xiě)進(jìn)程 ——
🧐由上總結(jié)出匿名管道的5個(gè)特點(diǎn) ——
- 管道是一個(gè)單向通信的通信管道,是半雙工通信的一種特殊情況
- 管道是用來(lái)進(jìn)行具有血緣關(guān)系的進(jìn)程進(jìn)行進(jìn)程間通信 —— 常用于父子通信
- 管道具有通過(guò)讓進(jìn)程間協(xié)同,提供了訪問(wèn)控制!
- 管道是 面向字節(jié)流 —— 協(xié)議(后面詳談)
- 管道是基于文件的,管道的聲明周期是隨進(jìn)程的
😎管道的大小
管道的容量是有限的,如果管道已滿(mǎn),那么寫(xiě)端將阻塞或失敗,那么管道的最大容量是多少呢?
ps:原子性:要么做了,要么不做,沒(méi)有中間狀態(tài)
方法1 :man手冊(cè)查詢(xún)
然后我們可以使用uname -r
命令,查看自己使用的Linux版本
我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字節(jié)
方法二:自行測(cè)試
也就是如果讀端一直不讀取,寫(xiě)端又不斷的寫(xiě)入,當(dāng)管道被寫(xiě)滿(mǎn)后,寫(xiě)端進(jìn)程就會(huì)被掛起。據(jù)此,我們可以寫(xiě)出以下代碼來(lái)測(cè)試管道的最大容量。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe創(chuàng)建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork創(chuàng)建子進(jìn)程if (id == 0){//child close(fd[0]); //子進(jìn)程關(guān)閉讀端char c = 'a';int count = 0;//子進(jìn)程一直進(jìn)行寫(xiě)入,一次寫(xiě)入一個(gè)字節(jié)while (1){write(fd[1], &c, 1);count++;printf("%d\n", count); //打印當(dāng)前寫(xiě)入的字節(jié)數(shù)}close(fd[1]);exit(0);}//fatherclose(fd[1]); //父進(jìn)程關(guān)閉寫(xiě)端//父進(jìn)程不進(jìn)行讀取waitpid(id, NULL, 0);close(fd[0]);return 0;
}
寫(xiě)端進(jìn)程最多寫(xiě)65536字節(jié)的數(shù)據(jù)就被操作系統(tǒng)掛起了,也就是說(shuō),我當(dāng)前Linux版本中管道的最大容量是65536字節(jié)
🌍命名管道
為了解決匿名管道只能在父子之間通信,我們引入命名管道,可以在任意不相關(guān)進(jìn)程進(jìn)行通信
多個(gè)進(jìn)程打開(kāi)同一個(gè)文件,OS只會(huì)創(chuàng)建一個(gè)struct_file
命名管道就是一種特殊類(lèi)型的文件(可以被打開(kāi),但不會(huì)將數(shù)據(jù)刷新進(jìn)磁盤(pán)),兩個(gè)進(jìn)程通過(guò)命名管道的文件名打開(kāi)同一個(gè)管道文件,此時(shí)這兩個(gè)進(jìn)程也就看到了同一份資源,進(jìn)而就可以進(jìn)行通信了。
命名管道就是通過(guò)唯一路徑/文件名的方式定位唯一磁盤(pán)文件的
ps:命名管道和匿名管道一樣,都是內(nèi)存文件,只不過(guò)命名管道在磁盤(pán)有一個(gè)簡(jiǎn)單的映像(所以有名字),但這個(gè)映像的大小永遠(yuǎn)為0,因?yàn)槊艿篮湍涿艿蓝疾粫?huì)將通信數(shù)據(jù)刷新到磁盤(pán)當(dāng)中。
🎨創(chuàng)建命名管道
💛 make FIFOs 在命令行上創(chuàng)建命名管道
mkfifo (named pipes)
FIFO
:First In First Out 隊(duì)列呀
來(lái)個(gè)小實(shí)驗(yàn):
命令行上執(zhí)行的命令echo
和cat
都是進(jìn)程,所以這就是通過(guò)管道文件進(jìn)行的進(jìn)程間通信 ——
💛 那么如何用代碼實(shí)現(xiàn)命名管道進(jìn)程間通信的呢?
//查手冊(cè):man 3 mkfifo
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
pathname
:管道文件路徑mode
:管道文件權(quán)限- 返回值:創(chuàng)建成功返回0;創(chuàng)建失敗返回-1,并設(shè)置錯(cuò)誤碼
我touch了server.c和client.c,最終希望在server
和client
兩個(gè)進(jìn)程之間相互通信,先寫(xiě)一個(gè)Makefile ——
.PHONY:all
all:client serverclient:client.cxxg++ -o $@ $^ -std=c++11
server:server.cxxg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server
- Makefile自頂向下掃描,只會(huì)把第一個(gè)目標(biāo)文件作為最終的目標(biāo)文件。所以要一次性生成兩個(gè)可執(zhí)行程序,需要定義偽目標(biāo)
.PHONY: all
,并添加依賴(lài)關(guān)系
🎨基于命名管道通信
comm.h
我們創(chuàng)建一個(gè)共用的頭文件,這只是為了兩個(gè)程序能有看到同一個(gè)資源的能力了
#ifndef _COMM_H_ //能避免頭文件的重定義
#define _COMM_H_//hpp和.h的區(qū)別:.h里面只有聲明,沒(méi)有實(shí)現(xiàn),而.hpp里聲明實(shí)現(xiàn)都有,后者可以減少.cpp的數(shù)量#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>using namespace std;#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";#endif
server.c
- 創(chuàng)建命名管道
- 讀信息,并實(shí)現(xiàn)相應(yīng)業(yè)務(wù)邏輯
#include "comm.hpp"int main()
{//1.創(chuàng)建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//3.編寫(xiě)正常的通信代碼char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "client say >" << buffer << endl;}else if(s == 0){//說(shuō)明寫(xiě)端關(guān)閉了cerr << "read end of file, client quit, server quit too" <<endl;}else{//讀取失敗perror("read");break;}}//4. 關(guān)閉文件close(fd);unlink(ipcPath.c_str());//通信完畢,刪除文件return 0;
}
client.c
此時(shí)不需要再創(chuàng)建命名管道,只需要獲取已打開(kāi)的命名管道文件
- 從鍵盤(pán)拿到了待發(fā)送數(shù)據(jù)
- 發(fā)送數(shù)據(jù),也就是向管道中寫(xiě)入
#include "comm.hpp"int main()
{//不需要?jiǎng)?chuàng)建fifo,只需獲取即可int fd = open(ipcPath.c_str(), O_WRONLY);if(fd < 0){perror("open");exit(1);}//2.ipc通信string buffer;while(1){cout << "Place Enter Message:";std::getline(std::cin, buffer);write(fd, buffer.c_str(), sizeof(buffer));}//3.關(guān)閉close(fd);return 0;
}
效果展示:
一定要先運(yùn)行服務(wù)端server
創(chuàng)建命名管道,再運(yùn)行客戶(hù)端,實(shí)現(xiàn)了不相關(guān)進(jìn)程通信 ——
如果我想讓多個(gè)子進(jìn)程來(lái)執(zhí)行打印任務(wù)
當(dāng)然我們就要調(diào)整一下server.c
的業(yè)務(wù)邏輯:
#include "comm.hpp"
#include <sys/wait.h>static void getMessage(int fd)
{//3.編寫(xiě)正常的通信代碼char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "[" << getpid() << "] " << "client say >" << buffer << endl;}else if(s == 0){//說(shuō)明寫(xiě)端關(guān)閉了cerr << "[" << getpid() << "] " << "read end of file, client quit, server quit too" <<endl;}else{//讀取失敗perror("read");break;}}
}int main()
{//1.創(chuàng)建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//log("創(chuàng)建管道文件成功", Debug) << "step 1" <<endl;//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//log("打開(kāi)管道文件成功", Debug) << "step 2" <<endl;int nums = 3;for(int i = 0; i < nums; i++){pid_t id = fork();if(id==0){//子進(jìn)程getMessage(fd);exit(2);}}for(int i = 0; i < nums; i++){waitpid(-1, nullptr, 0);}//4. 關(guān)閉文件close(fd);//log("關(guān)閉管道文件成功", Debug) << "step 3" <<endl;unlink(ipcPath.c_str());//通信完畢,刪除文件//log("刪除管道文件成功", Debug) << "step 4" <<endl;return 0;
}
🌍 pipe vs fifo
為什么pipe叫做匿名管道和和fifo叫做命名管道?
- 匿名管道文件屬于內(nèi)存級(jí)的文件,不需要名字,因?yàn)樗峭ㄟ^(guò)父子繼承的方式看到同一份資源
- 命名管道一定要有名字,從而使不相關(guān)進(jìn)程通過(guò)唯一路徑定位同一個(gè)文件
三. System V標(biāo)準(zhǔn)下的進(jìn)程間通信方式
下面我們要學(xué)習(xí)System V標(biāo)準(zhǔn),是在同一主機(jī)內(nèi)的進(jìn)程間通信方案,是站在OS層面,專(zhuān)門(mén)為進(jìn)程間通信設(shè)計(jì)的方案。
進(jìn)程通信的本質(zhì)是先讓不同進(jìn)程看到同一份資源,System V提供了這三個(gè)主流方案 ——
- 共享內(nèi)存 - 傳遞數(shù)據(jù)
- 消息隊(duì)列(有點(diǎn)落伍) - 傳遞數(shù)據(jù)
- 信號(hào)量 (多線程講POSIX標(biāo)準(zhǔn)) - 實(shí)現(xiàn)進(jìn)程同步&控制詳談
🌈共享內(nèi)存
基于共享內(nèi)存進(jìn)行進(jìn)程間通信原理 ——
- 首先在物理內(nèi)存當(dāng)中申請(qǐng)一塊內(nèi)存空間,將這塊內(nèi)存空間分別與各個(gè)進(jìn)程各自的頁(yè)表之間建立映射
- 進(jìn)程虛擬地址空間當(dāng)中開(kāi)辟空間(共享內(nèi)存)并將虛擬地址填充到各自頁(yè)表的對(duì)應(yīng)位置,使得虛擬地址和物理地址之間建立起對(duì)應(yīng)關(guān)系
- 所以?xún)蓚€(gè)進(jìn)程便看到了同一份物理內(nèi)存,這塊物理內(nèi)存就叫做共享內(nèi)存
💦共享內(nèi)存的建立
共享內(nèi)存提供者是操作系統(tǒng)OS,那么操作系統(tǒng)要不要管理共享內(nèi)存呢? -> 先描述再組織
共享內(nèi)存 = 共享內(nèi)存塊 + 對(duì)應(yīng)的共享內(nèi)存的內(nèi)核數(shù)據(jù)結(jié)構(gòu)(來(lái)描述其屬性)
💛 創(chuàng)建共享內(nèi)存
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
參數(shù):
-
key
:為了使不同進(jìn)程看到同一段共享內(nèi)存,即讓不同進(jìn)程拿到同一個(gè)ID,需要由用戶(hù)自己設(shè)定,但如何設(shè)定的與眾不同好難啊,就要借助下面這個(gè)函數(shù)。
所以怎么樣保證兩個(gè)進(jìn)程拿到同一個(gè)
key
值呢?#include <sys/types.h> #include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname
:自定義路徑名proj_id
:自定義項(xiàng)目ID- 返回值:成功后,返回生成的key_t值。失敗時(shí)返回1
-
szie
:共享內(nèi)存的大小,建議是4KB
的整數(shù)倍,因?yàn)楣蚕韮?nèi)存在內(nèi)核中申請(qǐng)的基本單位是頁(yè)(內(nèi)存頁(yè))。 -
shmflg
:標(biāo)記位,這一看就是宏,都是只有一個(gè)比特位是1且相互不重復(fù)的數(shù)據(jù),這樣|
在一起,就能傳遞多個(gè)標(biāo)志位IPC_CREAT
:如果單獨(dú)使用IPC_CREAT或者flg為0,如果創(chuàng)建共享內(nèi)存時(shí),底層已經(jīng)存在,獲取之;如果不存在,就創(chuàng)建之IPC_EXCL
:單獨(dú)使用沒(méi)有意義,通常要搭配起來(lái)IPC_CREAT | IPC_EXCL
,如果底層不存在,就創(chuàng)建,并返回;如果底層存在就出錯(cuò)返回。這樣的意義在于 如果調(diào)用成功,得到的一定是一個(gè)全新的共享內(nèi)存。
返回值:成功后,將返回有效的共享內(nèi)存標(biāo)識(shí)符。失敗了,返回-1,并設(shè)置errno錯(cuò)誤碼。
💛 控制共享內(nèi)存
手動(dòng)查看與手動(dòng)刪除
ipcs -m 查看ipc資源,不帶選項(xiàng)默認(rèn)查看消息隊(duì)列(-q)、共享內(nèi)存(-m)、信號(hào)量(-s)
ipcrm -m + shmid //刪除共享內(nèi)存
system V IPC資源,生命周期隨內(nèi)核!所以我們要手動(dòng) / 自動(dòng)刪除,那怎么樣自動(dòng)刪除呢?
💛 控制共享內(nèi)存
#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
參數(shù):
cmd
:設(shè)置IPC_RMID就行,IPC_RMID
:即便是有進(jìn)程和當(dāng)下的shm掛接,依舊刪除共享內(nèi)存(強(qiáng)大)buf
:這就是描述共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)啊!
返回值:失敗返回-1,成功返回0
💛 掛接和去關(guān)聯(lián)
attach 掛接 ——
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr
:掛接到什么位置,我們也不知道,給NULL,讓操作系統(tǒng)來(lái)設(shè)置shmflg
: 給0
最重要的是返回值:
- 這個(gè)地址一定是虛擬地址,類(lèi)似malloc返回申請(qǐng)到的起始地址
- 失敗返回-1,并設(shè)置錯(cuò)誤碼
detach 去關(guān)聯(lián) ——
int shmdt(const void *shmaddr);
shmaddr
:shmat返回的地址
注意:去關(guān)聯(lián),不是釋放共性?xún)?nèi)存,而是取消當(dāng)前進(jìn)程和共享內(nèi)存的關(guān)系,本質(zhì)是去掉進(jìn)程和物理內(nèi)存構(gòu)建映射關(guān)系的頁(yè)表項(xiàng)去掉
返回值:成功返回0,失敗返回-1
💛 shmid 和 key
只有創(chuàng)建的時(shí)候用key,大部分用戶(hù)訪問(wèn)共享內(nèi)存,都用的是shmid(用戶(hù)層)
💦共享內(nèi)存的進(jìn)程間通信
comm.h
#pragma one#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"using namespace std;//不推薦#define PATH_NAME "/home/ljj"
#define PROJ_ID 0x66
server.c
-
創(chuàng)建公共的
key
值 -
創(chuàng)建共享內(nèi)存 - 建議創(chuàng)建一個(gè)全新的共享內(nèi)存:因?yàn)槭峭ㄐ诺陌l(fā)起者
帶選項(xiàng)IPC_CREAT | IPC_EXCL
若和系統(tǒng)中已經(jīng)存在的ID沖突,則出錯(cuò)返回;
注意到其中權(quán)限perm
是0,那也可以設(shè)置一下int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
-
將指定的共享內(nèi)存,掛接到自己的地址空間上
-
將指定的共享內(nèi)存,從自己的地址空間去關(guān)聯(lián)
-
刪除共享內(nèi)存
#include "comm.hpp"string TransToHex(key_t k)
{char buffer[32];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}int main()
{//1.創(chuàng)建公共的key值key_t k = ftok(PATH_NAME, PROJ_ID);assert(k != -1);Log("create key done", Debug) << "server key : " << TransToHex(k) << endl;//2. 創(chuàng)建共享內(nèi)存 - 建議創(chuàng)建一個(gè)全新的共享內(nèi)存:因?yàn)槭峭ㄐ诺陌l(fā)起者int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(1);}Log("creat shm done", Debug) << "shmid : " << shmid << endl;sleep(10);//3.將指定的共享內(nèi)存,掛接到自己的地址空間上char *shmaddr = (char*)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //這里就是通信的代碼//4.將指定的共享內(nèi)存,從自己的地址空間去關(guān)聯(lián)int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //5.刪除共享內(nèi)存n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("delete shm done", Debug) << "shmid : " << shmid << endl;return 0;
}
關(guān)于申請(qǐng)共享內(nèi)存的大小size,我們說(shuō)建議是4KB的整數(shù)倍,因?yàn)楣蚕韮?nèi)存在內(nèi)核中申請(qǐng)的基本單位是頁(yè)(內(nèi)存頁(yè)),4KB。如果我申請(qǐng)4097Byte大小的空間,內(nèi)核會(huì)向上取整給我4096* 2Byte,誒?那我監(jiān)視到的↑怎么還是4097啊!雖然在底層申請(qǐng)到的是4096*2,但不會(huì)多給你,這樣也可能引起錯(cuò)誤~
client.c
- 只需獲取共享內(nèi)存;不用刪除
#include "comm.hpp"int main()
{key_t k = ftok(PATH_NAME, PROJ_ID);if(k < 0){Log("create key failed", Error) << "client key : " << k << endl;exit(1);}Log("create key done", Debug) << "client key : " << k << endl;//獲取共享內(nèi)存int shmid = shmget(k, SHM_SIZE, IPC_CREAT);if(shmid < 0){Log("create shm failed", Error) << "client key : " << k << endl;exit(2);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//掛接地址char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << "client key : " << k << endl;exit(3);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//使用//去關(guān)聯(lián)int n = shmdt(shmaddr);assert(n != -1);Log("datach shm success", Error) << "client key : " << k << endl;sleep(10);//你只管用,不需要?jiǎng)h除共享內(nèi)存return 0;
}
效果展示:
寫(xiě)一個(gè)命令行腳本來(lái)監(jiān)視共享內(nèi)存 ——
while :; do ipcs -m; echo "_________________________________________________________________"; sleep 1; done
注意觀察nattch
這個(gè)參數(shù)的變化:0->1->2->1->0
上面的框架都搭建好了之后,接下來(lái)就是通信部分:
1??客戶(hù)端不斷向共享內(nèi)存寫(xiě)入數(shù)據(jù):
//client將共享內(nèi)存看成一個(gè)char類(lèi)型的buffer
char a = 'a';
for(; a <= 'z'; a++)
{//每一次都想共享內(nèi)存shmaddr的起始地址snprintf(shmaddr, SHM_SIZE - 1,\"hello server, 我是其他進(jìn)程, 我的pid: %d, inc: %c\n",\getpid(), a);sleep(2);
}
2??服務(wù)端不斷讀取共享內(nèi)存當(dāng)中的數(shù)據(jù)并輸出:
//將共享內(nèi)存當(dāng)成一個(gè)大字符串
for(;;)
{printf("%s\n", shmaddr);sleep(1);
}
結(jié)果如下:
ps:我們發(fā)現(xiàn)即使我們沒(méi)有向server
端發(fā)消息,server也是不斷的在讀取信息的
💦共享內(nèi)存與管道進(jìn)行對(duì)比
共享內(nèi)存是所有進(jìn)程間通信方式中最快的一種通信方式。
將一個(gè)文件從一個(gè)進(jìn)程傳輸?shù)搅硪粋€(gè)進(jìn)程需要進(jìn)行四次拷貝操作:
我們?cè)賮?lái)看看共享內(nèi)存通信:
鍵盤(pán)寫(xiě)入shm,另一端可以直接獲取到,哪里還需要什么拷貝?最多兩次拷貝(鍵盤(pán)輸入一次,輸出到外設(shè)一次)
💦共享內(nèi)存歸屬誰(shuí)
共享內(nèi)存的區(qū)域是在OS內(nèi)核?還是在用戶(hù)空間?
- 用戶(hù)空間!
其中文本、初始化數(shù)據(jù)區(qū)、未初始化數(shù)據(jù)區(qū)、堆、棧、環(huán)境變量、命令行參數(shù)、再 往上就是1G
的OS內(nèi)核,其中剩余3G
都是用戶(hù)自己支配的
用戶(hù)空間:不用經(jīng)過(guò)系統(tǒng)調(diào)用,直接進(jìn)行訪問(wèn)!
- 所以雙方進(jìn)程如果要進(jìn)行通信,直接進(jìn)行內(nèi)存級(jí)的讀和寫(xiě)(減少了許多拷貝)
那為什么之前將的pipe和fifo都要通過(guò)read、write進(jìn)行通信,為什么呢?
因?yàn)楣艿离p方看到的資源都屬于內(nèi)核級(jí)的文件,我們無(wú)權(quán)直接進(jìn)行訪問(wèn),必須調(diào)用系統(tǒng)接口
💦共享內(nèi)存的特征
- 共享內(nèi)存的生命周期隨內(nèi)核
- 共享內(nèi)存是所有進(jìn)程中速度最快的,只需要經(jīng)過(guò)頁(yè)表映射,不需來(lái)回拷貝(不經(jīng)過(guò)OS)
- 共享內(nèi)存沒(méi)有提供訪問(wèn)控制,讀寫(xiě)雙方根本不知道對(duì)方的存在,會(huì)帶來(lái)并發(fā)問(wèn)題
🌈消息隊(duì)列(了解)
嚴(yán)重過(guò)時(shí):接口與文件不對(duì)應(yīng)
創(chuàng)建消息隊(duì)列,與創(chuàng)建共享內(nèi)存極其相似:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);
刪除消息隊(duì)列:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);
我們可以通過(guò)key找到同一個(gè)共享內(nèi)存。
我們發(fā)現(xiàn)共享內(nèi)存、消息隊(duì)列、信號(hào)量的 ——
- 接口都類(lèi)似
- 數(shù)據(jù)結(jié)構(gòu)的第一個(gè)結(jié)構(gòu)類(lèi)型
struct ipc_perm
是完全一致的!
我們由shmid
申請(qǐng)到的都是01234… 大膽推測(cè),在內(nèi)核中,所有的ipc
資源都是通過(guò)數(shù)組組織起來(lái)的。可是描述它們的結(jié)構(gòu)體類(lèi)型并不相同啊?但是~ System V標(biāo)準(zhǔn)的IPC資源,xxxid_ds結(jié)構(gòu)體的第一個(gè)成員都是ipc_perm
都是一樣的。
📢寫(xiě)在最后
應(yīng)該是我寫(xiě)過(guò)最長(zhǎng)的一篇博客了