關於在 Linux 下多個不相干的進程互斥訪問同一片共享內存的問題,記錄鎖,好文章


http://segmentfault.com/a/1190000000630435

http://blog.csdn.net/luansxx/article/details/7736618

 

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

  • 這幾個進程沒有父子關系,也沒有 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

歡迎各種圍觀!

===============================================================

進程間共享數據的保護,需要進程互斥鎖。與線程鎖不同,進程鎖並沒有直接的C庫支持,但是在Linux平台,要實現進程之間互斥鎖,方法有很多,大家不妨回憶一下你所了解的。下面就是標准C庫提供的一系列方案。

1、實現方案

不出意外的話,大家首先想到的應該是信號量(Semaphores)。對信號量的操作函數有兩套,一套是Posix標准,另一套是System V標准。

Posix信號量

[cpp]  view plain copy
 
  1. sem_t *sem_open(const char *name, int oflag);  
  2. sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);  
  3. int sem_init(sem_t *sem, int pshared, unsigned int value);  
  4. int sem_wait(sem_t *sem);  
  5. int sem_trywait(sem_t *sem);  
  6. int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);  
  7. int sem_close(sem_t *sem);  
  8. int sem_destroy(sem_t *sem);  
  9. int sem_unlink(const char *name);  

System V信號量

[cpp]  view plain copy
 
  1. int semget(key_t key, int nsems, int semflg);  
  2. int semctl(int semid, int semnum, int cmd, ...);  
  3. int semop(int semid, struct sembuf *sops, unsigned nsops);  
  4. int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);  

線程鎖共享

其實還有另外一個方案:線程鎖共享。這是什么呢,我估計了解它的人不多,如果你知道的話,那可以稱為Linux開發牛人了。

線程鎖就是pthread那一套C函數了:

[html]  view plain copy
 
  1. int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);  
  2. int pthread_mutex_destroy (pthread_mutex_t *mutex);  
  3. int pthread_mutex_trylock (pthread_mutex_t *mutex);  
  4. int pthread_mutex_lock (pthread_mutex_t *mutex);  
  5. int pthread_mutex_timedlock (pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);  
  6. int pthread_mutex_unlock (pthread_mutex_t *mutex);  

但是這只能用在一個進程內的多個線程實現互斥,怎么應用到多進程場合呢,被多個進程共享呢?

很簡單,首先需要設置互斥鎖的進程間共享屬性:

[html]  view plain copy
 
  1. int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);   
  2. pthread_mutexattr_t mattr;   
  3. pthread_mutexattr_init(&mattr);   
  4. pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);   

其次,為了達到多進程共享的需要,互斥鎖對象需要創建在共享內存中。

最后,需要注意的是,並不是所有Linux系統都支持這個特性,程序里需要檢查是否定義了_POSIX_SHARED_MEMORY_OBJECTS宏,只有定義了,才能用這種方式實現進程間互斥鎖。

2、平台兼容性

我們來看看這三套方案的平台移植性。

  • 絕大部分嵌入式Linux系統,glibc或者uclibc,不支持_POSIX_SHARED_MEMORY_OBJECTS;
  • 絕大部分嵌入式Linux系統,不支持Posix標准信號量;
  • 部分平台,不支持System V標准信號量,比如Android。

3、匿名鎖與命名鎖

當兩個(或者多個)進程沒有特殊關系(比如父子進程共享)時,我們只能通過約定好的名字來訪問同一個鎖,這就是命名鎖。然而,如果我們有其他途徑定位一個鎖,那么匿名鎖是更好的選擇。這三套方案是否都支持匿名鎖與命名鎖呢?

  • Posix信號量

通過sem_open創建命名鎖,通過sem_init創建匿名鎖,其實sem_init也支持進程內部鎖。

  • System V信號量

semget中的key參數可以看成是名字,所以支持命名鎖。該方案不支持匿名鎖。

  • 線程鎖共享

不支持命名鎖,支持匿名鎖。

