­

Posix訊號量

  • 2019 年 10 月 3 日
  • 筆記

1. Posix IPC

概述

以下三種類型的IPC合稱為Posix IPC:

  • Posix訊號量
  • Posix消息隊列
  • Posix共享記憶體

Posix IPC在訪問它們的函數和描述它們的資訊上有一些類似點,主要包括:

  • IPC名字
  • 創建或打開時指定的讀寫許可權、創建標誌以及用戶訪問許可權

下表匯總了所有Posix IPC函數。

  訊號量 消息隊列 共享記憶體
頭文件 semaphore.h mqueue.h sys/mman.h
創建、打開或刪除IPC的函數
 
 
 
 
 
sem_open
sem_close
sem_unlink
 
sem_init
sem_destroy
mq_open
mq_close
mq_unlink
 
 
 
shm_open
shm_unlink
 
 
 
 
控制IPC操作的函數
 
 
 
mq_getattr
mq_setattr
ftruncate
fstat
IPC操作函數
 
 
 
sem_wait
sem_trywait
sem_post
sem_getvalue
mq_send
mq_receive
mq_notify
 
mmap
munmap
 
 

IPC名字

除了Posix無名訊號量,其餘三種類型的Posix IPC都使用"Posix IPC"名字進行標識,它可能是文件系統中真實存在的一個路徑名,也可能不是。Posix.1是這麼描述的:

  • 它必須符合系統規定的路徑名規則
  • 如果它以斜杠符開頭,那麼Posix IPC函數的不同調用將訪問同一個IPC對象;否則,具體效果取決於系統實現
  • 對IPC名字中額外斜杠符的解釋取決於系統實現

因此,為了便於程式碼移植,通常在實際項目中遵循下面兩條規則:

  • Posix IPC名字必須以一個斜杠符開頭,且不能再含有任何其他斜杠符
  • 把所有Posix IPC名字的宏定義統一放在一個便於修改的頭文件中

創建與打開IPC

以下是三種Posix IPC的創建與打開函數:

  • sem_open用於創建或打開一個Posix有名訊號量
  • mq_open用於創建或打開一個Posix消息隊列
  • shm_open用於創建或打開一個Posix共享記憶體

讀寫許可權與創建標誌

這三個函數的第二個參數都是oflag,作用是指定IPC的讀寫許可權與創建標誌,下表給出了可組合構成該參數的所有常值。

說 明 sem_open mq_open shm_open
只讀
只寫
讀寫
O_RDONLY
O_WRONLY
O_RDWR
O_RDONLY

O_RDWR

若不存在則創建
排他性創建
O_CREAT
O_EXCL
O_CREAT
O_EXCL
O_CREAT
O_EXCL
非阻塞模式
若已存在則截短
O_NONBLOCK
O_EXCL
O_TRUNC

前三行指定讀寫許可權:只讀、只寫、讀寫,從表中可以看出:

  • 有名訊號量不指定該標誌
  • 消息隊列可指定任意模式
  • 共享記憶體不能以只寫方式打開

後面四行指定創建標誌:

  • O_CREAT若函數第一個參數指定的IPC不存在,則進行創建,此時至少需要第三個參數mode指定用戶訪問許可權(詳見後續)
  • O_EXCL:如果和O_CREAT一起指定,那麼當IPC已存在且指定了O_CREAT | O_EXCL標誌時,會出錯返回EEXIST
  • O_NONBLOCK:僅適用於Posix消息隊列,作用是隊列為空時的讀操作隊列為滿時的寫操作不會阻塞
  • O_TRUNC:僅適用於Posix共享記憶體,作用是當共享記憶體對象已存在時,將其長度截為0

用戶訪問許可權

創建一個新的Posix IPC時,需要使用第三個參數指定用戶訪問許可權,它是由下表所示常值按位或構成的,常值的格式為S_IRXXX和S_IWXXX,其中XXX代表訪問用戶。

常 值 說 明
S_IRUSR
S_IWUSR
用戶讀
用戶寫
S_IRGRP
S_IWGRP
組成員讀
組成員寫
S_IROTH
S_IWOTH
其他用戶讀
其他用戶寫

IPC對象的持續性

IPC對象的持續性,指的是該類型的一個對象一直存在多長時間,IPC的持續性有三類:

  • 隨進程持續:IPC對象一直存在到打開該對象的最後一個進程關閉該對象
  • 隨內核持續:IPC對象一直存在到內核重新自舉顯式刪除該對象為止
  • 隨文件系統持續:IPC對象一直存在到顯式刪除該對象為止,即使內核重新自舉,該對象依然存在

