筆者最近在閱讀 "Aerospike" 論文時,發現了Aerospike是利用了Linux 共用記憶體機制來實現的存儲索引快速重建的。這種方式比傳統利用索引文件進行快速重啟的方式大大提高了效率。( 減少了磁碟 i/o,但是缺點是耗費記憶體,並且伺服器一旦重啟之後就只能冷重啟了~~ )而目前筆者工作之中維 ...
筆者最近在閱讀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列
而更為詳盡的共用記憶體的數據,可以通過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 共用記憶體的作用,並且對如何操作共用記憶體進行了介紹,同時希望大家能夠在實際開發工作之後能夠很好的掌握共用記憶體這個「利器」,讓開發工作事倍功半~~