Linux 程式設計1:深入淺出 Linux 共享記憶體

happenlee發表於2019-01-02

筆者最近在閱讀Aerospike 論文時,發現了Aerospike是利用了Linux 共享記憶體機制來實現的儲存索引快速重建的。這種方式比傳統利用索引檔案進行快速重啟的方式大大提高了效率。(減少了磁碟 i/o,但是缺點是耗費記憶體,並且伺服器一旦重啟之後就只能冷重啟了~~)而目前筆者工作之中維護的 NoSQL 資料庫也是通過同樣的機制實現儲存索引的快速重建的,工欲善其事,必先利其器。所以筆者花時間調研了一下Linux共享記憶體的機制,希望對各位有所幫助~~

1.共享記憶體簡介

說到共享記憶體,有過作業系統學習的童靴應該十分熟悉,往往聊到程式之間通訊的4種方式時就能脫口而出(面試最常見的問題之一啊,哈哈哈~~):

  • 共享記憶體
  • 訊息佇列
  • 訊號量
  • Socket

今天我們的主角是共享記憶體。如下圖所示,所謂的共享記憶體,就是由多個程式的虛擬記憶體空間共同地對映到同一段實體記憶體空間,來實現記憶體的共享。
共享記憶體

共享記憶體通常是 ipc 之中效率最高的方式。Linux 之中實現共享記憶體的方式通常有如下幾類:

  • mmap記憶體共享對映 (通常用於父子程式之間的記憶體共享,存在一定侷限性,後文不表
  • System V的共享記憶體
  • POSIX共享記憶體

我們平時討論主要的共享記憶體就是後面兩者,但是其實無論是 System V 還是 POSIX 形式的共享記憶體,底層都是基於記憶體檔案系統tmpfs實現的,二者的主要區別是在介面設計上,POSIX旨在提供所有系統都一致的介面,遵循了 Linux 系統之中一切皆為檔案的理念。而System V只實現自己的一套內生的IPC邏輯,所以兩者在使用上存在一些差異,由於 Aerospike 之中沿用了 System V 的機制,所以筆者後續的介紹也以 System V 的共享記憶體來展開。

共享記憶體雖然給了多程式通訊的效率帶來了質的飛躍,但是存在的問題也很明顯:每一個參與使用共享記憶體的程式,都可以讀取寫入資料,這自然而然帶來了記憶體空間等競爭的問題。雖然這裡可以通過類似於管道的機制來單向通訊來規避競爭的問題,但是額外引入的複雜度和記憶體佔用同樣也是問題)所以這裡我們也可以反思共享記憶體真的是用來程式間通訊的嗎?筆者這裡反而是這樣認為的:通過通訊來共享記憶體,而不是通過共享記憶體來通訊

2.共享記憶體的設定與檢視

使用共享記憶體,需要在系統層面進行一些設定。這章需要介紹一些共享記憶體相關的設定,在 Linux 系統之中和共享記憶體有關的檔案有:

  /proc/sys/kernel/shmmni:限制整個系統可建立共享記憶體段個數。

  /proc/sys/kernel/shmall: 限制系統用在共享記憶體上的記憶體的頁數。

  /proc/sys/kernel/shmmax:限制一個共享記憶體段的最大長度,位元組為單位。

在使用共享記憶體時,我們可以修改上述檔案來滿足我們的設定需求。這裡要注意的是,上述的配置檔案是臨時性的,重啟之後就失效了。如果需要永久性設定這些引數,可以修改/etc/sysctl.conf來完成共享記憶體的設定。

共享記憶體本質上是對記憶體空間的使用,同時也是 ipc 的方式之一,所以我們可以使用對應的 Linux 命令來檢視對應共享記憶體的使用:

free 可以顯示系統的記憶體佔用,共享記憶體的記憶體佔用會歸類在 shared,buffer/cache
free 命令檢視共享記憶體
而更為詳盡的共享記憶體的資料,可以通過ipcs -m的命令來進行展示。
共享記憶體的使用狀況
這裡簡單介紹一下,共享記憶體各個列所代表的含義:

  • key:共享記憶體的key,後文會通過程式來解釋 key 的含義。
  • shmil:共享記憶體的編號。
  • owner:建立的共享記憶體的使用者。
  • perms:共享記憶體的許可權,基於使用者的。
  • bytes:共享記憶體的大小。
  • nattch:連線到共享記憶體的程式數。
  • status:共享記憶體的狀態,顯示“dest”表示共享記憶體段已經被刪除,但是還有別的引用,共享記憶體是通過引用計數的方式來決定生命週期,一旦程式應用記憶體地址的計數為0,作業系統會回收對應的記憶體資源。

