[TOC] 1. Posix IPC 概述 以下三種類型的IPC合稱為Posix IPC: Posix信號量 Posix消息隊列 Posix共用記憶體 Posix IPC在訪問它們的函數和描述它們的信息上有一些類似點,主要包括: IPC名字 創建或打開時指定的讀寫許可權、創建標誌以及用戶訪問許可權 下表匯 ...
目錄
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標誌時,會出錯返回EEXISTO_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
:一個進程可同時打開的最大信號量個數,該值至少為256SEM_VALUE_MAX
:信號量的最大值,該值至少為32767