💖作者:小樹苗渴望變成參天大樹🎈
🎉作者宣言:認真寫好每一篇博客💤
🎊作者gitee:gitee?
💞作者專欄:C語言,數據結構初階,Linux,C++ 動態規劃算法🎄
如 果 你 喜 歡 作 者 的 文 章 ,就 給 作 者 點 點 關 注 吧!
文章目錄
- 前言
- 一、共享內存的原理
- 二、直接代碼
- 2.1關于共享內存的四大接口
- 2.2如何通信
- 三、擴展知識
- 3.1 看看維護共享內存的結構體屬性
- 3.2 使用管道來實現同步互斥機制
- 四、總結
前言
今天我們來講進程間通信的的另一個通信方式,在第一篇講解進程間通信的博客中,博主就提到了SystemV標準的通信方式,我們前面講解的匿名管道和命名管道都是基于文件的,但是共享內存不是基于文件的,他的所有進程間通信最快的,因為他的拷貝少,共享內存的難點就在于他的接口多,復雜,因為SystemV標準下不止一個共享內存,還有消息隊列和信號量,都需要類似的接口,為了可以更好的復用接口函數,接下來博主就來帶大家學習共享內存。
講解邏輯:
- 直接原理,講解周邊問題
- 通過原理,寫一部分代碼,認識系統接口,進行測試
- 擴展代碼去講解
一、共享內存的原理
使用共享內存的目的是讓進程間進行通信,但是進程間通信的本質是讓不同的進程看到同一份資源,由共享內存這個名字可知,這篇共享的資源是一塊內存,計算機中我們一般由的地址要不是虛擬地址,要不是物理地址,想形成可執行程序里面的地址我們目前不談,而虛擬地址是每個進程特有的,所以我們猜測這塊共享內存是物理內存的一塊,因為有了前面的兩次通信方式的鋪墊,我們已經慢慢找到規律了,那博主就以一份圖給大家講解一下共享內存的原理。
共享內存的原理很簡單,就上幅這個圖片,但是博主要講一些周邊問題:
- 釋放共享內存,先去掛接,再釋放內存,是相反的操作
- 上面的操作都是進程直接做的嗎??不是,是直接由os去做的,原因涉及到物理內存了。
- 那既然有os去操作的,那么我們去創建,使用或者釋放都需要經過系統調用接口去讓os幫助我們實現
- 我們的不同進程通過共享內存進行通信,另外的進程也需要通過共享內存來進行通信,那么共享內存就不止一塊,由許多快,那么這塊共享內存都是需要管理起來的,所以先描述再組織,就對應我們上圖的struct結構體。里面存放的是對共享內存的管理屬性。
所以我們一會對共享內存的使用里面肯定會涉及到這個結構體里面的屬性,等會遇到了一個講一個,現在都講解出來讀者大概率不會理解。
二、直接代碼
我們通過剛才的原理分析,而且這些操作是需要通過系統調用接口的,所以我們一步步的來介紹這些系統調用接口。
2.1關于共享內存的四大接口
一、申請共享內存接口
- 返回值(用戶層)shmid:此函數申請一塊共享內存,返回共享內存標識符,可以先理解為和文件描述符唯一標志文件一樣的道理。
- 第二個參數,是申請共享內存的大小。單位是字節
- 第三個參數:共享內存是為了給不同的進程使用,那么使用這塊內存之前,只要由一個進程創建,其他進程拿來用就行了,那這個參數就是控制對共享內存的權限操作,來看我們自己要掌握的權限
(1)IPC_CREAT:(單獨使用)如果你申請的共享內存不存在就創建,存在就獲取返回
(2)IPC_CREAT | IPC_EXCL:如果你申請的共享內存不存在就創建,存在就報錯,這是保證了你創建的共享內存是最新的。IPC_EXCL不單獨使用
(3)第三個就是傳我們對應的權限,如0666上面的方式我們再講解文件操作的時候就講解過了,write函數里面需要傳這樣的參數,這些大寫字母起始就是對應的宏。
- 第一個參數:通過第三個參數,我們怎么知道這個共享內存存不存在,就好比你怎么保證讓不同的進程看到同一份共享內存是一樣的,此時就有了我們的第一個參數,接下來談談這個key。
(1)key是一個數字,這個數字是多少不重要。關鍵在于他必須再內核中具有唯一性,能夠讓不同的進程進程唯一標識
(2)第一個進程可以通過key來創建共享內存。第二個進程之后的進程,只要拿著這個key就可以和第一個進程看到同一個共享內存了
(3)對于一個已經創好的共享內存,key在哪??大家還記得一個說管理共享內存的結構體嗎,key就在共享內存的描述對象里
(4)通過第一點想要key去唯一標識共享內存,大家再回想一下命名管道是怎么唯一標識的,是不是通過就和文件名,所以這個key應該也類似于命名管道的標識方式。
(5)通過第二點,我們通過key創建共享內存,那么第一次創建的時候,這個key怎么有???我們總結出四個結論和一個問題,問題來到了這個key一開始時怎么產生的了,按照第四點的結論,我們來介紹一下這個函數
ftok
第一個參數:路徑這個隨便寫
第二個參數,這個是工程id,我們可以隨便去指定是一個數字
返回值(內核層):是一個共享內存標識符我們上面的兩個參數都是由用戶自己去定義的,所以可能會和系統中的key產生沖突,這個函數是通過一個算法將兩個參數進行運算的出來的這樣的一個key,每次生成的結果都是不一樣的,不是你每次傳的參數一樣計算出來的結果就是一樣的。這樣為什么就可以做到key是唯一的呢,我們的路徑是唯一的,而且第二個參數是我們自己傳,大概率也是唯一的,這樣就導致我們的key是唯一的,而且一旦創建這個key就是這個共享內存所獨有了,如果再生成這個key,只能獲取,不會再創建一個新的了
為什么key不由os自己創建呢,我們自己創建還有可能造成key沖突的問題??
(1)再談談key的時候的第二點我們知道這個我們通過創建共享內存是由一個進程去創建另一個進程去使用就可以,如果這個key是os生成的,創建好的共享內存,那另一個沒有關系的進程怎么獲取這塊共享內存,因為共享內存不是唯一的,所以os里面的key也不是唯一的,所以沒有辦法給另一個進程讓他獲取啊,有的人說傳給另一個進程,這樣就出現蛋生雞的問題,另一個進程要key才能進行通信,但是要key必須先通信,如果共享內存的個數是唯一的,那么可以讓os自己生成,大家自己理解一下
(2)這個key的獲取可以說是用戶的約定,和哪個進程通信只有用戶知道,就是程序員知道,兩個進程使用ftok這個相同的方式就可以獲取唯一的key,因為這兩個參數是唯一的
(3)有的人會說我們將系統自己生成的key通過管道傳給另一個進程就可以了,答案確實可以,但是這樣我們學習共享內存的成本就搞了,還要先學習管道,這樣也不嫩惡搞保證共享內存是一個獨立通信模塊了大家看到這里對于key的理解應該到位了,但是有一個關鍵的點,key vs shmid
這兩個都是共享內存的標識符,他兩有一個不就行了,key是內核中唯一標識的,shmid只有再進程里唯一標識的,我們操作共享共享內存的函數都是使用shmid。
通過上面的一系列分析,我們來申請一塊共享內存:shmget+ftok
sham.hpp:
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>#include "log.hpp"using namespace std;Log log;const int size = 4096;
const string pathname="/home/xdh";
const int proj_id = 0x6666;key_t GetKey()
{key_t k = ftok(pathname.c_str(), proj_id);if(k < 0){log(Fatal, "ftok error: %s", strerror(errno));exit(1);}log(Info, "ftok success, key is : 0x%x", k);return k;
}int GetShareMemHelper(int flag)
{key_t k = GetKey();int shmid = shmget(k, size, flag);if(shmid < 0){log(Fatal, "create share memory error: %s", strerror(errno));exit(2);}log(Info, "create share memory success, shmid: %d", shmid);return shmid;
}int CreateShm()//創建共享內存得到標識符shmid,進行了封裝
{return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
#endif
processa.cc:
#include"sham.hpp"
//這是進程a,有這個進程創建共享內存
int main()
{//申請共享內存int shmid=CreateShm();sleep(5);return 0;
}
通過結果我們發現,我們第一次運行程序的時候申請了一塊共享內存獲得了共享內存標識符,但是第二次運行的時候顯示就存在了,我們使用
ipcs -m
查看共享內存,我們得出結論,進程結束了,我們的共享內存還是存在的,共享內存的生命周期是隨著內核的,不是隨著進程的,通過原理圖也不難理解這點,沒有關閉共享內存,這也可能會造成內存泄漏,類似于malloc。這里面我們再來研究一個點,我們申請4097個字節大小的空間看看效果
我們看到大小是4097,在內核里面,我們的os實際上會給我們的4096*2大小的空間,但是我們只能使用4097,這個大家要記住,所以建議還是申請4096點整數倍,折合人民幣我們內存的頁寬有關系,大家先不用了解。
二、.掛接共享內存:shmat函數
我們的共享內存申請好了,我們就需要將其掛接到我們的地址空間上,就是原理圖上的第二步
- 第一個參數:就是傳剛才使用shmget函數的返回值即可是共享內存的唯一標識符
- 第二個參數:指定掛接到那個位置,我們申請好了共享內存,要掛接到我們進程的地址空間的共享群位置,這么多位置總要找到一個位置的其實位置吧,這樣也方便我們頁表進行映射,所以需要制定,我們在這里傳空指針就好了,意思讓系統自己決定
- 第三個參數:是掛接的方式
我們在這里傳0進去就好了
- 返回值:我們就是把掛接到地址空間的那塊位置的首地址返回出來,讓用戶能拿到,進行操作,所以返回值是void需要強轉,和malloc類似,失敗就返回(void)-1
我們來看代碼實現:
//將共享內存掛接到自己的地址空間char* shmaddr=(char*)shmat(shmid,nullptr,0);if(*shmaddr<0){log(Fatal,"shmat flase:%s",strerror(errno));exit(3);}log(Info,"shmat sucessful:%s",strerror(errno));sleep(3);
我們來觀察一下nattch這個屬性,他就是表示這塊共享內存當前的掛接樹,沒調用這個shmat函數之前為0,調用之后為1,而且當進程退出他的掛接數自然的就減少了1
三.去掉掛接關系:shmdt
剛才是因為程序結束,掛接數減少了,但我們有時候程序沒結束就像去掛接,怎么做??我們通過shmdt來去掛接,來看文檔
這個函數非常的簡單,就是傳剛才掛接函數返回值就可以了,我們直接來看使用效果:
我們分析,我們3秒后創建共享內存,5秒后掛接進程,掛接數變成1,3秒后,去掛接,掛接數變成1,在3秒后程序終止,
int n=shmdt(shmaddr);if(n<0){log(Fatal,"shmdt flase:%s",strerror(errno));}log(Info,"shmdt sucessful:%s",strerror(errno));sleep(3);
和我們預測的一樣,我們的掛接數不一定非得在程序結束才會減1
四.釋放共享內存:shmctl
我們想要將我們的共享概念內存釋放掉使用shmctl
第一個參數:共享內存唯一標識符
第三個參數:是一個描述共享內存的狀態和訪問權限的數據結構,也就是我們開頭說的描述共享內存的結構圖,看到key在里面了吧,對于這個參數我們可以傳一個null,因為不需要將狀態獲取到,這是一個輸出型參數和status一樣。
第二個參數:將要采取的動作,就是對第三個參數實行什么樣的操作,有三個操作
我們關注的是最后一個,刪除共享內存
來看操作:
int n1=shmctl(shmid,IPC_RMID,nullptr);if(n1<0){log(Fatal,"shmctl flase:%s",strerror(errno));}log(Info,"shmctl sucessful:%s",strerror(errno));sleep(3);
通過結果驗證我們的講解,我們也可以通過
ipcrm -m +shmid
來刪除共享內存,這個大家下去試試,但是shmctl傳進去的操作不一樣,功能就不一樣,如果傳IPC_STAT,就可以查看屬性。
我們將另一個進程也掛接到這個共享內存上吧,因為申請和釋放進程a幫助我們做了,我們做的就是掛接和去掛接就可以了,來看進程b的代碼:
先展示進程a的代碼:
#include"sham.hpp"
//這是進程a,有這個進程創建共享內存
int main()
{sleep(3);//申請共享內存int shmid=CreateShm();sleep(5);//將共享內存掛接到自己的地址空間char* shmaddr=(char*)shmat(shmid,nullptr,0);if(*shmaddr<0){log(Fatal,"shmat flase:%s",strerror(errno));exit(3);}log(Info,"shmat sucessful:%s",strerror(errno));sleep(3);//去掛接int n=shmdt(shmaddr);if(n<0){log(Fatal,"shmdt flase:%s",strerror(errno));}log(Info,"shmdt sucessful:%s",strerror(errno));sleep(3);//釋放共享內存int n1=shmctl(shmid,IPC_RMID,nullptr);if(n1<0){log(Fatal,"shmctl flase:%s",strerror(errno));}log(Info,"shmctl sucessful:%s",strerror(errno));sleep(3);return 0;
}
進程b:
#include "sham.hpp"int main()
{sleep(3);int shmid=GetShm();//這個函數在sham.hpp里面寫就行了,獲取shmidsleep(5);//將共享內存掛接到自己的地址空間char* shmaddr=(char*)shmat(shmid,nullptr,0);if(*shmaddr<0){log(Fatal,"shmat flase:%s",strerror(errno));exit(3);}log(Info,"shmat sucessful:%s",strerror(errno));sleep(3);//去掛接int n=shmdt(shmaddr);if(n<0){log(Fatal,"shmdt flase:%s",strerror(errno));}log(Info,"shmdt sucessful:%s",strerror(errno));sleep(3);return 0;
}
我們也成功看到了掛接數變成了2,上面講解的一切都是讓兩個不同的進程之間看到同一份資源,還沒有開始通信
2.2如何通信
我們通過上面一系列的操作終于實現我們再原理圖講的內容了,該說不說,確實太復雜的,但是這一系列的操作,讓他的通信顯得非常的簡單,我們共享內存就是一塊物理內存,映射到我們進程的地址空間上,我們程序通過這塊地址空間上的地址就可以直接訪問這塊物理空間,此時他就很想malloc申請空間,然后去使用這塊空間的方法很想,我們一起來看操作,讓b寫,a讀
a:
while(true){cout<<"a say@"<<shmaddr<<endl;sleep(1);}
b:
while(true){cout<<"b enter@";fgets(shmaddr,4096,stdin);sleep(1);}
結論:
- 我們我們兩個進程對這塊空間的操作是你搞你的我搞我的,兩者不受任何影響,所以說明共享內存間是沒有同步互斥機制的
- 我們的共享內存是所有進程中通信速度最快,因為拷貝少
- 我們的共享內存的數據是用戶自己去維護的,所以這些看到和管道有不同的地方,沒有清空數據,這是需要用戶自己去決定的。
但是我們確實實現了兩個進程間通信了,有問題我們一會來解決。
三、擴展知識
3.1 看看維護共享內存的結構體屬性
我們剛才的參數都是為了描述共享內存的,所以維護共享概念給內存的屬性有哪些呢,剛才其實也大致看到了一些。
我們通過代碼看看我們剛才提到一下屬性:
再a進程把通信代碼改成下面的
int count=0;struct shmid_ds shmds;while(true){sleep(1);if(count==0){shmctl(shmid, IPC_STAT, &shmds);cout << "shm size: " << shmds.shm_segsz << endl;cout << "shm nattch: " << shmds.shm_nattch << endl;printf("shm key: 0x%x\n", shmds.shm_perm.__key);cout << "shm mode: " << shmds.shm_perm.mode << endl;}count++;}
3.2 使用管道來實現同步互斥機制
我們因為目前只學了System V的共享內存,我們想要解決這個問題,還可以使用信號量,但是這個我們不做重點介紹,等有機會我們在給大家講解信號量是怎么解決共享內存的這個缺點,我們今天,就使用管道去解決這個問題吧,因為是不相關的進程,所以使用命名管道。
這個使用管道的方法其實和共享內存是一點關系沒有,之根據他會阻塞就不會執行下面的代碼,這樣間接控制了。我們后面會簡單介紹一下信號量是怎么解決這個問題的,但是知識帶大家了解一下。
四、總結
今天我們學習了共享內存,學習成本和前面兩個差不多,前面是原理的鋪墊大家不容易理解,但是使用簡單,二共享內存有了前面的原理鋪墊,理解起來不難,但是后面的使用接口對大家來說可能是一個難度,大家下去好好把四大接口函數理解一下,這對博主下一篇講解消息隊列以及信號量有很大幫助,希望大家下來可以去自己實現博主這篇博客上面的內容,我們下篇再見