目錄
一. 共享內存實現進程間通信的原理
二.?共享內存相關函數
2.1 共享內存的獲取 shmget / ftok
2.2?共享內存與進程地址空間相關聯 shmat
2.3?取消共享內存與進程地址空間的關聯 shmdt
2.4?刪除共享內存?shmctl
2.5?通信雙方創建共享內存代碼
三.?共享內存實現進程間通信
3.1?實現方法及特性
3.2?為共享內存添加訪問控制
四.?總結
一. 共享內存實現進程間通信的原理
要實現進程間通信,就必須讓相互之間進行通信的進程看到同一份資源(同一塊內存空間),如通過管道實現進程間通信,本質就是讓兩個進程分別以讀和寫的方式打開同一份管道文件,一個進程向管道中寫數據,另一個進程再從管道中將數據讀出,這樣兩個進程就可以看到同一份內存空間,從而實現了進程間通信。
System V共享內存實現進程間通信的方式與管道相同,區別在于管道是基于文件的,而共享內存則是直接申請內存空間,不需用進行文件相關操作。通過System V共享內存實現通信的進程,都會使用物理內存中的同一塊空間,這一塊公共的物理內存空間經過通信雙方進程的頁表,映射到進程地址空間的共享區,通信雙方進程在運行期間,拿到共享區虛擬地址,通過頁表映射,就可以看到同一塊物理內存,就可以實現進程間通信。
如果操作系統內有多組通過System V共享內存方式相互通信的進程處于運行狀態,那么就會存在多組共享內存,操作系統需要對這些共享內存空間進行管理,管理方式為:先通過struct結構體進行描述,再利用特定的數據結構組織。
可以這樣理解:共享內存 =?共享的物理內存 +?對應的內核級數據結構。

二.?共享內存相關函數
共享內存實現進程間通信的步驟可以總結為:創建共享內存 ->?共享內存與地址空間相關聯 ->?通信 ->?共享內存與地址空間解綁 ->?銷毀共享內存。
2.1 共享內存的獲取 shmget / ftok
shmget函數:獲取共享內存
頭文件:#include<sys/ipc.h>、#include<sys/shm.h>
函數原型:int shmget(key_t key, size_t size, int shmflg)
函數參數:
? ? ? ? key --?特定共享內存的標識符
? ? ? ? size --?共享內存的大小
????????shmflg --?共享內存獲取的權限參數
返回值:創建成功返回共享內存的編號(稱為shmid),失敗返回-1
共享內存標識符key:OS中可能存在多個共享內存,需要保證通信雙方看到同一塊共享內存,因此,每個共享內存都需要一個特定的key值進行區分,這個key值是多少并不重要,只要保證它在OS中是唯一的即可。通信雙方進程(Serve && Client)需要約定相同的算法,保證他們可以使用shmget獲取到同一塊共享內存。
ftok函數可以用于獲取key值,只要調用ftok的實參相同,就會返回相同的key值。
ftok函數:獲取共享內存標識符key
頭文件:?#include<sys/ipc.h>、#include<sys/types.h>
函數原型:key_t ftok(const char* pathname, int proj_id);
函數參數:
? ? ? ? pathname:項目(文件)路徑
? ? ? ? proj_id:項目(文件)的id編號
返回值:成功返回特定的key值,否則返回-1。
共享內存大小size:以字節為單位,建議取頁(PAGE:4096bytes)大小的整數倍,因為如果獲取共享內存空間的大小不是頁大小的整數倍,OS就會向上取整申請到頁大小整數倍的內存空間,但是多申請的空間卻不能被用戶所使用。如,申請4097bytes的共享內存,OS會實際申請2*4096bytes的空間,而能被使用的只有4097bytes,剩下的都浪費掉了。
權限參數shmflg:有IPC_CREAT、IPC_EXCL、共享內存起始權限碼、0這幾種選項,他們之間通過豎劃線 |?隔開,每個選項都有其意義。
- IPC_CREAT:如果key標識的共享內存存在,就直接將其獲取,如果不存在,就創建。
- IPC_EXCL:單獨使用沒有任何意義,一般配合IPC_CREAT使用,IPC_CREAT | IPC_EXCL表示如果共享內存不存在就將其創建,如果存在直接報錯,這樣可以保證獲取到的共享內存是一塊全新的共享內存。
- 起始權限碼:用戶對于這塊共享內存的使用權限,如0666就表示擁有者、所屬組、其他人就具有讀寫權限。
- 0:只能獲取已經存在的共享內存,不能創建新的,不存在就報錯。
一般而言,通信雙方分別以?IPC_CREAT | IPC_EXCL?和 0?的方式獲取共享內存,確保一方創建全新的共享內存,另一方只能獲取到該共享內存(傳0阻斷不存在創建新共享內存的可能)。
代碼2.1以?IPC_CREAT | IPC_EXCL | 0666?的方式獲取共享內存,運行代碼,就可以成功獲取共享內存,但是當第二次運行代碼,卻發現運行出錯了(見圖2.1),這是因為該共享內存再第一次程序運行后被創建,存在于操作系統中,IPC_CREAT | IPC_EXCL獲取的共享內存一定是全新的,因此第二次運行程序會失敗,刪除該共享內存之后才可以再次成功運行。
結論:共享內存的生命周期是隨OS內核的,而不是隨進程的。
代碼2.1:獲取共享內存
// common.hpp -- 頭文件
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096// shmServe.cc -- 客戶端代碼源文件(用于接收信息)
#include "common.hpp"int main()
{// 獲取共享內存key值key_t k = ftok(PATH_NAME, PROJ_ID); if(k == -1){perror("ftok");exit(1);}// 創建共享內存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(2);}printf("Serve# 共享內存獲取成功,shmid:%d\n", shmid);return 0;
}