在這裡如果需要清理對應的共享記憶體,可以藉助命令ipcrm -m [shmid]來回收對應的記憶體空間。

3.共享記憶體的使用

int shmget(key_t key, size_t size, int shmflg);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

extern key_t ftok (const char *__pathname, int __proj_id)

萬事俱備,現在我們要來介紹一下如何在對應的程式碼之中使用共享記憶體,主要涉及上述五個函式,我們通過一個簡單的 demo 來介紹這些函式:
int shmget(key_t key, size_t size, int shmflg)是申請共享記憶體的函式,這裡需要理解的是key這個引數,它本身是一個 int 型別,這個 key_t引數是通過key_t ftok (const char *__pathname, int __proj_id)產生的,這裡的pathname指的是一個固定的路徑,proj_id則表示對應專案的 id。所以在一個作業系統內,如何讓兩個不相關(沒有父子關係)的程式可以共享一個記憶體段呢?Bingo!就是通過這個 key_t型別讓所有的程式都唯一對映到對應記憶體空間,這裡就是通過對應的檔案路徑專案 id來產生對應的key
所以說,在一個使用到共享記憶體的程式之中,需要程式設定一個檔案路徑和一個專案的proj_id,來獲取系統之中確定一段共享記憶體的key。這裡需要注意的是ftok需要指定一個存在並且程式可以訪問的pathname路徑。因為 ftok使用的是指定檔案的inode編號。所以,用了不同的檔名同樣可能得到相同的key,因為可以通過硬連結的方式讓不同的檔名指向相同 inode 編號檔案。

    key_t shm_key;

    proj_id = 111;

    if ((shm_key = ftok("/home/happen", proj_id)) == -1) {
        exit(1);
    }

    shm_id = shmget(shm_key, sizeof(int), IPC_CREAT|IPC_EXCL|0600);
    if (shm_id < 0) {
        exit(1);
    }

ok,獲取了共享記憶體之後,我們需要將這部分共享記憶體的地址對映到當前程式的記憶體空間之上,需要藉助這個函式void *shmat(int shmid, const void *shmaddr, int shmflg)返回對應程式記憶體空間的指標,來對這部分記憶體進行操作。

   shm_p = (int *)shmat(shm_id, NULL, 0);
    if ((void *)shm_p == (void *)-1) {
        exit(1);
    }

這裡可以用過shmflg來設定對應記憶體空間的讀寫許可權,這裡我們取的是0,代表對應的空間有讀寫許可權。SHM_RDONLY可以設定為只讀許可權。之後我們就可以對對應的記憶體空間進行操作了:

    *shm_p = 100;

   
    if (shmdt(shm_p) < 0) {
        perror("shmdt()");
        exit(1);
    }

    if (shmctl(shm_id, IPC_RMID, NULL) < 0) {
        perror("shmctl()");
        exit(1);
    }

   return 0;

在使用完共享記憶體之後,需要使用int shmdt(const void *shmaddr)解除記憶體空間的對映,否則虛擬記憶體地址的洩漏,導致沒有可用地址可用。shmdt僅僅只是解除共享記憶體空間和程式地址的對映,而想要刪除一個共享記憶體需要使用int shmctl(int shmid, int cmd, struct shmid_ds *buf)函式進行處理同時也可以在命令列中使用第二小節的ipcrm命令來刪除指定的共享記憶體。在這裡必須強調的是,如果沒有顯式用shmctl或ipcrm命令刪除的話,那麼對應的共享記憶體將一直保留直到系統被關閉。

4.小結

到此為止,筆者展開聊了聊 Linux 共享記憶體的作用,並且對如何操作共享記憶體進行了介紹,同時希望大家能夠在實際開發工作之後能夠很好的掌握共享記憶體這個「利器」,讓開發工作事倍功半~~

相關文章