文章目錄:
一、共享內存基本概念
(一)定義
在物理內存上申請一塊地址,多個進程可以將其映射到自己的虛擬地址空間中,所有進程都可以訪問這塊內存的地址。這塊內存就稱爲被共享的內存
。
規範化來說就是:共享內存允許兩個或多個進程共享同一塊存儲區,通過地址映射將這塊物理內存映射到不同進程的地址空間中,多個進程可以通過這塊物理空間進行數據的交互,達到進程間通訊的目的。
如下圖所示:
如果某個進程向共享內存寫入數據,所做的改動將立刻被可以訪問同一段共享內存的其他進程看到,也會影響其他進程讀取到的數據。
因爲共享內存並沒有提供同步機制
,所以會出現多進程的讀取不可控,所以我們需要利用其他機制來同步對共享內存的訪問,如信號量。
(二)原理
在Linux中,每個進程都有屬於自己的進程控制塊PCB,地址空間,頁表。內存管理單元MMU負責將進程的虛擬地址與物理地址進行映射。 兩個不同的虛擬地址空間通過頁表映射到物理內存的同一區域,它們所指向的這塊區域即共享內存
進程通過頁表將虛擬地址映射到物理地址時,在物理地址中有一塊共同的內存區,即共享內存。這塊內存可以被兩個進程看到,那麼當一個進程修改共享內存的數據時,另一個進程訪問共享內存時就會得到新數據。
如果我們用函數指定獲取128字節大小的共享內存時,還是會映射4K大小的共享內存,因爲虛擬地址到物理地址的映射是通過頁表,以頁爲單位進行映射,故一個頁表大小4K的共享內存空間被進程共享,但只能使用128字節。
(三)特點
1.共享內存是最快的IPC(進程間通訊)方式,它只需要兩次數據拷貝
,而管道和消息隊列需要四次數據拷貝:因爲管道和消息隊列進行共享的空間都是由內核對象提供管理,所執行的操作也都是系統調用,而這些數據最終還是要在內存中存儲的。管道使用數組來保存數據,利用write,read系統調用將數據寫入/讀取;消息隊列使用自定義消息結構體保存數據,利用msgsnd,msgrcv將數據保存/讀取到內核中。我們畫出管道的拷貝過程:
所以有四次拷貝,分別是:
- 從內存空間緩衝區將數據拷貝到內核空間緩衝區。
- 從內核緩衝區將數據拷貝到內存
- 從內存將數據拷貝到內核空間緩衝區
- 從內核空間緩衝區將數據拷貝到用戶空間緩衝區。
而共享內存使用相關函數,在內存中開闢一塊空間,映射到不同進程的虛擬地址空間,並且向用戶返回指向該塊內存的指針,因此對該內存可通過指針直接訪問,只需要兩次:
- 從用戶空間緩衝區拷貝數據到內存。
- 從內存拷貝數據到用戶空間緩衝區。
2.可以實現任意兩個進程之間的通訊。
3.父進程forl子進程或者exec執行一個新的程序,在子進程和新程序裏面不會繼承父進程之間使用的共享內存。
4.共享內存沒有提供同步機制,需要配合其他機制實現進程間同步和通訊。
二、共享內存相關函數
內核爲每個共享內存維護着一個結構,該結構包含以下成員
struct shmid_ds {
struct ipc_perm shm_perm; //用戶權限
size_t shm_segsz; //共享內存大小
time_t shm_atime; //最後一次連接時間
time_t shm_dtime; //最後斷開連接時間
time_t shm_ctime; //最後改變事件
pid_t shm_cpid; //PID
pid_t shm_lpid; //最後一個PID
shmatt_t shm_nattch; //多少個進程正在使用這個共享存儲
};
(一)共享內存的創建引用shmget函數
用來創建或獲得一個共享內存,使用shmget函數,函數原型爲:
# include<sys/shm.h>
int shmget(key_t key,size_t size,int flag);
成功返回共享存儲ID,失敗返回-1
每一個共享存儲對應一個ID,唯一標識符,這個函數就返回一個ID。
參數:
- key,flag參數和消息隊列的使用一樣,不再闡述。
- size:指定創建一個共享內存的長度,單位爲字節。如果獲取共享內存,那麼指定爲0。
(二)共享內存的操作shmctl函數
用來控制共享內存shmctl函數,函數原型爲:
# include<sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds* buf);
成功返回0,失敗返回-1。
可以對指定的共享內存進行刪除,獲取信息的操作。
參數:
-
shmid:共享內存標識符
-
cmd:要採取的操作,取值如下:
(1)IPC_STAT:把shmid_ds結構中的數據設置爲共享內存當前的關聯值,即共用內存的當前關聯值覆蓋shmid_ds的值。
(2)IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置爲shmid_ds結構中給出的值。
(3)IPC_RMID:刪除共享內存段。 -
buf:內核爲每個共享內存創建的結構體shmid_ds。
(三)連接共享內存shmat函數
一旦創建/獲得一個共享內存ID,那麼進程就可以調用shmat函數將共享內存連接到它的地址空間中。函數原型爲:
# include<sys/shm.h>
void* shmat(int shmid,const void* addr,int flag);
成功返回指向共享存儲段的指針,出錯返回-1
參數:
- shmid:共享內存標識符
- addr:指定共享內存連接到當前地址中的地址位置。一般爲NULL空,表示讓系統來選擇共享內存的地址。
- flag:標誌位,通常爲0。
(四)斷開共享內存shmdt函數
對共享內存的操作完成時,將共享內存從當前進程分離,函數原型爲:
# include<sys/shm.h>
int shmdt(const void* shmaddr);
成功返回0,出錯返回-1
注意,分離不代表刪除共享內存,只是使該共享內存對當前進程不再可用,直到某個進程帶IPC_RMID命令的調用shmctl刪除共享內存爲止,共享內存才消失。
參數:
- shmaddr:shmat函數返回的地址指針。
執行成功,內核將使與該共享內存相關的shmid_dds結構中的shm_nattch計數器減一。
(五)命令
- 查看系統中的共享存儲段
ipcs -m
- 刪除系統中的共享存儲段
ipcrm -m [shmid]
三、實例
題目: 兩個進程,A進程將獲取到的用戶數據寫入內存,B進程打印共享內存中的數據。
如果我們不對這塊共享內存進行同步控制,就會出現讀取混亂的問題,會出現A寫入的數據還沒有被B讀,A又重新寫入了等問題,所以我們必須要對這塊內存進行一個同步控制,就採用信號量來控制,我們分析一下,如何控制A,B纔可以實現:
A寫數據時,B不能讀數據;B讀數據時,A不能寫數據,B進程只有在A寫入數據後纔可讀取數據,A只有在B取出數據後纔可以繼續發數據。
可以看到A對B有影響,B對A有影響。故需要兩個信號量控制,sem1控制B,sem2控制A。信號量初始值的考慮:運行程序,A進程先寫入數據,B進程處於阻塞狀態,故sem1=0,sem2=1
,那麼我們可以畫出A.c,B.c如何實現對進程的同步,達到要求:
那我們就可以按照這個控制方法寫出代碼:
- 先進行shmget函數對內存空間的創建和初始化,再連接共享內存,用ptr保存shmat連接到的內存地址。
- 信號量的創建,初始化。
- 進行循環讀取數據,對sem2信號量P操作,進行數據的寫入,寫完之後對sem1信號量V操作。
B進程這一塊相反,
- shmdt斷開共享內存連接。
- shcmtl刪除共享內存。
A.C
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<sys/shm.h>
# include<string.h>
# include "./sem.h"
int main()
{
int shmid=shmget((key_t)1234,128,IPC_CREAT|0664);//A先運行,創建共享內存
assert(shmid!=-1);
char* ptr=(char*)shmat(shmid,NULL,0);//連接共享內存,得到指向共享內存塊的地址
assert(ptr!=(char*)-1);
//創建兩個信號量sem1控制B,sem2控制A
int init_val[2]={0,1};
int semid=CreateSem(1234,init_val,2);
assert(semid!=-1);
while(1)
{
SemP(semid,1);//對sem2進行P操作
printf("input:");
fgets(ptr,127,stdin);
SemV(semid,0);//對sem1進行V操作
if(strncmp(ptr,"end",3)==0)
{
break;
}
}
shmdt(ptr);//斷開連接
shmctl(shmid,IPC_RMID,NULL);//刪除共享內存塊
}
B.C
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<sys/shm.h>
# include<string.h>
# include "./sem.h"
int main()
{
int shmid=shmget((key_t)1234,128,IPC_CREAT|0664);//獲得共享內存
assert(shmid!=-1);
char* ptr=(char*)shmat(shmid,NULL,0);
assert(ptr!=(char*)-1);
//創建兩個信號量sem1控制B,sem2控制A
int init_val[2]={0,1};
int semid=CreateSem(1234,init_val,2);
assert(semid!=-1);
while(1)
{
SemP(semid,0);//對sem1進行P操作
if(strncmp(ptr,"end",3)==0)
{
break;
}
printf("B process:%s\n",ptr);
sleep(3);
printf("B over\n");
memset(ptr,0,128);
SemV(semid,1);//對sem2進行V操作
}
shmdt(ptr);
shmctl(shmid,IPC_RMID,NULL);
}
可以看到代碼和我們控制的一樣。
加油哦!💪。