參考 http://www.startos.com/linux/tips/2011012822078.html
1)Linux和所有的UNIX操作系統都允許通過共享內存在應用程序之間共享存儲空間.
2)有兩類基本的API函數用於在進程間共享內存:System v和POSIX. (當然,還有mmap,屬於POSIX的)
3)這兩類函數上使用相同的原則,核心思想就是任何要被共享的內存都必須經過顯示的分配.
4)因為所有進程共享同一塊內存,共享內存在各種進程間通信方式中具有最高的效率.
5)內核沒有對訪問共享內存進行同步,所以必須提供自己的同步措施,比如數據在寫入之前,不允許其它進程對其進行讀寫.可以用wait來解決這個問題.
二)POSIX共享內存API
1)函數shm_open和shm_unlink非常類似於為普通文件所提供的open和unlink系統調用.
2)如果要編寫一個可移植的程序,那么shm_open和shm_unlink是最好的選擇.
3)shm_open:創建一個新的共享區域或者附加在已有的共享區域上.區域被其名字標識,函數返回各文件的描述符.
4)shm_unlink:類似於unlink系統調用對文件進行操作,直到所有的進程不再引用該內存區后才對其進行釋放.
5)mmap:用於將一個文件映射到某一內存區中,其中也使用了shm_open函數返回的文件描述符.
6)munmap:用於釋放mmap所映射的內存區域.
7)msync:同步存取一個映射區域並將高速緩存的數據回寫到物理內存中,以便其他進程可以監聽這些改變.
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/file.h> #include <sys/mman.h> #include <sys/wait.h> void error_out(const char *msg) { perror(msg); exit(EXIT_FAILURE); } int main (int argc, char *argv[]) { int r; const char *memname = "/mymem"; const size_t region_size = sysconf(_SC_PAGE_SIZE); int fd = shm_open(memname, O_CREAT|O_TRUNC|O_RDWR, 0666); if (fd == -1) error_out("shm_open"); r = ftruncate(fd, region_size); if (r != 0) error_out("ftruncate"); void *ptr = mmap(0, region_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (ptr == MAP_FAILED) error_out("MMAP"); close(fd); pid_t pid = fork(); if (pid == 0) { u_long *d = (u_long *)ptr; *d = 0xdeadbeef; exit(0); } else { int status; waitpid(pid, &status, 0); printf("child wrote %#lx\n", *(u_long *)ptr); } sleep(50); r = munmap(ptr, region_size); if (r != 0) error_out("munmap"); r = shm_unlink(memname); if (r != 0) error_out("shm_unlink"); return 0; }
編譯:
gcc -o postix-shm postix-shm.c -lrt
./postix-shm
child wrote 0xdeadbeef
等50秒后,程序退出.
-l表示鏈接指定庫
rt應該是庫名
POSIX.1b Realtime Extensions library
程序分析:
1)程序執行shm_open函數創建了共享內存區域,此時會在/dev/shm/創建mymem文件.
2)通過ftruncate函數改變shm_open創建共享內存的大小為頁大小(sysconf(_SC_PAGE_SIZE)),如果不執行ftruncate函數的話,會報Bus error的錯誤. (其實大小指定成多少都可以,1024也行,2048也行(page size的倍數?),但是一定要用ftruncate來將文件改成指定的大小,后面mmap要用的)
函數說明:ftruncate()會將參數fd指定的文件大小改為參數length指定的大小。參數fd為已打開的文件描述詞,而且必須是以寫入模式打開的文件。如果原來的文件件大小比參數length大,則超過的部分會被刪去 返 回 值:0、-1 錯誤原因:errno EBADF 參數fd文件描述詞為無效的或該文件已關閉 EINVAL 參數fd為一socket並非文件,或是該文件並非以寫入模式打開
3)通過mmap函數將創建的mymem文件映射到內存.
4)通過fork派生出子進程,而共享區域映射通過fork調用而被繼承.
5)程序通過wait系統調用來保持父進程與子進程的同步.
6)在非父子進程也可以通過共享內存區域的方式進行通訊.
Linux共享內存的實現依賴於共享內存文件系統,該文件系統通常裝載在/dev/shm,在調用shm_open系統函數的時候,會在/dev/shm/目錄下生成mymem文件.
而后程序調用shm_unlink刪除mymem,這里如果卸載掉/dev/shm掛載點會怎么樣呢?
查看分區信息 df -h Filesystem Size Used Avail Use% Mounted on /dev/sda1 19G 973M 17G 6% / tmpfs 253M 0 253M 0% /lib/init/rw udev 10M 88K 10M 1% /dev tmpfs 253M 0 253M 0% /dev/shm 卸載/dev/shm umount /dev/shm/ ./posix-shm & child wrote 0xdeadbeef [1] 15476 ls -l /dev/shm/mymem -rw-r--r-- 1 root root 4096 2010-10-26 14:25 /dev/shm/mymem 我們看到shm_open只是在/dev/shm下創建文件.而不管/dev/shm是否是用tmpfs類型掛載的分區. 如果刪除/dev/shm呢? rmdir /dev/shm 再次執行posix-shm ./posix-shm & child wrote 0xdeadbeef 此時程序找不到/dev/shm,而在/dev/目錄下建立共享內存文件 ls -l /dev/mymem -rw-r--r-- 1 root root 4096 2010-10-26 14:29 /dev/mymem
三)System V共享內存 API
1)System V API廣泛應用於X windows系統及其擴展版本中,許多X應用程序也使用它.
2)shmget:創建一個新的共享區域或者附加在已有的共享區域上(同shm_open).
3)shmat:用於將一個文件映射到內存區域中(同mmap).
4)shmdt:用於釋放所映射的內存區域(同munmap)
5)shmctl:對於多個用戶,斷開其對共享區域的連接(同shm_unlink)
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/wait.h> void error_out(const char *msg) { perror(msg); exit(EXIT_FAILURE); } int main (int argc, char *argv[]) { key_t mykey = 12345678; const size_t region_size = sysconf(_SC_PAGE_SIZE); int smid = shmget(mykey, region_size, IPC_CREAT|0666); if(smid == -1) error_out("shmget"); void *ptr; ptr = shmat(smid, NULL, 0); if (ptr == (void *) -1) error_out("shmat"); pid_t pid = fork(); if (pid == 0){ u_long *d = (u_long *)ptr; *d = 0xdeadbeef; exit(0); } else{ int status; waitpid(pid, &status, 0); printf("child wrote %#lx/n", *(u_long *)ptr); } sleep(30); int r = shmdt(ptr); if (r == -1) error_out("shmdt"); r = shmctl(smid, IPC_RMID, NULL); if (r == -1) error_out("shmdt"); return 0; }
gcc sysv-shm.c -o sysv-shm -lrt
./sysv-shm
child wrote 0xdeadbeef
程序分析:
1)shmget函數使用的key_t變量在功能上等價於shm_open使用的文件名,由shmget返回的smid在功能上等價於shm_open返回的文件描述符.
2)不同於POSIX API所創建的內存區,System V API創建的內存區在任何文件系統中都是不可見的.
3)可以用ipcs管理System V API共享內存.
共享內存的內部實現
參考 http://blog.csdn.net/taomeegaoke/article/details/7493319
每一個新創建的共享內存對象都用一個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;
unsigned long shm_npages;
unsigned long *shm_pages;
struct vm_area_struct *attaches;
};
(A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE)
shmid_ds是一個數據結構,它描述了這個共享內存區的認證信息,字節大小,最后一次粘附時間、分離時間、改變時間,創建該共享區域的進程,最后一次對它操作的進程,當前有多少個進程在使用它等信息。
創建時可以指定共享內存在它的虛擬地址空間的位置,也可以讓Linux自己為它選擇一塊足夠的空閑區域。
如下:
不管上面哪種IPC(System V或者Posix),在Linux上面都是Linux提供的系統調用來實現的。
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數據結構,並將其加入到相應的隊列中,此時並沒有創建真正的共享內存頁。
非常重要,shmat的時候也沒有創建真正的共享內存頁。
當進程第一次訪問共享虛擬內存的某頁時,因為所有的共享內存頁還都沒有分配,所以會發生一個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時,發現共享頁已經存在,它只需把這里的頁表項填到進程頁表的相應位置即可,而不需要重新創建物理頁。所以,是第一個訪問共享內存頁的進程使得這一頁被創建,而隨后訪問它的其它進程僅把此頁加到它們的虛擬地址空間。
(注:我的理解:因為共享內存訪問,都是對於內存位置的實際訪問。所以在訪問的時候,直接去訪問進程空間中的那部分地址,是通過頁表來映射的,如果頁表沒有這個位置的映射,就會觸發page fault異常,交給系統處理;而系統就會調用到上面的流程)(具體的,還要學習中斷與異常等內容)
3. 分離。當進程不再需要共享虛擬內存的時候,它們與之分離(detach)。只要仍舊有其它進程在使用這塊內存,這種分離就只會影響當前的進程,而不會影響其它進程。當前進程的vm_area_struct數據結構被從shmid_ds中刪除,並被釋放。當前進程的頁表也被更新,共享內存對應的虛擬內存頁被標記為無效。
關於 vm_area_struct
這個新的vm_area_struct結構是維系共享內存和使用它的進程之間的關系的,所以除了要關聯進程信息外,還要指明這個共享內存數據結構shmid_kernel所在位置; 另外,便於管理這些經常變化的vm_area_struct,所以采取了鏈表形式組織這些數據結構,鏈表由attaches指向,同時 vm_area_struct數據結構中專門提供了兩個指針:vm_next_shared和 vm_prev_shared,用於連接該共享區域在使用它的各進程中所對應的vm_area_struct數據結構。
分離時,物理內存與交換磁盤
如果共享的虛擬內存沒有被鎖定在物理內存中,分離會更加復雜。因為在這種情況下,共享內存的頁可能在系統大量使用內存的時候被交換到系統的交換磁盤。為了避免這種情況,可以通過下面的控制操作,將某共享內存頁鎖定在物理內存不允許向外交換。共享內存的換出和換入,已在第三章討論。
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()");
連接共享內存
char *shmat(int shmid,char *shmaddr,int shmflg); 參數說明 shmid:共享內存的關鍵字 shmaddr:指定共享內存出現在進程內存地址的什么位置,通常我們讓內核自己決定一個合適的地址位置,用的時候設為0。 shmflg:制定特殊的標志位。 第三個參數如果在flag中指定了SHM_RDONLY位,則以只讀方式連接此段,否則以讀寫的方式連接此段 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 r = shmdt(ptr); if (r == -1) error_out("shmdt"); r = shmctl(smid, IPC_RMID, NULL); if (r == -1) error_out("shmdt");
關於 shmctl
shmctl函數原型
shmctl(共享內存管理) |
||
所需頭文件 |
#include <sys/types.h> #include <sys/shm.h> |
|
函數說明 |
完成對共享內存的控制 |
|
函數原型 |
int shmctl(int shmid, int cmd, struct shmid_ds *buf) |
|
函數傳入值 |
shmid |
共享內存標識符 |
cmd |
IPC_STAT:得到共享內存的狀態,把共享內存的shmid_ds結構復制到buf中 |
|
IPC_SET:改變共享內存的狀態,把buf所指的shmid_ds結構中的uid、gid、mode復制到共享內存的shmid_ds結構內 |
||
IPC_RMID:刪除這片共享內存 |
||
buf |
共享內存管理結構體。具體說明參見共享內存內核結構定義部分 |
|
函數返回值 |
成功:0 |
|
出錯:-1,錯誤原因存於error中 |
||
錯誤代碼 |
EACCESS:參數cmd為IPC_STAT,確無權限讀取該共享內存 EFAULT:參數buf指向無效的內存地址 EIDRM:標識符為shmid的共享內存已被刪除 EINVAL:無效的參數cmd或shmid EPERM:參數cmd為IPC_SET或IPC_RMID,卻無足夠的權限執行 |
注意,其中除了刪除的 IPC_RMID,還有 IPC_STAT和IPC_SET,這時候會用到第三個參數 buf,來復制或者設置 shmid_ds結構。
而這個結構如下:
struct shmid_ds{
struct ipc_perm shm_perm;/* 操作權限*/
int shm_segsz; /*段的大小(以字節為單位)*/
time_t shm_atime; /*最后一個進程附加到該段的時間*/
time_t shm_dtime; /*最后一個進程離開該段的時間*/
time_t shm_ctime; /*最后一個進程修改該段的時間*/
unsigned short shm_cpid; /*創建該段進程的pid*/
unsigned short shm_lpid; /*在該段上操作的最后1個進程的pid*/
short shm_nattch; /*當前附加到該段的進程的個數*/
/*下面是私有的*/
unsigned short shm_npages; /*段的大小(以頁為單位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指針數組*/
struct vm_area_struct *attaches; /*對共享段的描述*/
};