這里介紹兩條指令,分別用于查看共享內存信息和刪除共享內存:
- ipcs -m 指令:查看系統中所有共享內存的詳細信息。
- ipcrm -m [shmid]:通過指定共享內存的shmid來刪除指定的共享內存。
當然,也可以通過代碼刪除共享內存,本文后面會講解。

2.2?共享內存與進程地址空間相關聯 shmat
shmat函數:將共享內存關聯到進程地址空間
頭文件:#include<sys/types.h>、#include<sys/shm.h>
函數原型:void* shmat(int shmid, const void* shmaddr, int shmflg)
函數參數:
? ? ? ? shmid:進行掛接的共享內存的shmid
? ? ? ? shmaddr:指定掛接的虛擬地址(傳NULL表示讓OS自動選擇掛接地址)
? ? ? ? shmflg:掛接權限相關參數
返回值:若成功返回掛接到的虛擬地址,失敗返回nullptr
掛接地址shmaddr參數:由于我們并不可知虛擬地址的具體使用情況,所以這個參數基本都是傳NULL/nullptr來讓OS自動選擇虛擬地址進行關聯。?
掛接權限shmflg:如果傳SHM_RDONLY,這表示對應共享內存空間只有讀權限,傳其他都是讀寫權限,一般shmflg都傳實參0。
當共享內存與虛擬地址關聯期間,使用ipcs -m指令查看共享內存屬性信息,nattch就會變為1,如果通信雙方都與共享內存進行了關聯,那么nattch就是2。
2.3?取消共享內存與進程地址空間的關聯 shmdt
shmdt函數:讓共享內存與當前進程脫離
頭文件:#include<sys/types.h>、#include<sys/shm.h>
函數原型:int shmdt(const char* shmaddr)
返回值:成功返回0,失敗返回-1
2.4?刪除共享內存?shmctl
通過共享內存控制shmctl函數(共享內存控制函數),可以刪除共享內存。
刪除共享內存的操作只要通信雙方有一方指向即可,否則會造成重復刪除。一般而言,讀取信息的進程創建新的共享內存,也負責刪除共享內存,遵循誰創建、誰刪除的原則。
shmctl函數:控制共享內存
頭文件:#include<sys/ipc.h>? #include<sys/shm.h>
函數原型:int shmctl(int shmid, int cmd, struct shmid_ds* buf)
函數參數:
? ? ? ? shmid --?共享內存的shmid
? ? ? ? cmd --?控制指令,選擇操作
? ? ? ? buf --?指向描述共享內存屬性信息的結構體指針
返回值:成功返回非負數,失敗返回-1
形參cmd可以選擇具體的控制策略:
- IPC_STAT --?以buf為輸出型參數,獲取共享內存的屬性信息。
- IPC_SET --?設置共享內存的屬性為buf指向的內容。
- IPC_RMID --?刪除共享內存,此時buf傳空指針NULL。
2.5?通信雙方創建共享內存代碼
代碼2.2:頭文件common.hpp --?由通信雙方共同包含
#pragma once#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096
代碼2.3:服務端代碼shmServe.cc --?用于數據讀取
#include "common.hpp"int main()
{// 獲取共享內存key值key_t k = ftok(PATH_NAME, PROJ_ID); if(k == -1){perror("Serve ftok");exit(1);}printf("Serve# 成功獲取key值,key:%d\n", k);// 創建共享內存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("Sreve shmget");exit(2);}printf("Serve# 共享內存獲取成功,shmid:%d\n", shmid);// 將共享內存與進程相關聯char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Serve shmat");exit(3);}printf("Serve# 共享內存與進程成功關聯,shmid:%d\n", shmid);// 通信代碼// ... ...// 讓共享內存脫離當前進程int n = shmdt(shmaddr);if(n == -1){perror("Serve shmdt");exit(4);}printf("Serve# 共享內存成功脫離進程,shmid:%d\n", shmid);// 刪除共享內存n = shmctl(shmid, IPC_RMID, NULL);if(n == -1){perror("Serve shmctl");exit(5);}printf("Serve# 共享內存刪除成功,shmid:%d\n", shmid);return 0;
}
代碼2.4:客戶端代碼shmClient.cc --?用于數據發送
#include "common.hpp"int main()
{// 獲取共享內存key值key_t k = ftok(PATH_NAME, PROJ_ID); if(k == -1){perror("Client ftok");exit(1);}printf("Client# 成功獲取key值,key:%d\n", k);// 創建共享內存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("Client shmget");exit(2);}printf("Client# 共享內存獲取成功,shmid:%d\n", shmid);// 將共享內存與進程相關聯char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Client shmat");exit(3);}printf("Client# 共享內存與進程成功關聯,shmid:%d\n", shmid);// 通信代碼// ... ...// 讓共享內存脫離當前進程int n = shmdt(shmaddr);if(n == -1){perror("Client shmdt");exit(4);}printf("Client# 共享內存成功脫離進程,shmid:%d\n", shmid);return 0;
}
三.?共享內存實現進程間通信
3.1?實現方法及特性
在數據輸入端(shmClient),我們可以將共享內存視為一塊通過malloc得來的char*指向的一段動態內存空,可以使用printf系列函數向這塊空間寫數據,或者將共享內存空間視為數組,使用下標的形式給每個位置賦值,這樣就實現了將數據寫入共享內存。
在數據讀取端(shmServe),可以將共享內存視為一個大字符串,通過特定的方式,從這個大字符串中獲取數據即可。
代碼3.1和代碼3.2實現了共享內存進程間通信的簡單邏輯,在shmClient端,通過下標訪問的方式,每隔3s寫一次數據,在shmServe端,每隔1s讀取一次數據。先運行shmServe端代碼,間隔幾秒后運行shmClient端代碼,根據圖3.1展示的運行結果,shmServe端在shmClient端開始運行之前就開始讀取共享內存中的內容,在shmClient運行起來后,由于讀快寫慢,shmClient寫入的內容在shmServe端被多次讀取,可見,共享內存,沒有訪問控制。
結論1:共享內存沒有訪問控制。
代碼3.1:shmClient端發送數據
// 通信代碼char ch = 'a';int count = 0;for(; ch <= 'c'; ++ch){shmaddr[count++] = ch;printf("write succsee# %s\n", shmaddr);sleep(3);}snprintf(shmaddr, SIZE, "quit");
代碼3.2:shmServe端讀取數據?
// 通信代碼while(true){printf("[Client say]# %s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;sleep(1);}

