1.什么是共享內存?
共享內存就是允許兩個或多個進程共享一定的存儲區。就如同 malloc() 函數向不同進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。因為數據不需要在客戶機和服務器端之間復制,數據直接寫到內存,不用若干次數據拷貝,所以這是最快的一種IPC。
注:共享內存沒有任何的同步與互斥機制,所以要使用信號量來實現對共享內存的存取的同步。

共享內存特點和優勢
當中共享內存的大致原理相信我們可以看明白了,就是讓兩個進程地址通過頁表映射到同一片物理地址以便於通信,你可以給一個區域里面寫入數據,理所當然你就可以從中拿取數據,這也就構成了進程間的雙向通信,而且共享內存是IPC通信當中
傳輸速度最快的通信方式沒有之一,理由很簡單,客戶進程和服務進程傳遞的數據直接從內存里存取、放入,數據不需要在兩進程間復制,沒有什么操作比這簡單了。再者用共享內存進行數據通信,它對數據也沒啥限制。
最后就是共享內存的生命周期隨內核。即所有訪問共享內存區域對象的進程都已經正常結束,共享內存區域對象仍然在內核中存在(除非顯式刪除共享內存區域對象),在內核重新引導之前,對該共享內存區域對象的任何改寫操作都將一直保留;簡單地說,共享內存區域對象的生命周期跟系統內核的生命周期是一致的,而且共享內存區域對象的作用域范圍就是在整個系統內核的生命周期之內。
缺陷
但是,共享內存也並不完美,共享內存並未提供同步機制,也就是說,在一個服務進程結束對共享內存的寫操作之前,並沒有自動機制可以阻止另一個進程(客戶進程)開始對它進行讀取。這明顯還達不到我們想要的,我們不單是在兩進程間交互數據,還想實現多個進程對共享內存的同步訪問,這也正是使用共享內存的竅門所在。基於此,我們通常會用平時常談到和用到
信號量來實現對共享內存同步訪問控制。
與共享內存有關的函數
所有的函數共用頭文件
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h>
創建共享內存——>shmget() 函數
int shmget(key_t key, size_t size, int shmflg); //成功返回共享內存的ID,出錯返回-1
(1)第一個參數key是長整型(唯一非零),系統建立IPC通訊 ( 消息隊列、 信號量和 共享內存) 時必須指定一個ID值。通常情況下,該id值通過ftok函數得到,由內核變成標識符,要想讓兩個進程看到同一個信號集,只需設置key值不變就可以。
(2)第二個參數size指定共享內存的大小,它的值一般為一頁大小的整數倍(未到一頁,操作系統向上對齊到一頁,但是用戶實際能使用只有自己所申請的大小)。
(3)第三個參數shmflg是一組標志,創建一個新的共享內存,將shmflg 設置了IPC_CREAT標志后,共享內存存在就打開。而IPC_CREAT | IPC_EXCL則可以創建一個新的,唯一的共享內存,如果共享內存已存在,返回一個錯誤。一般我們會還或上一個文件權限
3.2操作共享內存———>shmctl()函數
int shmctl(int shm_id, int cmd, struct shmid_ds *buf); //成功返回0,出錯返回-1
(1)第一個參數,shm_id是shmget函數返回的共享內存標識符。
(2)第二個參數,cmd是要采取的操作,它可以取下面的三個值 :
IPC_STAT:把shmid_ds結構中的數據設置為共享內存的當前關聯值,即用共享內存的當前關聯值覆蓋shmid_ds的值。
IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置為shmid_ds結構中給出的值
IPC_RMID:刪除共享內存段
(3)第三個參數,buf是一個結構指針,它指向共享內存模式和訪問權限的結構。 shmid_ds結構至少包括以下成員
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
掛接操作———>shmat()函數
創建共享存儲段之后,將進程連接到它的地址空間
void *shmat(int shm_id, const void *shm_addr, int shmflg); //成功返回指向共享存儲段的指針,出錯返回-1
(1)第一個參數,shm_id是由shmget函數返回的共享內存標識。
(2)第二個參數,shm_addr指定共享內存連接到當前進程中的地址位置,通常為空,表示讓系統來選擇共享內存的地址。
(3)第三個參數,shm_flg是一組標志位,通常為0
3.4分離操作———>shmdt()函數
該操作不從系統中刪除標識符和其數據結構,要顯示調用shmctl(帶命令IPC_RMID)才能刪除它
int shmdt(const void *shmaddr); //成功返回0,出錯返回-1
(1)addr參數是以前調用shmat時的返回值
4.模擬實現進程間的通信方式———>共享內存
三、使用共享內存進行進程間通信
說了這么多,又到了實戰的時候了。下面就以兩個不相關的進程來說明進程間如何通過共享內存來進行通信。其中一個文件shmread.c創建共享內存,並讀取其中的信息,另一個文件shmwrite.c向共享內存中寫入數據。為了方便操作和數據結構的統一,為這兩個文件定義了相同的數據結構,定義在文件shmdata.c中。結構shared_use_st中的written作為一個可讀或可寫的標志,非0:表示可讀,0表示可寫,text則是內存中的文件。
shmdata.h的源代碼如下:
#ifndef _SHMDATA_H_HEADER #define _SHMDATA_H_HEADER #define TEXT_SZ 2048 struct shared_use_st { int written;//作為一個標志,非0:表示可讀,0表示可寫 char text[TEXT_SZ];//記錄寫入和讀取的文本 }; #endif
源文件shmread.c的源代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/shm.h> #include "shmdata.h" int main() { int running = 1;//程序是否繼續運行的標志 void *shm = NULL;//分配的共享內存的原始首地址 struct shared_use_st *shared;//指向shm int shmid;//共享內存標識符 //創建共享內存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT); if(shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } //將共享內存連接到當前進程的地址空間 shm = shmat(shmid, 0, 0); if(shm == (void*)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } printf("\nMemory attached at %X\n", (int)shm); //設置共享內存 shared = (struct shared_use_st*)shm; shared->written = 0; while(running)//讀取共享內存中的數據 { //沒有進程向共享內存定數據有數據可讀取 if(shared->written != 0) { printf("You wrote: %s", shared->text); sleep(rand() % 3); //讀取完數據,設置written使共享內存段可寫 shared->written = 0; //輸入了end,退出循環(程序) if(strncmp(shared->text, "end", 3) == 0) running = 0; } else//有其他進程在寫數據,不能讀取數據 sleep(1); } //把共享內存從當前進程中分離 if(shmdt(shm) == -1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); } //刪除共享內存 if(shmctl(shmid, IPC_RMID, 0) == -1) { fprintf(stderr, "shmctl(IPC_RMID) failed\n"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }
源文件shmwrite.c的源代碼如下:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/shm.h> #include "shmdata.h" int main() { int running = 1; void *shm = NULL; struct shared_use_st *shared = NULL; char buffer[BUFSIZ + 1];//用於保存輸入的文本 int shmid; //創建共享內存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT); if(shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } //將共享內存連接到當前進程的地址空間 shm = shmat(shmid, (void*)0, 0); if(shm == (void*)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } printf("Memory attached at %X\n", (int)shm); //設置共享內存 shared = (struct shared_use_st*)shm; while(running)//向共享內存中寫數據 { //數據還沒有被讀取,則等待數據被讀取,不能向共享內存中寫入文本 while(shared->written == 1) { sleep(1); printf("Waiting...\n"); } //向共享內存中寫入數據 printf("Enter some text: "); fgets(buffer, BUFSIZ, stdin); strncpy(shared->text, buffer, TEXT_SZ); //寫完數據,設置written使共享內存段可讀 shared->written = 1; //輸入了end,退出循環(程序) if(strncmp(buffer, "end", 3) == 0) running = 0; } //把共享內存從當前進程中分離 if(shmdt(shm) == -1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); } sleep(2); exit(EXIT_SUCCESS); }
結果截圖如下:
分析:
1、程序shmread創建共享內存,然后將它連接到自己的地址空間。在共享內存的開始處使用了一個結構struct_use_st。該結構中有個標志written,當共享內存中有其他進程向它寫入數據時,共享內存中的written被設置為0,程序等待。當它不為0時,表示沒有進程對共享內存寫入數據,程序就從共享內存中讀取數據並輸出,然后重置設置共享內存中的written為0,即讓其可被shmwrite進程寫入數據。
2、程序shmwrite取得共享內存並連接到自己的地址空間中。檢查共享內存中的written,是否為0,若不是,表示共享內存中的數據還沒有被完,則等待其他進程讀取完成,並提示用戶等待。若共享內存的written為0,表示沒有其他進程對共享內存進行讀取,則提示用戶輸入文本,並再次設置共享內存中的written為1,表示寫完成,其他進程可對共享內存進行讀操作。
四、關於前面的例子的安全性討論
這個程序是不安全的,當有多個程序同時向共享內存中讀寫數據時,問題就會出現。可能你會認為,可以改變一下written的使用方式,例如,只有當written為0時進程才可以向共享內存寫入數據,而當一個進程只有在written不為0時才能對其進行讀取,同時把written進行加1操作,讀取完后進行減1操作。這就有點像文件鎖中的讀寫鎖的功能。咋看之下,它似乎能行得通。但是這都不是原子操作,所以這種做法是行不能的。試想當written為0時,如果有兩個進程同時訪問共享內存,它們就會發現written為0,於是兩個進程都對其進行寫操作,顯然不行。當written為1時,有兩個進程同時對共享內存進行讀操作時也是如些,當這兩個進程都讀取完是,written就變成了-1.
要想讓程序安全地執行,就要有一種進程同步的進制,保證在進入臨界區的操作是原子操作。例如,可以使用前面所講的信號量來進行進程的同步。因為信號量的操作都是原子性的。
五、使用共享內存的優缺點
1、優點:我們可以看到使用共享內存進行進程間的通信真的是非常方便,而且函數的接口也簡單,數據的共享還使進程間的數據不用傳送,而是直接訪問內存,也加快了程序的效率。同時,它也不像匿名管道那樣要求通信的進程有一定的父子關系。
2、缺點:共享內存沒有提供同步的機制,這使得我們在使用共享內存進行進程間通信時,往往要借助其他的手段來進行進程間的同步工作。