關於在 Linux 下多個不相干的程式互斥訪問同一片共享記憶體的問題

K_B_Z發表於2019-05-13

這裡的“不相干”,定義為:

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

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

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

如果用傳統 IPC 的 semget 那套介面,是沒法解決的。實測發現,down 了以後程式退出,訊號量的數值依然保持不變。

用 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 即可。

關於 ROBUST,官方解釋在:

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

需要注意的地方是:

如果持有 mutex 的執行緒退出,另外一個執行緒在 pthread_mutex_lock 的時候會返回 EOWNERDEAD。這時候你需要呼叫 pthread_mutex_consistent 函式來清除這種狀態,否則後果自負。

寫成程式碼就是這樣子:

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

歡迎各種圍觀!

相關文章