出處
http://www.blogjava.net/sunzhong/articles/297435.html
共用記憶體地區是被多個進程共用的一部分實體記憶體。如果多個進程都把該記憶體區域對應到自己的虛擬位址空間,則這些進程就都可以直接存取該共用記憶體地區,從而可以通過該地區進行通訊。共用記憶體是進程間共用資料的一種最快的方法,一個進程向共用記憶體地區寫入了資料,共用這個記憶體地區的所有進程就可以立刻看到其中的內容。這塊共用虛擬記憶體的頁面,出現在每一個共用該頁面的進程的頁表中。但是它不需要在所有進程的虛擬記憶體中都有相同的虛擬位址。
圖 共用記憶體映射圖
象所有的 System V IPC對象一樣,對於共用記憶體對象的擷取是由key控制。記憶體共用之後,對進程如何使用這塊記憶體就不再做檢查。它們必須依賴於其它機制,比如System V的號誌來同步對於共用記憶體地區的訪問(號誌如何控制對臨界代碼的訪問另起一篇說話)。
每一個新建立的共用記憶體對象都用一個shmid_kernel資料結構來表達。系統中所有的shmid_kernel資料結構都儲存在shm_segs向量表中,該向量表的每一個元素都是一個指向shmid_kernel資料結構的指標。
shm_segs向量表的定義如下:
struct shmid_kernel *shm_segs[SHMMNI];
SHMMNI為128,表示系統中最多可以有128個共用記憶體對象。
資料結構shmid_kernel的定義如下:
struct shmid_kernel
{
struct shmid_ds u; /* the following are private */
unsigned long shm_npages; /* size of segment (pages) */
unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */
struct vm_area_struct *attaches; /* descriptors for attaches */
};
其中:
shm_pages代表該共用記憶體對象的所佔據的記憶體頁面數組,數組裡面的每個元素當然是每個記憶體頁面的起始地址.
shm_npages則是該共用記憶體對象佔用記憶體頁面的個數,以頁為單位。這個數量當然涵蓋了申請空間的最小整數倍.
(A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE)
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 */
};
attaches描述被共用的實體記憶體對象所映射的各進程的虛擬記憶體地區。每一個希望共用這塊記憶體的進程都必須通過系統調用將其關聯(attach)到它的虛擬記憶體中。這一過程將為該進程建立了一個新的描述這塊共用記憶體的vm_area_struct資料結構。建立時可以指定共用記憶體在它的虛擬位址空間的位置,也可以讓Linux自己為它選擇一塊足夠的空閑地區。
這個新的vm_area_struct結構是維繫共用記憶體和使用它的進程之間的關係的,所以除了要關聯進程資訊外,還要指明這個共用記憶體資料結構shmid_kernel所在位置; 另外,便於管理這些經常變化的vm_area_struct,所以採取了鏈表形式組織這些資料結構,鏈表由attaches指向,同時 vm_area_struct資料結構中專門提供了兩個指標:vm_next_shared和 vm_prev_shared,用於串連該共用地區在使用它的各進程中所對應的vm_area_struct資料結構。
圖 System V IPC 機制 - 共用記憶體
Linux為共用記憶體提供了四種操作。
1. 共用記憶體對象的建立或獲得。與其它兩種IPC機制一樣,進程在使用共用記憶體地區以前,必須通過系統調用sys_ipc (call值為SHMGET)建立一個索引值為key的共用記憶體對象,或獲得已經存在的索引值為key的某共用記憶體對象的引用標識符。以後對共用記憶體對象的訪問都通過該引用標識符進行。對共用記憶體對象的建立或獲得由函數sys_shmget完成,其定義如下:
int sys_shmget (key_t key, int size, int shmflg)
這裡key是表示該共用記憶體對象的索引值,size是該共用記憶體地區的大小(以位元組為單位),shmflg是標誌(對該共用記憶體對象的特殊要求)。
它所做的工作如下:
1) 如果key == IPC_PRIVATE,則總是會建立一個新的共用記憶體對象。
但是 (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more clearly show its function)
* 算出size要佔用的頁數,檢查其合法性。
* 申請一塊記憶體用於建立shmid_kernel資料結構,注意這裡申請的記憶體地區大小不包括真正的共用記憶體區,實際上,要等到第一個進程試圖訪問它的時候才真正建立共用記憶體區。
* 根據該共用記憶體區所佔用的頁數,為其申請一塊空間用於建立頁表(每頁4個位元組),將頁表清0。
* 搜尋向量表shm_segs,為新建立的共用記憶體對象找一個空位置。
* 填寫shmid_kernel資料結構,將其加入到向量表shm_segs中為其找到的空位置。
* 返回該共用記憶體對象的引用標識符。
2) 在向量表shm_segs中尋找索引值為key的共用記憶體對象,結果有三:
* 如果沒有找到,而且在操作標誌shmflg中沒有指明要建立新共用記憶體,則錯誤返回,否則建立一個新的共用記憶體對象。
* 如果找到了,但該次操作要求必須建立一個索引值為key的新對象,那麼錯誤返回。
* 否則,合法性、認證檢查,如有錯,則錯誤返回;否則,返回該記憶體對象的引用標識符。
共用記憶體對象的建立者可以控制對於這塊記憶體的存取權限和它的key是公開還是私人。如果有足夠的許可權,它也可以把共用記憶體鎖定在實體記憶體中。
參見include/linux/shm.h
2. 關聯。在建立或獲得某個共用記憶體地區的引用標識符後,還必須將共用記憶體區域對應(黏附)到進程的虛擬位址空間,然後才能使用該共用記憶體地區。系統調用 sys_ipc(call值為SHMAT)用於共用記憶體區到進程虛擬位址空間的映射,而真正完成黏附動作的是函數sys_shmat,
其定義如下:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
其中:
shmid是shmget返回的共用記憶體對象的引用標識符;
shmaddr用來指定該共用記憶體地區在進程的虛擬位址空間對應的虛擬位址;
shmflg是映射標誌;
返回的是 在進程中的虛擬位址
該函數所做的工作如下:
1) 根據shmid找到共用記憶體對象。
2) 如果shmaddr為0,即使用者沒有指定該共用記憶體地區在它的虛擬空間中的位置,則由系統在進程的虛擬位址空間中為其找一塊地區(從1G開始);否則,就用shmaddr作為映射的虛擬位址。
(If shmaddr is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)
3) 檢查虛擬位址的合法性(不能超過進程的最大虛擬空間大小—3G,不能太接近堆棧棧頂)。
4) 認證檢查。
5) 申請一塊記憶體用於建立資料結構vm_area_struct,填寫該結構。
6) 檢查該記憶體地區,將其加入到進程的mm結構和該共用記憶體對象的vm_area_struct隊列中。
共用記憶體的黏附只是建立一個vm_area_struct資料結構,並將其加入到相應的隊列中,此時並沒有建立真正的共用記憶體頁。
當進程第一次訪問共用虛擬記憶體的某頁時,因為所有的共用記憶體頁還都沒有分配,所以會發生一個page fault異常。當Linux處理這個page fault的時候,它找到發生異常的虛擬位址所在的vm_area_struct資料結構。在該資料結構中包含有這類共用虛擬記憶體的一組處理常式,其中的 nopage操作用來處理虛擬頁對應的物理頁不存在的情況。對共用記憶體,該操作是shm_nopage(定義在ipc/shm.c中)。該操作在描述這個共用記憶體的shmid_kernel資料結構的頁表shm_pages中尋找發生page
fault異常的虛擬位址所對應的頁表條目,看共用頁是否存在(頁表條目為0,表示共用頁是第一次使用)。如果不存在,它就分配一個物理頁,並為它建立一個頁表條目。這個條目不但進入當前進程的頁表,同時也存到shmid_kernel資料結構的頁表shm_pages中。
當下一個進程試圖訪問這塊記憶體並得到一個page fault的時候,經過同樣的路徑,也會走到函數shm_nopage。此時,該函數查看shmid_kernel資料結構的頁表shm_pages時,發現共用頁已經存在,它只需把這裡的頁表項填到進程頁表的相應位置即可,而不需要重新建立物理頁。所以,是第一個訪問共用記憶體頁的進程使得這一頁被建立,而隨後訪問它的其它進程僅把此頁加到它們的虛擬位址空間。
3. 分離。當進程不再需要共用虛擬記憶體的時候,它們與之分離(detach)。只要仍舊有其它進程在使用這塊記憶體,這種分離就只會影響當前的進程,而不會影響其它進程。當前進程的vm_area_struct資料結構被從shmid_ds中刪除,並被釋放。當前進程的頁表也被更新,共用記憶體對應的虛擬記憶體頁被標記為無效。當共用這塊記憶體的最後一個進程與之分離時,共用記憶體頁被釋放,同時,這塊共用記憶體的shmid_kernel資料結構也被釋放。
系統調用sys_ipc (call值為SHMDT) 用於共用記憶體區與進程虛擬位址空間的分離,而真正完成分離動作的是函數
sys_shmdt,其定義如下:
int sys_shmdt (char *shmaddr)
其中shmaddr是進程要分離的共用頁的開始虛擬位址。
該函數搜尋進程的記憶體結構中的所有vm_area_struct資料結構,找到地址shmaddr對應的一個,調用函數do_munmap將其釋放。
在函數do_munmap中,將要釋放的vm_area_struct資料結構從進程的虛擬記憶體中摘下,清除它在進程頁表中對應的頁表項(可能佔多個頁表項).
如果共用的虛擬記憶體沒有被鎖定在實體記憶體中,分離會更加複雜。因為在這種情況下,共用記憶體的頁可能在系統大量使用記憶體的時候被交換到系統的交換磁碟。為了避免這種情況,可以通過下面的控制操作,將某共用記憶體頁鎖定在實體記憶體不允許向外交換。共用記憶體的換出和換入,已在第3章中討論。
4. 控制。Linux在共用記憶體上實現的第四種操作是共用記憶體的控制(call值為SHMCTL的sys_ipc調用),它由函數sys_shmctl實現。控制操作包括獲得共用記憶體對象的狀態,設定共用記憶體對象的參數(如uid、gid、mode、ctime等),將共用記憶體對象在記憶體中鎖定和釋放(在對象的mode上增加或去除SHM_LOCKED標誌),釋放共用記憶體對象資源等。
共用記憶體提供了一種快速靈活的機制,它允許進程之間直接共用大量的資料,而無須使用拷貝或系統調用。共用記憶體的主要局限性是它不能提供同步,如果兩個進程企圖修改相同的共用記憶體地區,由於核心不能序列化這些動作,因此寫的資料可能任意地互相混合。所以使用共用記憶體的進程必須設計它們自己的同步協議,如用號誌等。
以下是使用共用記憶體機制進行處理序間通訊的基本操作:
需要包含的標頭檔:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
1.建立共用記憶體:
int shmget(key_t key,int size,int shmflg);
參數說明:
key:用來表示建立或者已經存在的共用記憶體去的關鍵字。
size:建立共用記憶體的大小。
shmflg:可以指定的特殊標誌。IPC_CREATE,IPC_EXCL以及低九位的許可權。
eg:
int shmid;
shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);
if(shmid==-1)
perror("shmget()");
2.串連共用記憶體
char *shmat(int shmid,char *shmaddr,int shmflg);
參數說明
shmid:共用記憶體的關鍵字
shmaddr:指定共用記憶體出現在進程記憶體位址的什麼位置,通常我們讓核心自己決定一個合適的地址位置,用的時候設為0。
shmflg:制定特殊的標誌位。
eg:
int shmid;
char *shmp;
shmp=shmat(shmid,0,0);
if(shmp==(char *)(-1))
perror("shmat()\n");
3.使用共用記憶體
在使用共用記憶體是需要注意的是,為防止記憶體存取違規,我們一般與訊號量結合使用。
4.分離共用記憶體:當程式不再需要共用內後,我們需要將共用記憶體分離以便對其進行釋放,分離共用記憶體的函數原形如下:
int shmdt(char *shmaddr);
5. 釋放共用記憶體
int shmctl(int shmid,int cmd,struct shmid_ds *buf);