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信號量
- sem_t *sem_open(const char *name, int oflag);
- sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
- int sem_init(sem_t *sem, int pshared, unsigned int value);
- int sem_wait(sem_t *sem);
- int sem_trywait(sem_t *sem);
- int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- int sem_close(sem_t *sem);
- int sem_destroy(sem_t *sem);
- int sem_unlink(const char *name);
System V信號量
- int semget(key_t key, int nsems, int semflg);
- int semctl(int semid, int semnum, int cmd, ...);
- int semop(int semid, struct sembuf *sops, unsigned nsops);
- int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);
線程鎖共享
其實還有另外一個方案:線程鎖共享。這是什么呢,我估計了解它的人不多,如果你知道的話,那可以稱為Linux開發牛人了。
線程鎖就是pthread那一套C函數了:
- int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
- int pthread_mutex_destroy (pthread_mutex_t *mutex);
- int pthread_mutex_trylock (pthread_mutex_t *mutex);
- int pthread_mutex_lock (pthread_mutex_t *mutex);
- int pthread_mutex_timedlock (pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- int pthread_mutex_unlock (pthread_mutex_t *mutex);
但是這只能用在一個進程內的多個線程實現互斥,怎么應用到多進程場合呢,被多個進程共享呢?
很簡單,首先需要設置互斥鎖的進程間共享屬性:
- int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);
- pthread_mutexattr_t mattr;
- pthread_mutexattr_init(&mattr);
- 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、接口描述
- #include <sys/types.h>
- #include <unistd.h>
- #include <fcnt1.h>
- int fcnt1(int filedes, int cmd, .../* struct flock *flockptr */);
對於記錄鎖,cmd是F_GETLK、F_SETLK或F_SETLKW。第三個參數(稱其為flockptr)是一個指向flock結構的指針。
- struct flock {
- short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
- short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
- off_t l_start; /* Starting offset for lock */
- off_t l_len; /* Number of bytes to lock */
- pid_t l_pid; /* PID of process blocking our lock
- };
以下說明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類中:
- class FileLocks
- {
- public:
- FileLocks();
- ~FileLocks();
- size_t alloc_lock();
- size_t alloc_lock(std::string const & keyname);
- void lock(size_t pos);
- bool try_lock(size_t pos);
- void unlock(size_t pos);
- void free_lock(size_t pos);
- void free_lock(std::string const & keyname);
- private:
- int m_fd_;
- };
- inline FileLocks & global_file_lock()
- {
- static FileLocks g_fileblocks( "process.filelock" );
- return g_fileblocks;
- }
這里用了一個FileLocks全局單例對象,它對應的文件名是“/tmp/filelock”,在FileLocks中,分別用alloc()和alloc(keyname)分配匿名鎖和命名鎖,用free_lock刪除鎖。free_lock(pos)刪除匿名鎖,free_lock(keyname)刪除命名鎖。
對鎖的使用通過lock、try_lock、unlock實現,他們都帶有一個pos參數,代表鎖的編號。
有了FileLocks類作為基礎,要實現匿名鎖和命名鎖就很簡單了。
4.1、匿名鎖
- class FileMutex
- {
- public:
- FileMutex()
- : m_lockbyte_(global_file_lock().alloc_lock())
- {
- }
- ~FileMutex()
- {
- global_file_lock().free_lock(m_lockbyte_);
- }
- void lock()
- {
- global_file_lock().lock(m_lockbyte_);
- }
- bool try_lock()
- {
- return global_file_lock().try_lock(m_lockbyte_);
- }
- void unlock()
- {
- global_file_lock().unlock(m_lockbyte_);
- }
- protected:
- size_t m_lockbyte_;
- };
需要注意的是,進程匿名互斥鎖需要創建在共享內存上。只需要也只能某一個進程(比如創建共享內存的進程)調用構造函數,其他進程直接使用,同樣析構函數也只能調用一次。
4.2、命名鎖
命名鎖只需要構造函數不同,可以直接繼承匿名鎖實現
- class NamedFileMutex
- : public FileMutex
- {
- public:
- NamedFileMutex(std::string const & key)
- : m_lockbyte_(global_file_lock().alloc_lock(key))
- {
- }
- ~NamedFileMutex()
- {
- m_lockbyte_ = 0;
- }
- };
需要注意,命名鎖不住析構時刪除,因為可能多個對象共享該鎖。