4、缺陷

在匿名鎖與命名鎖的支持上,一些方案是有不足的,但這還是小問題,更嚴重的問題是異常狀況下的死鎖問題。

與多線程環境不一樣的是,在多進程環境中,一個進程的異常退出不會影響其他進程,但是如果使用了進程互斥鎖呢?假如一個進程獲取了互斥鎖,但是在訪問互斥資源的代碼中crash了,或者遇到信號退出了,那么其他等待同一個鎖的進程(內部某個線程)就掛死了。在多線程環境中,程序異常整個進程退出,不需要考慮異常時鎖的釋放,多進程環境則是一個實實在在的問題。

System V信號量通過UNDO方式可以解決該問題。但是如果考慮到平台兼容性等問題,這三個方案仍不能滿足需求,我會接着介紹一種更好的方案。

 

========================================================================

http://blog.csdn.net/luansxx/article/details/7737899

《進程互斥鎖》中,我們看到了實現進程互斥鎖的幾個常見方案:Posix信號量、System V信號量以及線程鎖共享,並且分析了他們的平台兼容性以及嚴重缺陷。這里要介紹

一種安全且平台兼容的進程互斥鎖,它是基於文件記錄鎖實現的。

1、文件記錄鎖

UNIX編程的“聖經”《Unix環境高級編程》中有對文件記錄鎖的詳細描述。

下載鏈接:http://dl02.topsage.com/club/computer/Unix環境高級編程.rar

記錄鎖(record locking)的功能是:一個進程正在讀或修改文件的某個部分時,可以阻止其他進程修改同一文件區。對於UNIX,“記錄”這個定語也是誤用,因為UNIX內核根本沒有使用文件記錄這種概念。一個更適合的術語可能是“區域鎖”,因為它鎖定的只是文件的一個區域(也可能是整個文件)。

2、平台兼容性

各種UNIX系統支持的記錄鎖形式: 

 

系統 建議性 強制性 fcntl lockf flock
POSIX.1 *   *    
XPG3 *   *    
SVR2 *   * *  
SVR3 SVR4 * * * *  
4.3BSD *   *   *
4.3BSDReno *   *   *

 

可以看成,記錄鎖在各個平台得到廣泛支持。特別的,在接口上,可以統一於fcntl。

建議性鎖和強制性鎖之間的區別,是指其他文件操作函數(如open,read、write)是否受記錄鎖影響,如果是,那就是強制性的記錄鎖,大部分平台只是建議性的。不過,對實現進程互斥鎖而言,這個影響不大。

3、接口描述

[cpp]  view plain copy
 
  1. #include <sys/types.h>  
  2. #include <unistd.h>  
  3. #include <fcnt1.h>  
  4. int fcnt1(int filedes, int cmd, .../* struct flock *flockptr */);  

對於記錄鎖,cmd是F_GETLK、F_SETLK或F_SETLKW。第三個參數(稱其為flockptr)是一個指向flock結構的指針。

