實作程序間通信最簡單也是最直接的方法就是共享記憶體——為參與通信的多個程序在記憶體中開辟一個共享區。由于程序可以直接對共享記憶體進行讀寫操作,是以這種通信方式效率特别高,但其弱點是,它沒有互斥機制,需要信号量之類的手段來配合。
共享記憶體原理與shm系統
共享記憶體,顧名思義,就是兩個或多個程序都可以通路的同一塊記憶體空間,一個程序對這塊空間内容的修改可為其他參與通信的程序所看到的。
顯然,為了達到這個目的,就需要做兩件事:一件是在記憶體劃出一塊區域來作為共享區;另一件是把這個區域映射到參與通信的各個程序空間。
通常在記憶體劃出一個區域的方法是,在記憶體中打開一個檔案,若通過系統調用mmap()把這個檔案所占用的記憶體空間映射到參與通信的各個程序位址空間,則這些程序就都可以看到這個共享區域,進而實作程序間的通信。
為了友善,再把mmap()的原理簡述如下:
mmap()原型如下:
其中,參數fd用來指定被映射的檔案;offset指定映射的起始位置偏移量(通常為0);len指定檔案被映射部分的長度;start用來指定映射到虛位址空間的起始位置(通常為NULL,即由系統确定)。
mmap是一種記憶體映射檔案的方法,即将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對映關系。實作這樣的映射關系後,程序就可以采用指針的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再調用read,write等系統調用函數。相反,核心空間對這段區域的修改也直接反映使用者空間,進而可以實作不同程序間的檔案共享。
mmap()映射過程示意圖如下所示:

