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