介紹內存共享前,說下之前的誤區,覺得,可以用指針來在父子進程中傳遞數據,其實,在fork()后,父子進程的地址空間是相互獨立的!所以在父子進程間傳遞指針是沒有意義的。
這里就涉及到物理地址和邏輯地址(或稱虛擬地址)的概念。
從邏輯地址到物理地址的映射稱為地址重定向。分為:
靜態重定向--在程序裝入主存時已經完成了邏輯地址到物理地址和變換,在程序執行期間不會再發生改變。
動態重定向--程序執行期間完成,其實現依賴於硬件地址變換機構,如基址寄存器。
邏輯地址:CPU所生成的地址。CPU產生的邏輯地址被分為 :p (頁號) 它包含每個頁在物理內存中的基址,用來作為頁表的索引;d (頁偏移),同基址相結合,用來確定送入內存設備的物理內存地址。
用戶程序看不見真正的物理地址。用戶只生成邏輯地址,且認為進程的地址空間為0到max。物理地址范圍從R+0到R+max,R為基地址,地址映射-將程序地址空間中使用的邏輯地址變換成內存中的物理地址的過程。由內存管理單元(MMU)來完成。
fork()會產生一個和父進程完全相同的子進程,但子進程在此后多會exec系統調用,出於效率考慮,linux中引入了“寫時復制“技術,也就是只有進程空間的各段的內容要發生變化時,才會將父進程的內容復制一份給子進程。在fork之后exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間,如果不是因為exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。
fork時子進程獲得父進程數據空間、堆和棧的復制,所以變量的地址(當然是虛擬地址)也是一樣的。
每個進程都有自己的虛擬地址空間,不同進程的相同的虛擬地址顯然可以對應不同的物理地址。因此地址相同(虛擬地址)而值不同沒什么奇怪。
具體過程是這樣的:
fork子進程完全復制父進程的棧空間,也復制了頁表,但沒有復制物理頁面,所以這時虛擬地址相同,物理地址也相同,但是會把父子共享的頁面標記為“只讀”(類似mmap的private的方式),如果父子進程一直對這個頁面是同一個頁面,知道其中任何一個進程要對共享的頁面“寫操作”,這時內核會復制一個物理頁面給這個進程使用,同時修改頁表。而把原來的只讀頁面標記為“可寫”,留給另外一個進程使用。
這就是所謂的“寫時復制”。正因為fork采用了這種寫時復制的機制,所以fork出來子進程之后,父子進程哪個先調度呢?內核一般會先調度子進程,因為很多情況下子進程是要馬上執行exec,會清空棧、堆。。這些和父進程共享的空間,加載新的代碼段。。。,這就避免了“寫時復制”拷貝共享頁面的機會。如果父進程先調度很可能寫共享頁面,會產生“寫時復制”的無用功。所以,一般是子進程先調度滴。
假定父進程malloc的指針指向0x12345678, fork 后,子進程中的指針也是指向0x12345678,但是這兩個地址都是虛擬內存地址 (virtual memory),經過內存地址轉換后所對應的 物理地址是不一樣的。所以兩個進城中的這兩個地址相互之間沒有任何關系。
(注1:在理解時,你可以認為fork后,這兩個相同的虛擬地址指向的是不同的物理地址,這樣方便理解父子進程之間的獨立性)
(注2:但實際上,Linux為了提高 fork 的效率,采用了 copy-on-write 技術,fork后,這兩個虛擬地址實際上指向相同的物理地址(內存頁),只有任何一個進程試圖修改這個虛擬地址里的內容前,兩個虛擬地址才會指向不同的物理地址(新的物理地址的內容從原物理地址中復制得到))
參考:http://blog.csdn.net/xy010902100449/article/details/44851453
http://blog.csdn.net/gatieme/article/details/51005811
在 linux 系統中,每個進程的虛擬內存是被分為許多頁面的。這些內存頁面中包含了實際的數據。每個進程都會維護一個從內存地址到虛擬內存頁面之間的映射關系。盡管每個進程都有自己的內存地址,不同的進程可以同時將同一個內存頁面映射到自己的地址空間中,從而達到共享內存的目的。
分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標識該內存塊的標識符。
一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。
這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之后,這個映射關系將被刪除。
當再也沒有進程需要使用這個共享內存塊的時候,必須有一個(且只能是一個)進程負責釋放這個被共享的內存頁面。
所有共享內存塊的大小都必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個內存頁面包含的字節數。在 Linux 系統中,內存頁面大小是4KB,不過您仍然應該通過調用 getpagesize 獲取這個值。
共享內存的實現分為兩個步驟:
創建共享內存,使用shmget函數。
映射共享內存,將這段創建的共享內存映射到具體的進程空間去,使用shmat函數。
用於共享內存的函數
共享內存的使用,主要有以下幾個API:ftok()
、shmget()
、shmat()
、shmdt()
及shmctl()。
#include <sys/shm.h> void *shmat(int shm_id, const void *shm_addr, int shmflg); int shmctl(int shm_id, int cmd, struct shmid_ds *buf); int shmdt(const void *shm_addr); int shmget(key_t key, size_t size, int shmflg);
與信號量相類似,通常需要在包含shm.h文件之前包含sys/types.h與sys/ipc.h這兩個頭文件。
用ftok()函數獲得一個ID號
應用說明,在IPC中,我們經常用用key_t的值來創建或者打開信號量,共享內存和消息隊列。
key_t ftok(const char *pathname, int proj_id);
- 1
- 1
參數 | 描述 |
---|---|
pathname | 一定要在系統中存在並且進程能夠訪問的 |
proj_id | 一個1-255之間的一個整數值,典型的值是一個ASCII值。 |
當成功執行的時候,一個key_t值將會被返回,否則-1被返回。我們可以使用strerror(errno)來確定具體的錯誤信息。
考慮到應用系統可能在不同的主機上應用,可以直接定義一個key,而不用ftok獲得:
#define IPCKEY 0x344378
創建共享內存
進程通過調用shmget(Shared Memory GET,獲取共享內存)來分配一個共享內存塊。
int shmget(key_t key ,int size,int shmflg)
參數 | 描述 |
---|---|
key | 一個用來標識共享內存塊的鍵值 |
size | 指定了所申請的內存塊的大小 |
shmflg | 操作共享內存的標識 |
返回值:如果成功,返回共享內存表示符,如果失敗,返回-1。
- 該函數的第二個參數key是一個用來標識共享內存塊的鍵值。
彼此無關的進程可以通過指定同一個鍵以獲取對同一個共享內存塊的訪問。不幸的是,其它程序也可能挑選了同樣的特定值作為自己分配共享內存的鍵值,從而產生沖突。
用特殊常量IPC_PRIVATE作為鍵值可以保證系統建立一個全新的共享內存塊。|
key標識共享內存的鍵值:0/IPC_PRIVATE。當key的取值為IPC_PRIVATE,則函數shmget將創建一塊新的共享內存;如果key的取值為0,而參數中又設置了IPC_PRIVATE這個標志,則同樣會創建一塊新的共享內存。
- 該函數的第二個參數size指定了所申請的內存塊的大小。
因為這些內存塊是以頁面為單位進行分配的,實際分配的內存塊大小將被擴大到頁面大小的整數倍。
- 第三個參數shmflg是一組標志,通過特定常量的按位或操作來shmget。這些特定常量包括:
IPC_CREAT:這個標志表示應創建一個新的共享內存塊。通過指定這個標志,我們可以創建一個具有指定鍵值的新共享內存塊。
IPC_EXCL:這個標志只能與 IPC_CREAT 同時使用。當指定這個標志的時候,如果已有一個具有這個鍵值的共享內存塊存在,則shmget會調用失敗。也就是說,這個標志將使線程獲得一個“獨有”的共享內存塊。如果沒有指定這個標志而系統中存在一個具有相同鍵值的共享內存塊,shmget會返回這個已經建立的共享內存塊,而不是重新創建一個。
模式標志:這個值由9個位組成,分別表示屬主、屬組和其它用戶對該內存塊的訪問權限。
其中表示執行權限的位將被忽略。指明訪問權限的一個簡單辦法是利用
映射共享內存
shmat()是用來允許本進程訪問一塊共享內存的函數,將這個內存區映射到本進程的虛擬地址空間。
int shmat(int shmid,char *shmaddr,int flag)
參數 | 描述 |
---|---|
shmid | 那塊共享內存的ID,是shmget函數返回的共享存儲標識符 |
shmaddr | 是共享內存的起始地址,如果shmaddr為0,內核會把共享內存映像到調用進程的地址空間中選定位置;如果shmaddr不為0,內核會把共享內存映像到shmaddr指定的位置。所以一般把shmaddr設為0。 |
shmflag | 是本進程對該內存的操作模式。如果是SHM_RDONLY的話,就是只讀模式。其它的是讀寫模式 |
成功時,這個函數返回共享內存的起始地址。失敗時返回-1。
要讓一個進程獲取對一塊共享內存的訪問,這個進程必須先調用 shmat(SHared Memory Attach,綁定到共享內存)。
將 shmget 返回的共享內存標識符 SHMID 傳遞給這個函數作為第一個參數。
該函數的第二個參數是一個指針,指向您希望用於映射該共享內存塊的進程內存地址;如果您指定NULL則Linux會自動選擇一個合適的地址用於映射。第三個參數是一個標志位,包含了以下選項:
SHM_RND表示第二個參數指定的地址應被向下靠攏到內存頁面大小的整數倍。如果您不指定這個標志,您將不得不在調用shmat的時候手工將共享內存塊的大小按頁面大小對齊。
SHM_RDONLY表示這個內存塊將僅允許讀取操作而禁止寫入。 如果這個函數調用成功則會返回綁定的共享內存塊對應的地址。通過 fork 函數創建的子進程同時繼承這些共享內存塊;
如果需要,它們可以主動脫離這些共享內存塊。 當一個進程不再使用一個共享內存塊的時候
共享內存解除映射
當一個進程不再需要共享內存時,需要把它從進程地址空間中多里。
int shmdt(char *shmaddr)
參數 | 描述 |
---|---|
shmaddr | 那塊共享內存的起始地址 |
成功時返回0。失敗時返回-1。
應通過調用 shmdt(Shared Memory Detach,脫離共享內存塊)函數與該共享內存塊脫離。將由 shmat 函數返回的地址傳遞給這個函數。如果當釋放這個內存塊的進程是最后一個使用該內存塊的進程,則這個內存塊將被刪除。對 exit 或任何exec族函數的調用都會自動使進程脫離共享內存塊。
控制釋放
shmctl控制對這塊共享內存的使用
函數原型
int shmctl( int shmid , int cmd , struct shmid_ds *buf );
參數 | 描述 |
---|---|
shmid | 是共享內存的ID。 |
cmd | 控制命令 |
buf | 一個結構體指針。IPC_STAT的時候,取得的狀態放在這個結構體中。如果要改變共享內存的狀態,用這個結構體指定。 |
其中cmd的取值如下
cmd | 描述 |
---|---|
IPC_STAT | 得到共享內存的狀態 |
IPC_SET | 改變共享內存的狀態 |
IPC_RMID | 刪除共享內存 |
返回值: 成功:0 失敗:-1
調用 shmctl(”Shared Memory Control”,控制共享內存)函數會返回一個共享內存塊的相關信息。同時 shmctl 允許程序修改這些信息。
該函數的第一個參數是一個共享內存塊標識。
要獲取一個共享內存塊的相關信息,則為該函數傳遞 IPC_STAT 作為第二個參數,同時傳遞一個指向一個 struct shmid_ds 對象的指針作為第三個參數。
要刪除一個共享內存塊,則應將 IPC_RMID 作為第二個參數,而將 NULL 作為第三個參數。當最后一個綁定該共享內存塊的進程與其脫離時,該共享內存塊將被刪除。
您應當在結束使用每個共享內存塊的時候都使用 shmctl 進行釋放,以防止超過系統所允許的共享內存塊的總數限制。調用 exit 和 exec 會使進程脫離共享內存塊,但不會刪除這個內存塊。 要查看其它有關共享內存塊的操作的描述,請參考shmctl函數的手冊頁。
示例
簡單映射一塊共享內存
#include <stdio.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> #define IPCKEY 0x366378 typedef struct st_setting { char agen[10]; unsigned char file_no; }st_setting; int main(int argc, char** argv) { int shm_id; //key_t key; st_setting *p_setting; // 首先檢查共享內存是否存在,存在則先刪除 shm_id = shmget(IPCKEY , 1028, 0640); if(shm_id != -1) { p_setting = (st_setting *)shmat(shm_id, NULL, 0); if (p_setting != (void *)-1) { shmdt(p_setting); shmctl(shm_id,IPC_RMID,0) ; } } // 創建共享內存 shm_id = shmget(IPCKEY, 1028, 0640 | IPC_CREAT | IPC_EXCL); if(shm_id == -1) { printf("shmget error\n"); return -1; } // 將這塊共享內存區附加到自己的內存段 p_setting = (st_setting *)shmat(shm_id, NULL, 0); strncpy(p_setting->agen, "gatieme", 10); printf("agen : %s\n", p_setting->agen); p_setting->file_no = 1; printf("file_no : %d\n",p_setting->file_no); system("ipcs -m");// 此時可看到有進程關聯到共享內存的信息,nattch為1 // 將這塊共享內存區從自己的內存段刪除出去 if(shmdt(p_setting) == -1) perror(" detach error "); system("ipcs -m");// 此時可看到有進程關聯到共享內存的信息,nattch為0 // 刪除共享內存 if (shmctl( shm_id , IPC_RMID , NULL ) == -1) { perror(" delete error "); } system("ipcs -m");// 此時可看到有進程關聯到共享內存的信息,nattch為0 return EXIT_SUCCESS; }