在默認情況下,除了Posix無名訊號量是隨進程持續,其餘所有Posix IPC和System V IPC都是隨內核持續。

2. 訊號量概述

訊號量定義及分類

訊號量是一種用於進程間同步或執行緒間同步的機制,共有三種類型的訊號量IPC:

  • Posix有名訊號量
  • Posix無名訊號量
  • System V訊號量

按訊號量值的範圍,可分為:

  • 記錄訊號量:訊號量的值可以為負數,負數的絕對值代表當前因等待該訊號量的值變為正數而阻塞的進程和執行緒數
  • 計數訊號量:訊號量的值必須是非負整數,二值訊號量(訊號量值只能為0或1)是其特殊情況,Linux採用計數訊號量

訊號量操作

  • 創建(create):創建訊號量時需要指定初始值
  • 等待(wait):也叫P操作,若訊號量的值大於0就將它減1並結束操作,否則就阻塞等待
  • 掛出(post):也叫V操作,該操作將訊號量的值加1

訊號量、互斥鎖和條件變數的差異

  • 互斥鎖必須由給他上鎖的執行緒解鎖,而訊號量的等待和掛出沒有這種限制
  • 互斥鎖只有上鎖和解鎖兩種狀態,訊號量可以有多個狀態,因為訊號量的值可以有多個
  • 訊號量掛出後的狀態是持續的,即使掛出時沒有執行緒阻塞於該訊號量,掛出操作也不會丟失
  • 條件變數給執行緒發訊號時,若沒有相應的執行緒阻塞,那麼給該訊號將會丟失

3. Posix有名訊號量

Posix有名訊號量由IPC路徑名標識,因此它天生既可用於執行緒同步,又可用於進程同步,相關API在頭文件<semaphore.h>中,編譯時需要指定鏈接-lrt-pthread

創建和打開

sem_open用於創建一個新的訊號量或打開一個已存在的訊號量。

//成功返回訊號量指針,失敗返回SEM_FAILED,鏈接時需指定 -lrt or -pthread  sem_t *sem_open(const char *name, int oflag, ... /*mode_t mode, unsigned int value*/);

函數參數說明在概述中基本都有介紹,這裡不再贅述,只強調兩點:

  • oflag只能指定為0、O_CREAT或O_CREAT | O_EXCL
  • value為訊號量的初始值,可設範圍為[0, SEM_VALUE_MAX]

在Linux中,創建的Posix有名訊號量存放在/dev/shm/目錄下,可通過ls命令查看:

#include <semaphore.h>  #include <fcntl.h>           /* For O_* constants */  #include <sys/stat.h>        /* For mode constants */  #include <stdio.h>    #define POSIX_SEM_NAME  "sem_test"    int main()  {      sem_t *sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 1);        if (sem != SEM_FAILED)      {          printf("sem_open() successn");      }        return 0;  }

關閉和刪除

//兩個函數返回值:成功返回0,失敗返回-1  int sem_close(sem_t *sem);  int sem_unlink(const char *name);
  • sem_close用於關閉已經打開的有名訊號量
  • sem_unlink用於從系統中刪除有名訊號量

進程終止時,會自動關閉所有已打開的IPC對象(包括有名訊號量、消息隊列和共享記憶體),但關閉不等於刪除,因為它們都至少具有隨內核的持續性,這一點從上面示例程式碼的執行結果也可以看出來——進程已終止,但/dev/shm/目錄下剛剛創建的訊號量依然存在。事實上,所有以路徑名標識的Posix IPC都有一個引用計數:

  • close和unlink會使引用計數減1
  • IPC名字本身也佔用一個引用計數
  • 當引用計數大於0時,unlink就能夠從文件系統中刪除IPC對象
  • 如果在引用計數大於1時調用unlink,IPC對象會被刪除,但不會被析構
  • 只有當引用計數變為0,即在引用計數為1時調用unlink,內核才會對IPC對象進行析構
#include <semaphore.h>  #include <fcntl.h>           /* For O_* constants */  #include <sys/stat.h>        /* For mode constants */  #include <stdio.h>    #define POSIX_SEM_NAME  "sem_test"    int main()  {      sem_t *sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 1);        if (sem != SEM_FAILED)      {          printf("sem_open() successn");            printf("before sem_unlink()n");          system("ls /dev/shm/");            sem_close(sem);          sem_unlink(POSIX_SEM_NAME);            printf("after sem_unlink()n");          system("ls /dev/shm/");      }        return 0;  }

等待和掛出

