一 共享內存的概念
1. 1 共享內存的原理
? ???之前我們學過管道通信,分為匿名管道和命名管道,匿名管道通過父子進程的屬性繼承原理來完成父子進程看到同一份資源的目的,而命名管道則是通過路徑與文件名來唯一標識管道文件,來讓不同的進程之間進行通信。
? ?而共享內存也是同理, 就是允許兩個或多個不相關的進程訪問同一段物理內存空間。
1.2 基本概念?
? ?不同進程之間共享的內存通常為同一段物理內存。進程可以 將同一段物理內存連接到他們自己的地址空間中 ,所有的進程都可以訪問共享內存中的地址。如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。
?概念圖如下:
?
?二 共享內存的使用
2.1 使用流程
一般而言,共享內存的使用可以分為以下幾步:
- 創建共享內存段
- 關聯共享內存(建立通信)
- 傳輸數據
- 關閉共享內存區域
2.1.1 創建共享內存段 shmget
創建共享內存接口:shmget
創建一個新共享內存段或者取得一個既有共享內存段的標識符(即由其他進程創建的共享內存段)。這個調用將返回后續調用中需要用到的??共享內存標識符。
接口如下:
?
int shmget(key_t key, size_t size, int shmflg);- 功能: 創建一個新的共享內存段,或者獲取一個既有的共享內存段的標識新創建的內存段中的數據都會被初始化為0- 參數:- key: key_t 類型是一個整型,通過這個找到或者創建一個共享內存一般使用16進制標識,非0值- size: 共享內存的大小,以幾頁的大小創建(大于1的數值,一般為4的整數)- shmflg: 屬性- 訪問權限- 附加屬性: 創建/判斷共享內存是不是存在- 創建: IPC_CREAT- 判斷共享內存是否存在: IPC_EXCL,需要和 IPC_CREAT一起使用IPC_CREAT | IPC_EXCL | 0664(權限)- IPC_CREAT 如果內核中對應key值的共享內存不存在,則新建一個共享內存并返回該共享內存的句柄;如果已存在,則直接返回該共享內存的句柄– IPC_CREAT IPC_EXCL 如果不存在對應key值的共享內存則新建一個共享內存并返回該共享內存的句柄;如果已存在,則出錯返回- 返回值:失敗: -1 并設置錯誤號成功: >0 返回共享內存的引用的ID (int類型),后面操作共享內存都是通過這個值
2.1.2 key值的獲取 ftok?
?key值的作用:
問:當我們調用系統接口申請了一塊共享內存,我們要保證對應的進程能夠訪問到同一塊共享內存,那么如何做到這一點呢?
答案:每一個共享內存被申請的時候都會有一個key值,這個key值用于標識系統中共享內存的唯一性。
ftok函數的作用:通過數學運算將一個已存在的路徑名pathname和一個整數標識符proj_id轉換成一個key值,稱為IPC鍵值,在使用shmget函數獲取共享內存時,這個key值會被填充進維護共享內存的數據結構當中。需要注意的是,pathname所指定的文件必須存在且可存取。
ftok運算出來的key值可能會產生沖突,不過概率很小。如果產生沖突,就對ftok的參數進行修改即可。
當我們想讓兩個進程使用共享內存的時候,我們就利用key值尋找到這一塊共享內存。
key_t ftok(const char* pathname, int proj_id);- 功能: 根據指定的路徑名, 和int值, 生成一個共享內存的key- 參數: - pathname: 指定一個存在的路徑/home/ubuntu/Linux/a.txt/root- proj_id: int類型的值,但是這個系統調用只會使用其中的1個字節(8bit)范圍: 0-255 一般指定一個字符 'a'
2.1.3??共享內存的關聯 shmat
我們不是單單把共享內存創建出來就行了,我們還要將需要獲取這塊共享內存的進程與對應的共享內存關聯起來,通信結束后,解除關聯,最后進行共享內存的釋放。
void* shmat(int shmid, const void* shmaddr, int shmflg);- 功能: 和當前的進程進行關聯- 參數: - shmid: 共享內存的標識(ID),由shmget返回值獲取- shmaddr: 申請的共享內存的起始地址(虛擬內存的地址,一般不會手動指定,由內核指定,設置為NULL)- shmflg: 對共享內存的操作- 讀權限: SHM_RDONLY,必須要有讀權限- 讀寫: 0- 返回值:成功: 返回共享內存的首(起始)地址失敗: (void*) -1 設置錯誤號
?2.1.4 解除共享內存的關聯 shmdt
? ?接觸進程和共享內存的關聯。
int shmdt(const void* shmaddr);- 功能: 解除當前進程和共享內存的關聯- 參數:- shmaddr: 共享內存的首地址- 返回值:成功: 0失敗: -1
?2.1.5 刪除共享內存函數? shmctl
int shmctl(int shmid, int cmd, struct shmid_ds* buf);- 功能: 對共享內存進行操作。 刪除共享內存,共享內存要刪除才會消失, 創建共享內存的進程被銷毀了對共享內存是沒有任何影響的。- 參數:- shmid: 共享內存的ID- cmd: 要做的操作- IPC_STAT: 獲取共享內存的當前狀態- IPC_SET: 設置共享內存的狀態- IPC_RMID: 標記共享內存被銷毀- buf: 需要設置或者獲取的共享內存的屬性信息- IPC_STAT: buf存儲數據- IPC_SET: buf中需要初始化數據,設置到內核中- IPC_RMID: 沒有用,NULL
注意:一般有幾個進程關聯?這一塊共享內存,就需要解除關聯多少次,但是,刪除共享內存只需要刪除一次就好了(其實可以刪除多次,看補充)。
2.1.6 示例?
?接下來我們進行一段示例,操作內容如下:
要求:使用代碼創建一個共享內存, 支持 A.B 兩個進程進行通信。
?進程A 向共享內存當中寫 “i am process A”。
?進程B 從共享內存當中讀出內容,并且打印到標準輸出。
? 因為兩個進程中有大量重復代碼,因此,我們封裝一個頭文件,以此來復用。
shared.h內容如下:
?
#include<iostream>
#include<cassert>
#include<cstring>
#include<cerrno>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstdlib>
#include<unistd.h>
using namespace std;#define PATHNAME "./shared2"//ftok的第一個參數,是一個合法路徑
#define PROJ_IO 'a'//ftok的第二個參數
#define MAXSIZE 4096//創建的共享內存的大小
//1.獲取key值代碼
key_t GetKey()
{key_t k = ftok(PATHNAME, PROJ_IO);//如果返回值小于0 則創建失敗if(k < 0){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(1);}return k;
}//2.創建共享內存段代碼
int GetShmget(key_t k,int flags){int shmid = shmget(k, MAXSIZE, flags);if(shmid < 0){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(2);}return shmid;
}//獲取共享內存
int Getshm(key_t k){return GetShmget(k, IPC_CREAT);//沒有就創建,有就獲取
}//創建共享內存
int Createshm(key_t k){return GetShmget(k, IPC_CREAT | IPC_EXCL | 0600);//沒有創建,有就報錯,這里創建內存需要給對應的權限
}//3. 關聯共享內存,返回共享內存的空間起始位置
void* attachshm(int shmid){void* p = shmat(shmid, nullptr, 0);//因為linux系統是64位,一個地址是8個字節,所以要變成8個字節大小的數據類型做對比if((long long)p == -1L){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(4);}return p;
}//4.解除共享內存的關聯
void detachshm(void* p){//如果失敗報錯if(shmdt(p) == -1){std::cerr << errno << ":" << strerror(errno) << std::endl;}
}//5 shmctl刪除共享內存
void delshm(int shmid){if(shmctl(shmid, IPC_RMID, nullptr) == -1){std::cerr << errno << ":" << strerror(shmid) << std::endl;exit(3);}
}
A.cpp:
#include"shared.hpp"int main(){//先生成一個key值key_t k=GetKey();printf("A 獲取key值成功: %d\n",k);//創建共享內存,有就獲取標識碼shmidint shmid=Getshm(k);printf("A 獲取共享內存塊成功\n");//關聯共享內存塊char* p = (char*)attachshm(shmid);if (p == (void*)-1) {perror("shmat failed");delshm(shmid);printf("A 刪除共享內存成功\n");exit(EXIT_FAILURE);}printf("A 關聯共享內存塊成功\n");//開始寫入:const char* str = "i am process A";snprintf(p, MAXSIZE, "%s", str);//去關聯detachshm(p);printf("A 去關聯成功\n");return 0;
}
B.cpp
#include"shared.hpp"int main(){//先生成一個key值key_t k=GetKey();printf("B 獲取key值成功: %d\n",k);//創建共享內存,有就獲取標識碼shmidint shmid=Getshm(k);printf("B 獲取共享內存塊成功\n");//關聯共享內存塊char* p = (char*)attachshm(shmid);if (p == (void*)-1) {perror("shmat failed");delshm(shmid);printf("B 刪除共享內存成功\n");exit(EXIT_FAILURE);}printf("B 關聯共享內存塊成功\n");//開始讀出:sleep(5);printf("attach sucess, address p: %s\n",p);//去關聯detachshm(p);printf("B 去關聯成功\n");//刪除共享內存delshm(shmid);printf("B 刪除共享內存成功\n");return 0;
}
?代碼結果:
2.2 共享內存的命令行操作
2.2.1?共享內存的查看 –?ipcs指令
????報告進程間通信設施的狀態,包括共享內存、消息隊列以及信號量等等!
ipcs用法:
ipcs -a //打印當前系統中所有進程間通信方式的信息
ipcs -m //打印出使用共享內存進行進程間通信的信息(**常用**)
ipcs -q //打印出使用消息隊列進行進程間通信的信息
ipcd -s //打印出使用信號進行進程間通信的信息
2.2.2? 共享內存的刪除-?ipcrm 指令
ipcrm用法(rm:remove)
ipcrm -M shmkey //移除用shmkey創建的共享內存段
ipcrm -m shmid //移除用shmid標識的共享內存段
ipcrm -Q msgkey //移除用msqkey創建的消息隊列
ipcrm -q msqid //移除用msqid標識的消息隊列
ipcrm -S semkey //移除用semkey創建的信號
ipcrm -s semid //移除用semid標識的信號
三 補充說明
問題1:操作系統如何知道共享內存被讀誦進程關聯- 共享內存委會一個結構體 struct shmid_ds 這個結構體中有一個成員 shm_nattach- shm_nattach 記錄了關聯的進程個數
問題2:可不可以對共享內存進行多次刪除 shmctl- 可以的- 因為 shmctl 僅是標記刪除共享內存,不是直接刪除- 什么時候真正刪除呢?當和共享內存關聯的進程數為0的時候,就真正被刪除- 當共享內存的key為0的時候, 共享內存被標記刪除了- 因此,需要合理衡量共享內存的刪除,不要在其它進程還在使用時刪除
?