信號量分 System V 信號量和 POSIX 信號量,這里僅介紹 POSIX 信號量。
1. 概述
2. 命令信號量
3. 信號量操作
3.1 等待一個信號量
sem_wait() 函數會遞減(減小 1)sem 引用的信號量的值。
#include <semaphore.h>
int sem_wait(sem_t *sem);
- 如果信號量的當前值大於 0,那么 sem_wait() 會立即返回。
- 如果信號量的當前值等於 0,那么 sem_wait() 會阻塞直到信號量的值大於 0 為止,當信號量值大於 0 時該信號量值就被遞減並且 sem_wait() 會返回。
如果一個阻塞的 sem_wait() 調用被一個信號處理器中斷了,那么它就會失敗並返回 EINTR 錯誤,不管在使用 sigaction() 建立這個信號處理器時是否采用了 SA_RESTART 標記。(在其他一些 UNIX 實現上,SA_RESTART 會導致 sem_wait() 自動重啟。)
3.1.1 sem_trywait()
sem_trywait() 函數是 sem_wait() 的一個非阻塞版本。
#include <semaphore.h>
int sem_trywait(sem_t *sem);
如果遞減操作無法立即被執行,那么 sem_trywait() 就會失敗並返回 EAGAIN 錯誤。
3.1.2 sem_timedwait()
sem_timedwait() 函數是 sem_wait() 的另一個變體,它允許調用者為調用被阻塞的時間量指定一個限制。
#define _XOPEN_SOURCE 600
#include <semaphore.h>
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
如果 sem_timedwait() 調用因超時而無法遞減信號量,那么這個調用就會失敗並返回 ETIMEDOUT 錯誤。
abs_timeout 參數是一個結構,它將超時時間表示成了自新紀元到現在為止的秒數和納秒數的絕對值。如果需要指定一個相對超時時間,那么就必須要使用 clock_gettime() 獲取 CLOCK_REALTIME 時鍾的當前值並在該值上加上所需的時間量來生成一個適合在 sem_timedwait() 中使用的 timespec 結構。
3.2 發布一個信號量
sem_post() 函數遞增(增加 1)sem 引用的信號量的值。
#include <semaphore.h>
int sem_post(sem_t *sem);
如果在 sem_post() 調用之前信號量的值為 0,並且其他某個進程(或線程)正在因等待遞減這個信號量而阻塞,那么該進程會被喚醒,它的 sem_wait() 調用會繼續往前執行來遞減這個信號量。如果多個進程(或線程)在 sem_wait() 中阻塞了,並且這些進程的調度采用的是默認的循環時間分享策略,那么哪個進程會被喚醒並允許遞減這個信號量是不確定的。
3.3 獲取信號量的當前值
sem_getvalue() 函數將 sem 引用的信號量的當前值通過 sval 指向的 int 變量返回。
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
如果一個或多個進程(或線程)當前正在阻塞以等待遞減信號量值,那么 sval 中的返回值將取決於實現。
注意在 sem_getvalue() 返回時,sval 中的返回值可能已經過時了。依賴於 sem_getvalue() 返回的信息再執行后續操作時未發生變化的程序將會碰到檢查時、使用時(time-of-check、time-of-use)的競爭條件。
4. 未命令信號量
未命名信號量(也被稱為基於內存的信號量)是類型為 sem_t 並存儲在應用程序分配的內存中的變量。通過將這個信號量放在由幾個進程或線程共性的內存區域中就能夠使這個信號量對這些進程或線程可用。
操作未命名信號量所使用的函數與操作命令信號量使用的函數是一樣的(sem_wait()、sem_post() 以及 sem_getvalue()等)。此外,還需要用到另外兩個函數。
- sem_init() 函數對一個信號量進行初始化並通知系統該信號量會在進程間共享還是在單個進程中的線程間共享。
- sem_destroy(sem) 函數銷毀一個信號量。
這兩個函數不可以被應用到命名信號量上。
未命名與命名信號量對比
未命名信號量無需為信號量創建一個名字,這種做法在如下情況比較常見:
- 在線程間共享的信號量不需要名字。將一個未命名信號量作為一個共享(全局或堆上的)變量自動會使之對所有線程可訪問。
- 在相關進程間共享的信號量不需要名字。如果一個父進程在一塊共享內存區域中(如一個共享匿名映射)分配了一個未命名信號量,那么作為 fork() 操作的一部分,子進程會自動繼承這個映射,從而繼承這個信號量。
- 如果正在構建的是一個動態數據結構(如二叉樹),並且其中的每一項都需要一個關聯的信號量,那么最簡單的做法是在每一項中都分配一個未命名信號量。為每一項打開一個命名信號量需要為如何生成每一項中的信號量名字(唯一的)和管理這些名字設計一個規則(如當不需要它們時就對它們進行斷開鏈接操作)。
4.1 初始化一個未命名信號量
sem_init() 函數使用 value 中指定的值來對 sem 指向的未命名信號量進行初始化。
#include <semaphort.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Returns 0 on success, or -1 on error.
pshared 參數表明這個信號量是在線程間共享還是在進程間共享。
- 如果 pshared 等於 0,那么信號量將會在調用進程中的線程間進行共享。在這種情況下,sem 通常被指定成一個全局變量的地址或分配在一個堆上的一個變量的地址。線程共享的信號量具備進程持久性,它在進程終止時會被銷毀。
- 如果 pshared 不等於 0,那么信號量將會在進程間共享。在這種情況下,sem 必須是共享內存區域(一個 POSIX 共享內存對象、一個使用 mmap() 創建的共享映射、或一個System V 共享內存段)中的某個位置的地址。信號量的持久性與它所處的共享內存的持久性是一樣的。(通過其中大部分技術創建的共享內存區域具備內核持久性。但共享匿名映射是一個例外,只要存在一個進程維持着這種映射,那么它就一直存在下去。)由於通過 fork() 創建的子進程會繼承其父進程的內存映射,因此進程共享的信號量會被通過 fork() 創建的子進程繼承,這樣父進程和子進程也就能夠使用這些信號量來同步它們的動作了。
之所以需要 pshared 參數是因為如下原因:
- 一些實現不支持進程間共享的信號量。在這些系統上為 pshared 指定一個非零值會導致 sem_init() 返回一個錯誤。Linux 直到內核 2.6 以及 NPTL 線程化技術的出現之后才開始支持未命名的進程間共享的信號量。
- 在同時支持進程間共享信號量和線程間共享信號量的實現上,指定采用何種共享方式是有必要的,因為系統必須要執行特殊的動作來支持所需的共享方式。提供此類信息還使得系統能夠根據共享的種類來執行優化工作。
NPTL sem_init() 實現會忽略 pshared,因為不管采用何種共享方式都無需執行特殊的工作。
未命名信號量不存在相關的權限設置(即 sem_init() 中並不存在 sem_open() 中所需的 mode 參數)。對一個未命名信號量的訪問將由進程在底層共享內存區域上的權限來控制。
4.2 銷毀一個未命名信號量
sem_destroy() 函數將銷毀信號量 sem,其中 sem 必須是一個之前使用 sem_init() 進行初始化的未命名信號量。只有在不存在進程或線程在等待一個信號量時才能安全銷毀這個信號量。
#include <semaphore.h>
int sem_destroy(sem_t *sem);
Returns 0 on success, or -1 on error.
當使用 sem_destroy() 銷毀了一個未命名信號量之后就能夠使用 sem_init() 來重新初始化這個信號量了。
一個未命名信號量應該在其底層的內存被釋放之前被銷毀。如,如果信號量在一個自動分配的變量,那么在其宿主函數返回之前就應該銷毀這個信號量。如果信號量位於一個 POSIX 共享內存區域中,那么在所有進程都是用完這個信號量以及在使用 shm_unlink() 對這個共享內存對象執行斷開鏈接操作之前應該銷毀這個信號量。
在一些實現上,省略 sem_destroy() 調用不會導致問題的發生,但在其他實現上,不調用 sem_destroy() 會導致資源泄露。