//兩個函數返回值:成功返回0,失敗返回-1  int sem_wait(sem_t *sem);  int sem_post(sem_t *name);

sem_wait用於等待有名訊號量:

  • 若訊號量的值等於0,調用執行緒將阻塞,直到該值變為大於0
  • 若訊號量的值大於0,就將它減1並立即返回

sem_post用於掛出有名訊號量,該函數把訊號量的值加1,然後阻塞於sem_wait等待該訊號量的執行緒就能夠被喚醒。

#include <pthread.h>  #include <semaphore.h>  #include <fcntl.h>  #include <sys/stat.h>  #include <stdio.h>  #include <stdlib.h>    #define POSIX_SEM_NAME  "sem_test"    pthread_t tid[2];  sem_t *sem;    /*thread0先處理自己的工作,之後調用sem_post將訊號量的值加1,通知thread1可以執行了*/  void *thread0(void *arg)  {      int value;        while (1)      {          /* do work thread0 */            sem_post(sem);          sem_getvalue(sem, &value);          printf("thread 0: sem value is %dn", value);          sleep(2);      }  }    /*thread1等待時間比thread0少,但也必須等待thread0調用sem_post將訊號量的值加1,才能繼續執行*/  void *thread1(void *arg)  {      int value;        while (1)      {          sem_wait(sem);          sem_getvalue(sem, &value);          printf("thread 1: sem value is %dn", value);          sleep(1);      }  }    int main()  {      sem = sem_open(POSIX_SEM_NAME, O_CREAT, 0666, 0);        pthread_create(&tid[0], NULL, thread0, NULL);      pthread_create(&tid[1], NULL, thread1, NULL);      sleep(10);        pthread_cancel(tid[0]);      pthread_join(tid[0], NULL);        pthread_cancel(tid[1]);      pthread_join(tid[1], NULL);        sem_close(sem);      sem_unlink(POSIX_SEM_NAME);        return 0;  }

獲取訊號量的值

//成功返回0,失敗返回-1  int sem_getvalue(sem_t *sem, int *sval);

sem_getvalue用於獲取訊號量sem的當前值,該值通過參數sval返回。如果有執行緒或進程正阻塞於sem_wait,POSIX.1-2001允許通過sval返回兩種結果:

  • 返回0,這也是Linux的選擇,因為Linux採用計數訊號量
  • 返回一個負值,其絕對值代表當前阻塞於sem_wait調用的進程和執行緒數,對應記錄訊號量

4. Posix無名訊號量

Posix無名訊號量是基於記憶體的訊號量,也就是說它沒有IPC路徑名,而是像普通變數一樣創建在記憶體中。

  • Posix無名訊號量由sem_init初始化,由sem_destroy銷毀
  • Posix無名訊號量沒有close和unlink之分,銷毀即徹底刪除
  • Posix無名訊號量等待、掛出、獲取訊號量的值使用和有名訊號量相同的API
//兩個函數返回值:成功返回0,失敗返回-1  int sem_init(sem_t *sem, int shared, unsigned int value);  int sem_destroy(sem_t *sem);

sem_init的sem參數指向要初始化的訊號量,shared參數用於指定訊號量在執行緒間共享還是在進程間共享:

  • shared = 0:在執行緒間共享,訊號量創建在當前進程地址空間中,可用於執行緒間同步,隨進程持續
  • shared ≠ 0:在進程間共享,訊號量必須創建在共享記憶體中,可用於進程間同步,隨內核持續

一般來說,執行緒間同步使用有名訊號量和無名訊號量都可以,而進程間同步直接使用有名訊號量就可以了,除非對通訊速度有特殊需求,才考慮shared ≠ 0的無名訊號量。

把第3章的示例程式碼改為使用shared = 0的無名訊號量,只有main函數發生了變動,如下所示:

int main()  {      sem = (sem_t *)malloc(sizeof(sem_t)); //這裡使用動態分配,也可以使用靜態分配sem,然後給sem_init傳&sem      sem_init(sem, 0, 0);        pthread_create(&tid[0], NULL, thread0, NULL);      pthread_create(&tid[1], NULL, thread1, NULL);      sleep(10);        pthread_cancel(tid[0]);      pthread_join(tid[0], NULL);        pthread_cancel(tid[1]);      pthread_join(tid[1], NULL);        free(sem);      sem_destroy(sem);        return 0;  }

5. Posix訊號量限制

Posix定義了兩個訊號量限制:

  • SEM_NSEMS_MAX:一個進程可同時打開的最大訊號量個數,該值至少為256
  • SEM_VALUE_MAX:訊號量的最大值,該值至少為32767