那麼mmap是怎麼形成這個檔案映射過程呢?
mmap本身其實是一個很簡單的操作,在程序頁表中添加一個頁表項,該頁表項是實體記憶體的位址。調用mmap的時候,核心會在該程序的位址空間的映射區域查找一塊滿足需求的空間用于映射該檔案,然後生成該虛拟位址的頁表項,改頁表項此時的有效位(标志是否已經在實體記憶體中)為0,頁表項的内容是檔案的磁盤位址,此時mmap的任務已經完成。
簡而言之,就是在程序對應的虛存段添加一個段,也就是建立一個新的vm_area_struct結構,并将其與檔案的實體磁盤位址相連。在建立虛拟區間并完成位址映射,但是并沒有将任何檔案資料的拷貝至主存。真正的檔案讀取是當程序發起讀或寫操作時。程序的讀或寫操作通路虛拟位址空間這一段映射位址,通過查詢頁表,引發缺頁異常,核心進行請頁。
IPC的共享記憶體通信方式與上面的mmap()方式極為相似,但因為建立一個檔案的目的僅是為了通信,于是這種檔案沒有永久儲存的意義,是以IPC并沒有使用正規的檔案系統,而是在系統初始化時在磁盤交換區建立了一個專門用來實作共享記憶體的特殊臨時檔案系統shm,當系統斷電後,其中的檔案會全部自行銷毀。
Linux共享記憶體結構
Linux的一個共享記憶體區由多個共享段組成。用來描述共享記憶體段的核心資料結構shmid_kernel如下:
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm; //描述程序間通信許可的結構
struct file * shm_file; //指向共享記憶體檔案的指針
unsigned long shm_nattch; //挂接到本段共享記憶體的程序數
unsigned long shm_segsz; //段大小
time_t shm_atim; //最後挂接時間
time_t shm_dtim; //最後解除挂接時間
time_t shm_ctim; //最後變化時間
pid_t shm_cprid; //建立程序的PID
pid_t shm_lprid; //最後使用程序的PID
struct user_struct *mlock_user;
};
shmid_kernel中最重要的域是指針shm_file,它指向臨時檔案file對象。當程序需要使用這個檔案進行通信時,由核心負責将其映射到使用者位址空間。
為了便于管理,核心把共享記憶體區的所有描述結構shmid_kernel都存放在結構ipc_id_ary中的一個數組中。結構ipc_id_ary的定義如下:
struct ipc_id_ary
{
int size;
struct kern_ipc_perm *p[0]; //存放段描述結構的數組
};
同樣,為了描述一個共享記憶體區的概貌,核心使用了資料結構ipc_ids。該結構的定義如下:
struct ipc_ids {
int in_use;
unsigned short seq;
unsigned short seq_max;
struct rw_semaphore rw_mutex;
struct idr ipcs_idr;
struct ipc_id_ary *entries; //指向struct ipc_id_ary的指針
};
由多個共享段組成的共享區的結構如下所示:
共享記憶體的使用
頭檔案:
共享記憶體的打開或建立
程序可以通過調用函數shmget()來打開或建立一個共享記憶體區。函數shmget()内部由系統調用sys_shmget來實作。函數shmget()的原型如下:
其中,參數key為使用者給定的鍵值。
所謂的鍵值,是在IPC的通信模式下每個IPC對象的名字。程序通過鍵值識别所有的對象。如果不使用鍵,程序将無法擷取IPC對象,是以IPC對象并不存在于程序本身所使用的的記憶體中。
是以任何程序都無法為一塊共享記憶體定義一個鍵值。是以,在調用函數shmget()時,需要key設為IPC_PRIVATE,這樣,作業系統将忽略鍵,建立一個新的共享記憶體,指定一個鍵值并傳回這塊共享記憶體的IPC辨別符ID,然後再設法将這個新的共享記憶體的辨別符ID告訴其他需要使用這個共享記憶體區的程序。
函數中的參數size為所申請的共享存儲段的長度(以頁為機關)。
函數中的參數flag為标志,常用的有效标志有IPC_CREAT和IPC_EXCL,它們的功能與檔案打開函數open()的O_CREAT和O_EXCL相當。如果使用者希望所建立的共享記憶體區可讀,則需要使用标志S_IRUSR;若可讀,則需要使用标志S_IWUSR。
函數shmget()調用成功後,傳回共享記憶體區的ID,否則傳回-1。
Linux用shmid_ds資料結構表示每個建立的共享記憶體。當shmget()建立一塊新的共享記憶體後,傳回一個可以引用該共享記憶體的shmid_ds資料結構的辨別符。定義在include/linux/shm.h檔案中的shmid_ds如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
例如:調用函數shmget()為目前程序建立一個共享記憶體區。
代碼如下:
int main(void)
{
int shmid;
if((shmid = shmget(IPC_PRIVATE, 10, IPC_CREAT)) < 0)
{
perror("shmget error!");
exit(1);
}else
printf("shmget success!");
return 0;
}
共享記憶體與程序的連接配接
如果一個程序已建立或打開一個共享記憶體,則在需要使用它時,要調用函數shmat()把該共享記憶體連接配接到程序上,即要把待使用的共享記憶體映射到程序空間。函數shmat()通過系統調用sys_shmat()實作。函數shmat()的原型如下:
其中,參數shmid為共享記憶體的辨別;參數shmaddr為映射位址,如果該值為0,則由核心決定;參數shmflg為共享記憶體的标志,如果shmflg的值為SHM_RDONLY,則程序以隻讀的方式通路共享記憶體,否則以讀寫方式通路共享記憶體。
若函數調用成功,則傳回共享存儲段位址;若出錯,則傳回-1。
斷開共享記憶體與程序的連接配接
調用函數shmdt()可以斷開共享記憶體與程序的連接配接,其原型如下:
其中,參數addr為共享存儲段的位址,即調用shmat時的傳回值。shmdt将使相關shmid_ds結構中的shm_nattch計數器值減1。
共享記憶體的控制
調用函數shmctl()可以對共享記憶體進行一些控制,其原型如下:
其中,參數shmid為共享存儲段的ID;參數cmd為控制指令,常用的值有IPC_STAT(指派)、IPC_SET(指派)、IPC_RMID(删除)、SHM_LOCK(上鎖)、SHM_UNLOCK(解鎖)等等;參數buf為struct shmid_ds類型指針,由buf傳回的數值與指令參數cmd表示的操作相關。
共享記憶體不會随着程式的結束而自動消除,要麼調用shmctl()删除,要麼手動使用指令ipcrm -m shmid去删除,否則一直保留在系統中,直至系統掉電。
例子:調用函數shmget()為目前程序建立一個共享記憶體區并使用它。
代碼如下:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <sys/stat.h>
int main(void)
{
int shm_id; //定義共享記憶體鍵
char* shared_memory; //定義共享記憶體指針
struct shmid_ds shmbuffer; //定義共享記憶體緩沖
int shm_size; //定義共享記憶體大小
shm_id = shmget(IPC_PRIVATE, 0x6400, IPC_CREAT | IPC_EXCL | S_IRUSE | S_IWUSE); //建立一個共享記憶體區
shared_memory = (char*)shmat(shm_id, 0, 0); //綁定到共享記憶體
printf("shared memory attached at address %p\n", shared_memory);
shmctl(shm_id, IPC_STAT, &shmbuffer); //讀共享記憶體結構struct shmid_ds
shm_size = shmbuffer.shm_segsz; //自結構struct shmid_ds擷取記憶體大小
printf("segment size:%d\n", shm_size);
sprintf(shared_memory, "Hello,world."); //向共享記憶體中寫入一個字元串
shmdt(shared_memory); //脫離該共享記憶體
shared_memory = (char*)shmat(shm_id, (void *)0x500000, 0); //重新綁定共享記憶體
printf("shared memory reattched at address %p\n", shared_memory);
printf("%s\n", shared_memory);
shmdt(shared_memory); //脫離該共享記憶體
shmctl(shm_id, IPC_RMID, 0); //釋放共享記憶體
return 0;
}
共享記憶體的互斥
從上面的叙述中可以看到,共享記憶體是一種低級的通信機制,它沒有提供程序間同步和互斥的功能。是以,共享記憶體通常是要與信号量結合使用。
原文連結:https://blog.csdn.net/qq_38410730/article/details/81488145?utm_medium=distribute.pc_relevant_right.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase