1. 程式人生 > >Posix訊號量

Posix訊號量

目錄

  • 1. Posix IPC
    • 概述
    • IPC名字
    • 建立與開啟IPC
      • 讀寫許可權與建立標誌
      • 使用者訪問許可權
    • IPC物件的持續性
  • 2. 訊號量概述
    • 訊號量定義及分類
    • 訊號量操作
    • 訊號量、互斥鎖和條件變數的差異
  • 3. Posix有名訊號量
    • 建立和開啟
    • 關閉和刪除
    • 等待和掛出
    • 獲取訊號量的值
  • 4. Posix無名訊號量
  • 5. Posix訊號量限制

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() success\n");
    } 
    
    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() success\n");
        
        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 %d\n", 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 %d\n", 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