通過觀察上面的代碼我們發現,用戶可以直接向共享內存中寫數據和從共享內存中讀取數據,不需要經過用戶級緩沖區,共享內存的讀或寫操作最少只需要一次拷貝即可完成。而通過管道進行讀寫,則需要將數據預先寫入或讀入緩沖區,才可以寫入管道文件或讀出。圖3.2為使用管道和共享內存的方法進行進程間通信時,讀和寫操作涉及的數據拷貝情況,管道通信至少要進行兩次數據拷貝,而共享內存可以只進行一次數據拷貝,因此共享內存是一種高效的進程間通信手段。
結論2:共享內存進行進程間通信,通信的一方向共享內存中寫入數據,通信的另一方馬上就能讀取到數據,不需要向操作系統中拷貝數據,共享內存是所有進程間通信方法中效率最高的。

管道通信的特性總結:
- 不具有訪問控制,存在并發問題。
- 不需要向OS內核中拷貝數據,通信效率高。
3.2?為共享內存添加訪問控制
通過使用命名管道加以輔助,就可以為共享內存添加訪問控制,具體的實現方法和原理為:
- 在讀端(shmServe)程序開始運行時創建命名管道文件,程序運行結束后管道文件銷毀。
- 在寫端(shmClient)向共享內存中寫入數據后,向管道文件中寫入任意的、少量的數據,在讀端(shmServe)獲取共享內存內容之前,先讀取管道中的資源,如果寫端沒有將期望的數據全部寫入共享內存,那么就不會向管道中寫數據,讀端就必須阻塞等待管道中被寫入數據,也就無法獲取共享內存中的數據。只有當寫端完成向共享內存中寫入一次數據,然后向管道文件中寫入數據讓讀端讀到了管道資源后,讀端代碼才可以繼續運行,獲取到共享內存中的資源。
代碼3.3 ~ 3.5,為通過管道為共享內存添加訪問控制的實現代碼。
代碼3.3:common.hpp頭文件 --?被通信雙方源文件包含
#pragma once#include <iostream>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096#define FIFO_NAME "fifo.ipc"
#define MODE 0666// 定義類,其構造和析構函數可以創建和銷毀管道文件
class Init
{
public:Init(){int n = mkfifo(FIFO_NAME, MODE);if(n == -1) perror("mkfifo");assert(n != -1);(void)n;}~Init(){int n = unlink(FIFO_NAME);assert(n != -1);(void)n;}
};#define READ O_RDONLY
#define WRITE O_WRONLY// 管道文件打開函數
int OpenFifo(const char* pathname, int flags)
{int fd = open(pathname, flags);assert(fd != -1);return fd;
}// 等待函數 -- 用于讀端訪問控制
// 管道內沒有資源時就阻塞
void Wait(int fd)
{uint32_t temp = 0;ssize_t sz = read(fd, &temp, sizeof(uint32_t));assert(sz == sizeof(uint32_t));(void)sz;
}// 喚醒函數 -- 用于寫端進程控制
// 向管道內寫數據,終止讀端進程的阻塞等待
void WakeUp(int fd)
{uint32_t temp = 1;ssize_t sz = write(fd, &temp, sizeof(uint32_t));assert(sz == sizeof(uint32_t));(void)sz;
}// 管道關閉函數
void CloseFifo(int fd)
{close(fd);
}
代碼3.4:讀端源文件(shmServe.cc)代碼
#include "common.hpp"// 全局類對象
// 構造和析構函數分別負責管道文件的創建和銷毀
Init init;int main()
{// 獲取共享內存key值key_t k = ftok(PATH_NAME, PROJ_ID); if(k == -1){perror("Serve ftok");exit(1);}printf("Serve# 成功獲取key值,key:%d\n", k);// 創建共享內存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("Sreve shmget");exit(2);}printf("Serve# 共享內存獲取成功,shmid:%d\n", shmid);// 將共享內存與進程相關聯char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Serve shmat");exit(3);}printf("Serve# 共享內存與進程成功關聯,shmid:%d\n", shmid);// 通信代碼int fd = OpenFifo(FIFO_NAME, READ); // 只讀方式打開管道文件while(true){Wait(fd); // 等待讀取printf("[Client say]# %s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;}// while(true)// {// printf("[Client say]# %s\n", shmaddr);// if(strcmp(shmaddr, "quit") == 0) break;// sleep(1);// }// 讓共享內存脫離當前進程int n = shmdt(shmaddr);if(n == -1){perror("Serve shmdt");exit(4);}printf("Serve# 共享內存成功脫離進程,shmid:%d\n", shmid);// 刪除共享內存n = shmctl(shmid, IPC_RMID, NULL);if(n == -1){perror("Serve shmctl");exit(5);}printf("Serve# 共享內存刪除成功,shmid:%d\n", shmid);CloseFifo(fd);return 0;
}
代碼3.5:寫端源文件(shmClient.cc)代碼
#include "common.hpp"int main()
{// 獲取共享內存key值key_t k = ftok(PATH_NAME, PROJ_ID); if(k == -1){perror("Client ftok");exit(1);}printf("Client# 成功獲取key值,key:%d\n", k);// 創建共享內存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("Client shmget");exit(2);}printf("Client# 共享內存獲取成功,shmid:%d\n", shmid);// 將共享內存與進程相關聯char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Client shmat");exit(3);}printf("Client# 共享內存與進程成功關聯,shmid:%d\n", shmid);// 通信代碼int fd = OpenFifo(FIFO_NAME, WRITE);while(true){ssize_t sz = read(0, shmaddr, SIZE); // 共享內存從鍵盤中讀入數據(換行符也被寫入)assert(sz >= 0);shmaddr[sz - 1] = '\0'; //末尾添加'\0'表示終止WakeUp(fd); // 喚醒讀端進程if(strcmp(shmaddr, "quit") == 0) break;}// char ch = 'a';// int count = 0;// for(; ch <= 'c'; ++ch)// {// shmaddr[count++] = ch;// printf("write succsee# %s\n", shmaddr);// sleep(3);// }// snprintf(shmaddr, SIZE, "quit");// 讓共享內存脫離當前進程int n = shmdt(shmaddr);if(n == -1){perror("Client shmdt");exit(4);}printf("Client# 共享內存成功脫離進程,shmid:%d\n", shmid);CloseFifo(fd);return 0;
}
四.?總結
- System V共享內存實現進程間通信的底層原理是通信雙方進程看到同一塊內存,位于物理內存上的共享內存塊,通過頁表映射到通信雙方的進程地址空間的共享區,通信雙方拿到共享區的虛擬地址,通過頁表映射,訪問到同一塊物理內存。
- 使用System V共享內存實現進程間通信的操作流程為:通過ftok函數獲取唯一的共享內存標識符key ->?通過shmget函數獲取共享內存 ->?通過shmat函數讓共享內存和進程綁定 -> 【進行進程通信】->?通過shmdt函數讓共享內存和進程脫離 ->?通過shmctl刪除共享內存。
- System V共享內存 進程間通信的特點為:(1)不需要向操作系統內核中拷貝數據,是所有進程間通信的方法中效率最高的。(2)沒有訪問控制。
- 通過管道的輔助,可以為?System V共享內存 進程間通信添加訪問控制。