System V IPC 之共享內存
IPC 是進程間通信(Interprocess Communication)的縮寫,通常指允許用戶態進程執行系列操作的一組機制:
- 通過信號量與其他進程進行同步
- 向其他進程發送消息或者從其他進程接收消息
- 和其他進程共享一段內存區
System V IPC 最初是在一個名為 "Columbus Unix" 的開發版 Unix 變種中引入的,之後在 AT&T 的 System III 中采用。現在在大部分 Unix 系統 (包括 Linux) 中都可以找到。
IPC 資源包含信號量、消息隊列和共享內存三種。IPC 的數據結構是在進程請求 IPC 資源時動態創建的。每個 IPC 資源都是持久的:除非被進程顯式地釋放,否則永遠駐留在內存中(直到系統關閉)。IPC 資源可以由任一進程使用,包括那些不共享祖先進程所創建的資源的進程。
由於一個進程可能需要同類型的多個 IPC 資源,因此每個新資源都是使用一個 32 位的 IPC 關鍵字來標識的,這和系統的目錄樹中的文件路徑名類似。每個 IPC 資源都有一個 32 位的 IPC 標識符,這與和打開文件相關的文件描述符有些類似。IPC 標識符由內核分配給 IPC 資源,在系統內部是唯一的,而 IPC 關鍵字可以由程序員自由地選擇。
當兩個或者更多的進程要通過一個 IPC 資源進行通信時,這些進程都要引用該資源的 IPC 標識符。
共享內存是進程間通信的一種最基本、最快速的機制。共享內存是兩個或多個進程共享同一塊內存區域,並通過該內存區域實現數據交換的進程間通信機制。通常是由一個進程開辟一塊共享內存區域,然後允許多個進程對此區域進行訪問。由於不需要使用中間介質,而是數據由內存直接映射到進程空間,因此共享內存是最快速的進程間通信機制。
使用共享內存有兩種方法:映射 /dev/mem 設備和內存映像文件。本文主要通過 demo 演示通過映射 /dev/mem 設備實現共享內存的方法。
共享內存的最大不足之處在於,由於多個進程對同一塊內存區具有訪問的權限,各個進程之間的同步問題顯得尤為突出。必須控制同一時刻只有一個進程對共享內存區域寫入數據,否則將造成數據的混亂。同步控制的問題,筆者將在隨後的文章中介紹如何通過信號量解決。
共享內存相關的數據結構
ipc_perm 結構
對於每一個進程間通信機制的對象,都有一個 ipc_perm 結構與之相對應,該結構的定義如下:
struct ipc_perm { uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode;ulong seq; key_t key; }
該結構用於記錄對象的各種相關信息,各個字段的具體含義如下:
uid:所有者的有效用戶 ID。
gid:所有者的有效組 ID。
cuid:創建者的有效用戶 ID。
cgid:創建者的有效組 ID。
mode:表示此對象的訪問權限。
seq:對象的應用序號。
key:對象的鍵。
shmid_ds 結構
每個共享內存都有與之相對應的 shmid_ds 結構,其定義如下:
struct shmid_ds { struct ipc_perm shm_perm; int shm_segsz; pid_t shm_cpid; pid_t shm_lpid; ulong shm_nattch; time_t shm_atime; time_t shm_dtiem; time_t shm_ctime; }
此機構記錄了一個共享內存的各種屬性,該結構的各個字段的含義如下:
shm_perm:對應於該共享內存的 ipc_perm 結構。
shm_segsz:以字節表示的共享內存區域的大小。
shm_lpid:最近一次調用 shmop 函數的進程 ID。
shm_cpid:創建該共享內存的進程 ID。
shm_nattch:當前使用該共享內存區域的進程數。
shm_atime:最近一次附加操作的時間。
shm_dtime:最近一次分離操作的時間。
shm_ctime:最近一次改變的時間。
操作共享內存的函數
創建或打開共享內存
要使用共享內存,首先要創建一個共享內存區域,創建共享內存區域的函數聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int flg);
函數 shmget 除了可用於創建一個新的共享內存外,也可用於打開一個已存在的共享內存。其中,參數 key 表示所創建或打開的共享內存的鍵。參數 size 表示共享內存區域的大小,只在創建一個新的共享內存時生效。參數 flag 表示調用函數的操作類型,也可用於設置共享內存的訪問權限。
當函數調用成功時,返回值為共享內存的引用標識符;調用失敗時,返回值為 -1。
附加共享內存
當一個共享內存創建或打開後,某個進程如果要使用該共享內存,則必須將這個共享內存區域附加到它的地址空間中。附加操作的函數聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int flag);
參數 shmid 表示要附加的共享內存區域的引用標識符。參數 shmaddr 和 flag 共通決定共享內存區域要附加到的地址值。比如設置 shmaddr 為 0 時,系統將自動查找進程地址空間,將共享內存區域附加到第一塊有效內存區域上,此時 flag 參數無效。
當函數調用成功時,返回值為指向共享內存區域的指針;調用失敗時,返回值為 -1。
分離共享內存
當一個進程對共享內存區域的訪問完成後,可以調用 shmdt 函數使共享內存區域與該進程的地址空間分離,shmdt 函數的聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
此函數僅用於將共享內存區域與進程的地址空間分離,並不刪除共享內存本身。參數 shmaddr 為指向要分離的共享內存區域的指針(就是調用 shmat 函數的返回值)。該函數調用成功時返回 0;調用失敗時返回 -1。
共享內存的控制
對共享內存區域的具體控制操作是通過函數 shmctl 來實現的,shmctl 函數的聲明如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
參數 shmid 為共享內存的引用標識符。參數 cmd 表示調用該函數希望執行的操作。參數 buf 是指向 shmid_ds 結構體的指針。參數 cmd 的取值和對應的操作如下:
SHM_LOCK:將共享內存區域上鎖。
IPC_RMID:用於刪除共享內存。
IPC_SET:按參數 buf 指向的結構中的值設置該共享內存對應的 shmid_ds 結構。
IPC_STAT:用於取得該共享內存區域的 shmid_ds 結構,保存到 buf 指向的緩沖區。
SHM_UNLOCK:將上鎖的共享內存區域釋放。
進程間通過共享內存通信的 demo
下面我們創建兩個程序 demoa 和 demob 來簡單的演示進程間如何通過共享內存通信。其中 demoa 的代碼如下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #define BUF_SIZE 1024 #define MYKEY 24 int main(void) { int shmid; char *shmptr; // 創建或打開內存共享區域 if((shmid=shmget(MYKEY,BUF_SIZE,IPC_CREAT))==-1){ printf("shmget error!\n"); exit(1); } if((shmptr=shmat(shmid,0,0))==(void*)-1){ printf("shmat error!\n"); exit(1); } while(1){ // 把用戶的輸入存到共享內存區域中 printf("input:"); scanf("%s",shmptr); } exit(0); }
demoa 程序創建或打開 key 為 24 的共享內存區域,並把用戶輸入的字符串存入這個共享內存區域。把上面的代碼保存到文件 shm_a.c 文件中,並用下面的命令編譯:
$ gcc -Wall shm_a.c -o demoa
下面是 demob 的代碼:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 1024 #define MYKEY 24 int main(void) { int shmid; char *shmptr; // 創建或打開內存共享區域 if((shmid=shmget(MYKEY,BUF_SIZE,IPC_CREAT))==-1){ printf("shmget error!\n"); exit(1); } if((shmptr=shmat(shmid,0,0))==(void*)-1){ fprintf(stderr,"shmat error!\n"); exit(1); } while(1){ // 每隔 3 秒從共享內存中取一次數據並打印到控制臺 printf("string:%s\n",shmptr); sleep(3); } exit(0); }
demob 程序創建或打開 key 為 24 的共享內存區域,然後每隔 3 秒從共享內存中取一次數據並打印到控制臺。這樣通過共享內存程序 demob 就可以獲取到 demoa 程序中的數據。 把上面的代碼保存到文件 shm_b.c 文件中,並用下面的命令編譯:
$ gcc -Wall shm_b.c -o demob
接下來分別運行 demoa 和 demob,然後嘗試在 demoa 中輸入一些字符串:
demob 完全不關心 demoa 在幹什麽,只是機械的每隔 3 秒鐘去共享內存中取一次數據,取到什麽就輸出什麽。
管理 ipc 資源的基本命令
我們在 demoa 和 demob 中並沒有通過 shmctl 函數在適當的時機刪除創建的共享內存區域,所以當程序 demoa 和 demob 退出後,我們創建的 key 為 24 的共享內存區域仍然駐留在系統的內存中。
Linux 系統默認自帶了一些管理 ipc 資源的基本命令,比如 ipcs、ipcmk 和 ipcrm。我們可以使用 ipcs 命令查看系統中的 ipc 資源:
$ ipcs -m
紅框中的共享內存就是我們的 demo 程序創建的,第一列的 key 0x18 換算成十進制就是 24。
現在我們已經不需要這個共享內存區域了,所以可以使用下面的命令把它刪除掉:
$ sudo ipcrm -M 24
當然,除了刪除 ipc 資源,我們還可以通過 ipcmk 命令創建 ipc 資源。關於 ipcs、ipcmk 和 kpcrm 這三個命令的具體用法請參考相關的 man page,此文不再贅述。
總結
本文簡單的介紹了 IPC 相關的基本概念和共享內存編程中的一些結構與函數。並通過一個簡單的 demo 演示了共享內存工作的基本原理。由於 demo 中沒有采取任何同步技術,demob 的輸出就顯得有些雜亂無章。在接下來介紹信號量的文章中,我們會在 demo 中通過信號量來同步共享內存的訪問。
參考:
《深入理解 Linux 內核》
《Linux 環境下 C 編程指南》
System V IPC 之共享內存