linux內核剖析(十一)進程間通訊之-共享內存Shared Memory

共享內存


共享內存是進程間通訊中最簡單的方式之一。linux

共享內存是系統出於多個進程之間通信的考慮,而預留的的一塊內存區。函數

共享內存容許兩個或更多進程訪問同一塊內存,就如同 malloc() 函數向不一樣進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。ui

關於共享內存


當一個程序加載進內存後,它就被分紅叫做頁的塊。spa

通訊將存在內存的兩個頁之間或者兩個獨立的進程之間。命令行

總之,當一個程序想和另一個程序通訊的時候,那內存將會爲這兩個程序生成一塊公共的內存區域。這塊被兩個進程分享的內存區域叫作共享內存線程

由於全部進程共享同一塊內存,共享內存在各類進程間通訊方式中具備最高的效率。訪問共享內存區域和訪問進程獨有的內存區域同樣快,並不須要經過系統調用或者其它須要切入內核的過程來完成。同時它也避免了對數據的各類沒必要要的複製。3d

若是沒有共享內存的概念,那一個進程不能存取另一個進程的內存部分,於是致使共享數據或者通訊失效。由於系統內核沒有對訪問共享內存進行同步,您必須提供本身的同步措施。指針

解決這些問題的經常使用方法是經過使用信號量進行同步。不過,咱們的程序中只有一個進程訪問了共享內存,所以在集中展現了共享內存機制的同時,咱們避免了讓代碼被同步邏輯搞得混亂不堪。code

爲了簡化共享數據的完整性和避免同時存取數據,內核提供了一種專門存取共享內存資源的機制。這稱爲互斥體或者mutex對象對象

例如,在數據被寫入以前不容許進程從共享內存中讀取信息、不容許兩個進程同時向同一個共享內存地址寫入數據等。

當一個進程想和另一個進程通訊的時候,它將按如下順序運行:

  • 獲取mutex對象,鎖定共享區域。

  • 將要通訊的數據寫入共享區域。

  • 釋放mutex對象。

當一個進程從從這個區域讀數據時候,它將重複一樣的步驟,只是將第二步變成讀取。

內存模型


要使用一塊共享內存

  • 進程必須首先分配

  • 隨後須要訪問這個共享內存塊的每個進程都必須將這個共享內存綁定到本身的地址空間中

  • 當完成通訊以後,全部進程都將脫離共享內存,而且由一個進程釋放該共享內存塊

/proc/sys/kernel/目錄下,記錄着共享內存的一些限制,如一個共享內存區的最大字節數shmmax,系統範圍內最大共享內存區標識符數shmmni等,能夠手工對其調整,但不推薦這樣作。

這裏寫圖片描述

理解 Linux 系統內存模型能夠有助於解釋這個綁定的過程。

linux系統內存模型


在 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);

 

參數 描述
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; } 

 

這裏寫圖片描述

ipcrm命令刪除共享內存

在使用共享內存,結束程序退出後。若是你沒在程序中用shmctl()刪除共享內存的話,必定要在命令行下用ipcrm命令刪除這塊共享內存。你要是無論的話,它就一直在那兒放着了。
簡單解釋一下ipcs命令和ipcrm命令。

取得ipc信息:

usage : ipcs -asmq -tclup ipcs [-s -m -q] -i id ipcs -h for help. m 輸出有關共享內存(shared memory)的信息 -q 輸出有關信息隊列(message queue)的信息 -s 輸出有關「遮斷器」(semaphore)的信息

 

刪除ipc

usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
          [-Q msgkey] [-M shmkey] [-S semkey] ... ] 

 

兩端通訊的程序


讀者程序


#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define N 64 typedef struct { pid_t pid; char buf[N]; } SHM; void handler(int signo) { //printf("get signal\n"); return; } int main() { key_t key; int shmid; SHM *p; pid_t pid; if ((key = ftok(".", 'm')) < 0) { perror("fail to ftok"); exit(-1); } signal(SIGUSR1, handler);//註冊一個信號處理函數 if ((shmid = shmget(key, sizeof(SHM), 0666|IPC_CREAT|IPC_EXCL)) < 0) { if (EEXIST == errno)//存在則直接打開 { shmid = shmget(key, sizeof(SHM), 0666); p = (SHM *)shmat(shmid, NULL, 0); pid = p->pid; p->pid = getpid();//把本身的pid寫到共享內存 kill(pid, SIGUSR1); } else//出錯 { perror("fail to shmget"); exit(-1); } } else//成功 { p = (SHM *)shmat(shmid, NULL, 0); p->pid = getpid(); pause(); pid = p->pid;//獲得寫端進程的pid } while ( 1 ) { pause();//阻塞,等待信號 if (strcmp(p->buf, "quit\n") == 0) exit(0);//輸入"quit結束" printf("read from shm : %s", p->buf); kill(pid, SIGUSR1);//向寫進程發SIGUSR1信號 } return 0; } 

 

寫者程序


#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define N 64 typedef struct { pid_t pid; char buf[N]; } SHM; void handler(int signo) { //printf("get signal\n"); return; } int main() { key_t key; int shmid; SHM *p; pid_t pid; if ((key = ftok(".", 'm')) < 0) { perror("fail to ftok"); exit(-1); } signal(SIGUSR1, handler); // 註冊一個信號處理函數 if ((shmid = shmget(key, sizeof(SHM), 0666 | IPC_CREAT | IPC_EXCL)) < 0) { if (EEXIST == errno) // 存在則直接打開 { shmid = shmget(key, sizeof(SHM), 0666); p = (SHM *)shmat(shmid, NULL, 0); pid = p->pid; p->pid = getpid(); kill(pid, SIGUSR1); } else//出錯 { perror("fail to shmget"); exit(-1); } } else//成功 { p = (SHM *)shmat(shmid, NULL, 0); p->pid = getpid(); // 把本身的pid寫到共享內存 pause(); pid = p->pid; // 獲得讀端進程的pid } while ( 1 ) { printf("write to shm : "); fgets(p->buf, N, stdin); // 接收輸入 kill(pid, SIGUSR1); // 向讀進程發SIGUSR1信號 if (strcmp(p->buf, "quit\n") == 0) break; pause(); // 阻塞,等待信號 } shmdt(p); shmctl(shmid, IPC_RMID, NULL); // 刪除共享內存 return 0; } 

 

這裏寫圖片描述

相關文章
相關標籤/搜索