[cpp]  view plain copy
 
  1. struct flock {  
  2.     short l_type;    /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */  
  3.     short l_whence;  /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */  
  4.     off_t l_start;   /* Starting offset for lock */  
  5.     off_t l_len;     /* Number of bytes to lock */  
  6.     pid_t l_pid;     /* PID of process blocking our lock  
  7. };  

以下說明fcntl函數的三種命令:

  • F_GETLK決定由flockptr所描述的鎖是否被另外一把鎖所排斥(阻塞)。如果存在一把鎖,它阻止創建由flockptr所描述的鎖,則這把現存的鎖的信息寫到flockptr指向的結構中。如果不存在這種情況,則除了將ltype設置為F_UNLCK之外,flockptr所指向結構中的其他信息保持不變。
  • F_SETLK設置由flockptr所描述的鎖。如果試圖建立一把按上述兼容性規則並不允許的鎖,則fcntl立即出錯返回,此時errno設置為EACCES或EAGAIN。
  • F_SETLKW這是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果由於存在其他鎖,那么按兼容性規則由flockptr所要求的鎖不能被創建,則調用進程睡眠。如果捕捉到信號則睡眠中斷。

4、實現方案

如何 基於記錄鎖實現進程互斥鎖?

1、需要一個定義全局文件名,這個文件名只能有相關進程使用。這需要在整個系統做一個規划。

2、規定同一個進程互斥鎖對應着該文件的一個字節,字節位置稱為鎖的編號,這樣可以用一個文件實現很多互斥鎖。

3、編號要有分配邏輯,文件中要記錄已經分配的編號,這個邏輯也要保護,所以分配0號鎖為系統鎖。

4、為了實現命名鎖,文件中要記錄名稱與編號對應關系,這個對應關系的維護也需要系統鎖保護。

這些邏輯都實現在一個FileLocks類中:

[cpp]  view plain copy
 
  1. class FileLocks  
  2. {  
  3. public:  
  4.     FileLocks();  
  5.     ~FileLocks();  
  6.     size_t alloc_lock();  
  7.     size_t alloc_lock(std::string const & keyname);  
  8.     void lock(size_t pos);  
  9.     bool try_lock(size_t pos);  
  10.     void unlock(size_t pos);  
  11.     void free_lock(size_t pos);  
  12.     void free_lock(std::string const & keyname);  
  13. private:  
  14.     int             m_fd_;  
  15. };  
  16.   
  17. inline FileLocks & global_file_lock()  
  18. {  
  19.     static FileLocks g_fileblocks( "process.filelock" );  
  20.     return g_fileblocks;  
  21. }  

這里用了一個FileLocks全局單例對象,它對應的文件名是“/tmp/filelock”,在FileLocks中,分別用alloc()和alloc(keyname)分配匿名鎖和命名鎖,用free_lock刪除鎖。free_lock(pos)刪除匿名鎖,free_lock(keyname)刪除命名鎖。

對鎖的使用通過lock、try_lock、unlock實現,他們都帶有一個pos參數,代表鎖的編號。

有了FileLocks類作為基礎,要實現匿名鎖和命名鎖就很簡單了。

4.1、匿名鎖

[cpp]  view plain copy
 
  1. class FileMutex  
  2. {  
  3. public:  
  4.     FileMutex()  
  5.         : m_lockbyte_(global_file_lock().alloc_lock())  
  6.     {  
  7.     }  
  8.     ~FileMutex()  
  9.     {  
  10.         global_file_lock().free_lock(m_lockbyte_);  
  11.     }  
  12.     void lock()  
  13.     {  
  14.         global_file_lock().lock(m_lockbyte_);  
  15.     }  
  16.     bool try_lock()  
  17.     {  
  18.         return global_file_lock().try_lock(m_lockbyte_);  
  19.     }  
  20.     void unlock()  
  21.     {  
  22.         global_file_lock().unlock(m_lockbyte_);  
  23.     }  
  24. protected:  
  25.     size_t m_lockbyte_;  
  26. };  

需要注意的是,進程匿名互斥鎖需要創建在共享內存上。只需要也只能某一個進程(比如創建共享內存的進程)調用構造函數,其他進程直接使用,同樣析構函數也只能調用一次。

4.2、命名鎖

 命名鎖只需要構造函數不同,可以直接繼承匿名鎖實現

[cpp]  view plain copy
 
  1. class NamedFileMutex   
  2.     : public FileMutex  
  3. {  
  4. public:  
  5.     NamedFileMutex(std::string const & key)  
  6.         : m_lockbyte_(global_file_lock().alloc_lock(key))  
  7.     {  
  8.     }  
  9.     ~NamedFileMutex()  
  10.     {  
  11.         m_lockbyte_ = 0;  
  12.     }  
  13. };  

需要注意,命名鎖不住析構時刪除,因為可能多個對象共享該鎖。

5、線程安全性


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM