關於在 Linux 下多個不相干的進程互斥訪問同一片共享內存的問題

這裏的「不相干」,定義爲:html

  • 這幾個進程沒有父子關係,也沒有 Server/Client 關係
  • 這一片共享內存一開始不存在,第一個要訪問它的進程負責新建
  • 也沒有額外的 daemon 進程能管理這事情

看上去這是一個很簡單的問題,實際上不簡單。有兩大問題:git

進程在持有互斥鎖的時候異常退出

若是用傳統 IPC 的 semget 那套接口,是無法解決的。實測發現,down 了之後進程退出,信號量的數值依然保持不變。github

用 pthread (2013年的)新特性能夠解決。在建立 pthread mutex 的時候,指定爲 ROBUST 模式。框架

pthread_mutexattr_t ma;

pthread_mutexattr_init(&ma);
pthread_mutexattr_setpshared(&ma, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&ma, PTHREAD_MUTEX_ROBUST);

pthread_mutex_init(&c->lock, &ma);

注意,pthread 是能夠用於多進程的。指定 PTHREAD_PROCESS_SHARED 便可。socket

關於 ROBUST,官方解釋在:函數

http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutexattr_setrobust.html線程

須要注意的地方是:code

若是持有 mutex 的線程退出,另一個線程在 pthread_mutex_lock 的時候會返回 EOWNERDEAD。這時候你須要調用 pthread_mutex_consistent 函數來清除這種狀態,不然後果自負。

寫成代碼就是這樣子:htm

int r = pthread_mutex_lock(lock);
if (r == EOWNERDEAD)
  pthread_mutex_consistent(lock);

因此要使用這個新特新的話,須要比較新的 GCC ,要 2013 年之後的版本。接口

好了第一個問題解決了。咱們能夠在初始化共享內存的時候,新建一個這樣的 pthread mutex。可是問題又來了:

怎樣用原子操做新建並初始化這一片共享內存?

這個問題看上去簡單至極,不過若是用這樣子的代碼:

void *p = get_shared_mem();
if (p == NULL)
    p = create_shared_mem_and_init_mutex();
lock_shared_mem(p);
....

是不嚴謹的。若是共享內存初始化成全 0,那可能碰巧還能夠。但咱們的 mutex 也是放到共享內存裏面的,是須要 init 的。

想象一下四個進程同時執行這段代碼,極可能某兩個進程發現共享內存不存在,而後同時新建並初始化信號量。某一個 lock 了 mutex,而後另一個又 init mutex,就亂了。

可見,在 init mutex 以前,咱們就已經須要 mutex 了。問題是,哪來這樣的 mutex?前面已經說了傳統 IPC 無法解決第一個問題,因此也不能用它。

其實,Linux 的文件系統自己就有這樣的功能。

首先 shm_open 那一系列的函數是和文件系統關聯上的。

~ ll /dev/shm/

其實 /dev/shm 是一個 mount 了的文件系統。這裏面放的就是一堆經過 shm_open 新建的共享內存。都是以文件的形式展示出來。能夠 rm,rename,link 各類文件操做。

其實 link 函數,也就是硬連接。是完成「原子操做」的關鍵所在。

搞過彙編的可能知道 CMPXCHG 這類(兩個數比較,符合條件則交換)指令,是原子操做內存的最底層指令,最底層的信號量是經過它實現的。

而 link 系統調用,相似的,是系統調用級,原子操做文件的最底層指令。處於 link 操做中的進程即使被 kill 掉,在內核中也會完成最後一次此次系統調用,對文件不會有影響,不存在 「link 了一半」 這種狀態,它是「原子」的。

僞代碼以下:

shm_open("ourshm_tmp", ...);
// ... 初始化 ourshm_tmp 副本 ...

if (link("/dev/shm/ourshm_tmp", "/dev/shm/ourshm") == 0) {
   // 我成功建立了這片共享內存
} else {
   // 別人已經建立了
}
shm_unlink("ourshm_tmp");

首先新建初始化一份副本。而後用 link 函數。

最後不管如何都要 unlink 掉副本。

開源項目 kbz-event

這兩種方法,貌似在各種經典書籍中都沒說起,由於是 2013 年新出的,也是由於 Unix 鼓勵用管道進行這類通訊的緣由。

在同類開源項目中。D-Bus 用的是另外的 daemon 進程去管理 socket。Android 的 IPC 則用了另外的內核模塊(netlink 接口)來完成。

總之,都是用了額外的接口。

所以我開發了不須要額外 daemon 的輕量級 IPC 通訊框架 kbz-event

歡迎各類圍觀!

相關文章
相關標籤/搜索