轉自:http://blog.csdn.net/icechenbing/article/details/7715472
最近準備用c++做些東西, 遇到共享內存的問題,在網上搜索到這篇文件,轉載保留下
一. 共享內存介紹
系統V共享內存指的是把所有共享數據放在共享內存區域(IPC shared memory region),任何想要訪問該數據的
進程都必須在本進程的地址空間新增一塊內存區域,用來映射存放共享數據的物理內存頁面。系統調用mmap()通
過映射一個普通文件實現共享內存。系統V則是通過映射shm文件系統中的文件實現進程間的共享內存通信。
也就是說,每個共享內存區域對應shm文件系統的一個文件.
二、系統V共享內存API
對於系統V共享內存,主要有以下幾個API:shmget()、shmat()、shmdt()及shmctl()。
#include <sys/ipc.h>
#include <sys/shm.h>
shmget()用來獲得共享內存區域的ID,如果不存在指定的共享區域就創建相應的區域。
shmat()把共享內存區域映射到調用進程的地址空間中去,這樣,進程就可以方便地對共享區域進行訪問操作。
shmdt()調用用來解除進程對共享內存區域的映射。
shmctl實現對共享內存區域的控制操作。
注:shmget的內部實現包含了許多重要的系統V共享內存機制;shmat在把共享內存區域映射到進程空間時,並不真正改變進程的頁表。當進程第一次訪問內存映射區域訪問時,會因爲沒有物理頁表的分配而導致一個缺頁異常,然後內核再根據相應的存儲管理機制爲共享內存映射區域分配相應的頁表。
三. 系統V共享內存範例
範例1
兩個進程, 進程A創建一塊共享內存, 寫下Hello, World然後退出. 進程B根據key得到進程A創建的共享內存, 然後讀取
共享內存中的數據. 並打印出來. 示意圖如下:
進程A的代碼:
- #include <sys/types.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <error.h>
- #define SHM_SIZE 4096
- #define SHM_MODE (SHM_R | SHM_W) /* user read/write */
- int main(void)
- {
- int shmid;
- char *shmptr;
- if ( (shmid = shmget(0x44, SHM_SIZE, SHM_MODE | IPC_CREAT)) < 0)
- perror("shmget");
- if ( (shmptr = shmat(shmid, 0, 0)) == (void *) -1)
- perror("shmat");
- /* 往共享內存寫數據 */
- sprintf(shmptr, "%s", "hello, world");
- exit(0);
- }
進程B的代碼:
- #include <sys/types.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <error.h>
- #define SHM_SIZE 4096
- #define SHM_MODE (SHM_R | SHM_W | IPC_CREAT) /* user read/write */
- int main(void)
- {
- int shmid;
- char *shmptr;
- if ( (shmid = shmget(0x44, SHM_SIZE, SHM_MODE | IPC_CREAT)) < 0)
- perror("shmget");
- if ( (shmptr = shmat(shmid, 0, 0)) == (void *) -1)
- perror("shmat");
- /* 從共享內存讀數據 */
- printf("%s\n", shmptr);
- exit(0);
- }
總結:
1、系統V共享內存中的數據,從來不寫入到實際磁盤文件中去;而通過mmap()映射普通文件實現的共享內存通信可以
指定何時將數據寫入磁盤文件中。 注:系統V共享內存機制實際是通過shm文件系統中的文件
實現的,shm文件系統的安裝點在交換分區上,系統重新引導後,所有的內容都丟失。
2、系統V共享內存是隨內核持續的,即使所有訪問共享內存的進程都已經正常終止,共享內存區仍然存在(除非顯式刪
除共享內存),在內核重新引導之前,對該共享內存區域的任何改寫操作都將一直保留。
3、通過調用mmap()映射普通文件進行進程間通信時,一定要注意考慮進程何時終止對通信的影響。而通過系統V共享
內存實現通信的進程則不然。
四. shm文件系統內核實現 (linux-1.2.13)
每一個新創建的共享內存對象都用一個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數據結構。
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);
*****************示例**********************
int *__accept_socketfd;
int shmid = shmget(SHAREMEMID,sizeof(int),IPC_CREAT|0666);
if (( __accept_socketfd = (int *)shmat(shmid,NULL,0 )) == (int *)-1 )
{
printf("Error:shmat\n");
return;
}
*__accept_socketfd = 0;