1. 程式人生 > >System V IPC 之共享內存

System V IPC 之共享內存

應用 創建者 CP 機構 ons 最大 基本概念 返回 time_t

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 資源的基本命令,比如 ipcsipcmk 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 之共享內存