进程间通信——POSIX 有名信号量与无名信号量
原文地址:blogof33.com/post/9/
前言
在 POSIX 系統中,進程間通信是一個很有意思的話題。
POSIX信號量進程是3種 IPC(Inter-Process Communication) 機制之一,3種 IPC 機制源于 POSIX.1 的實時擴展。Single UNIX Specification 將3種機制(消息隊列,信號量和共享存儲)置于可選部分中。在 SUSv4 之前,POSIX 信號量接口已經被包含在信號量選項中。在 SUSv4 中,這些接口被移至了基本規范,而消息隊列和共享存儲接口依然是可選的。
POSIX 信號量接口意在解決 XSI 信號量接口的幾個缺陷。
-
相比于 XSI 接口,POSIX 信號量接口考慮了更高性能的實現。
-
POSIX 信號量使用更簡單:沒有信號量集,在熟悉的文件系統操作后一些接口被模式化了。盡管沒有要求一定要在文件系統中實現,但是一些系統的確是這么實現的。
-
POSIX 信號量在刪除時表現更完美。回憶一下,當一個 XSI 信號量被刪除時,使用這個信號量標識符的操作會失敗,并將 errno 設置成 EIDRM。使用 POSIX 信號量時,操作能繼續正常工作直到該信號量的最后一次引用被釋放。
——摘自《UNIX高級環境編程(中文第3版)》465-466頁
前段時間筆者在寫管道通信的時候,探究了一下 POSIX 進程間的兩種信號量通信方式:有名信號量和無名信號量。有很多人認為進程間通信只能使用有名信號量,無名信號量只能用于單進程間的多線程通信。其實無名信號量也可以進行進程間通信。
區別
有名信號量和無名信號量的差異在于創建和銷毀的形式上,但是其他工作一樣。
無名信號量只能存在于內存中,要求使用信號量的進程必須能訪問信號量所在的這一塊內存,所以無名信號量只能應用在同一進程內的線程之間(共享進程的內存),或者不同進程中已經映射相同內存內容到它們的地址空間中的線程(即信號量所在內存被通信的進程共享)。意思是說無名信號量只能通過共享內存訪問。
相反,有名信號量可以通過名字訪問,因此可以被任何知道它們名字的進程中的線程使用。
單個進程中使用 POSIX 信號量時,無名信號量更簡單。多個進程間使用 POSIX 信號量時,有名信號量更簡單。
聯系
無論是有名信號量還是無名信號量,都可以通過以下函數進行信號量值操作。
wait
weit 為信號量值減一操作,總共有三個函數,函數原型如下:
#include <semaphore.h> int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);Link with -pthread.這一句表示 gcc 編譯時,要加 -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼其中,第一個函數的作用是,若 sem 小于 0 ,則線程阻塞于信號量 sem ,直到 sem 大于 0 ;否則信號量值減1。
第二個函數作用與第一個相同,只是此函數不阻塞線程,如果 sem 小于 0,直接返回一個錯誤(錯誤設置為 EAGAIN )。
第三個函數作用也與第一個相同,第二個參數表示阻塞時間,如果 sem 小于 0 ,則會阻塞,參數指定阻塞時間長度。 abs_timeout 指向一個結構體,這個結構體由從 1970-01-01 00:00:00 +0000 (UTC) 開始的秒數和納秒數構成。結構體定義如下:
struct timespec {time_t tv_sec; /* Seconds */long tv_nsec; /* Nanoseconds [0 .. 999999999] */}; 復制代碼如果指定的阻塞時間到了,但是 sem 仍然小于 0 ,則會返回一個錯誤 (錯誤設置為 ETIMEDOUT )。
post
post 為信號量值加一操作,函數原型如下:
#include <semaphore.h>int sem_post(sem_t *sem);Link with -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼應用實例
有名信號量
創建
有名信號量創建可以調用 sem_open 函數,函數說明如下:
#include <semaphore.h> sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Link with -pthread.返回值:若成功,返回指向信號量的指針;若出錯,返回SEM_FALLED 復制代碼其中第一種函數是當使用已有的有名信號量時調用該函數,flag 參數設為 0 。
如果要調用第二種函數,flag 參數應設為 O_CREAT ,如果有名信號量不存在,則會創建一個新的,如果存在,則會被使用并且不會再初始化。
當我們使用 O_CREAT 標志時,需要提供兩個額外的參數:
mode 參數指定誰可以訪問信號量,即權限組,mode 的取值和打開文件的權限位相同,比如 0666 表示 所有用戶可讀寫 。因為只有讀和寫訪問要緊,所以實現經常為讀和寫打開信號量。
value 指定信號量的初始值,取值范圍為 0~SEM_VALUE_MAX 。
如果信號量存在,則調用第二個函數會忽略后面兩個參數(即 mode 和 value )。
釋放
當完成信號量操作以后,可以調用 sem_close 函數來釋放任何信號量的資源。函數說明如下:
#include <semaphore.h>int sem_close(sem_t *sem);Link with -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼如果進程沒有調用該函數便退出了,內核會自動關閉任何打開的信號量。無論是調用該函數還是內核自動關閉,都不會改變釋放之前的信號量值。
銷毀
可以使用 sem_unlink 函數銷毀一個有名信號量。函數說明如下:
#include <semaphore.h>int sem_unlink(const char *name);Link with -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼sem_unlink 函數會刪除信號量的名字。如果沒有打開的信號量引用,則該信號量會被銷毀,否則,銷毀會推遲到最后一個打開的引用關閉時才進行。
例子
例如,管道通信中,如果父進程使用 fork()創建兩個子進程1和2,子進程1,2按順序向管道寫一段文字,最后父進程從管道將子進程寫入的內容讀出來,要保證進程執行的先后順序,可以用有名信號量來解決。
int main(){int pid1,pid2;sem_t *resource1; sem_t *resource2; int Cpid1,Cpid2=-1;int fd[2];//0為讀出段,1為寫入端char outpipe1[100],inpipe[200],outpipe2[100];pipe(fd);//建立一個無名管道pid1 = fork();if(pid1<0){printf("error in the first fork!");}else if(pid1==0){//子進程1resource1=sem_open("name_sem1",O_CREAT,0666,0);Cpid1 = getpid();close(fd[0]);//關掉讀出端lockf(fd[1],1,0);//上鎖,則鎖定從當前偏移量到文件結尾的區域sprintf(outpipe1,"Child process 1 is sending a message!");write(fd[1],outpipe1,strlen(outpipe2));lockf(fd[1],0,0);//解鎖sem_post(resource1);sem_close(resource1);exit(0);}else{pid2 = fork();if(pid2<0){printf("error in the second fork!\n");}else if(pid2==0){ resource1=sem_open("name_sem1",O_CREAT,0666,0);resource2=sem_open("name_sem2",O_CREAT,0666,0);Cpid2 = getpid();sem_wait(resource1);close(fd[0]);lockf(fd[1],1,0);sprintf(outpipe2,"Child process 2 is sending a message!");write(fd[1],outpipe2,strlen(outpipe2));lockf(fd[1],0,0);//解鎖sem_post(resource2);sem_close(resource1);sem_close(resource2);exit(0);}if(pid1 > 0 && pid2 >0){resource2=sem_open("name_sem2",O_CREAT,0666,0);sem_wait(resource2);waitpid(pid1,NULL,0);waitpid(pid2,NULL,0);close(fd[1]);//關掉寫端read(fd[0],inpipe,200);printf("%s\n",inpipe);sem_close(resource2);exit(0);}sem_unlink("name_sem1");sem_unlink("name_sem2");}return 0; }復制代碼無名信號量
創建
無名信號量可以通過 sem_init 函數創建,函數說明如下:
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);Link with -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼pshared 參數指示該信號量是被一個進程的多個線程共享還是被多個進程共享。
如果 pshared 的值為 0 ,那么信號量將被單進程中的多線程共享,并且應該位于某個地址,該地址對所有線程均可見(例如,全局變量或變量在堆上動態分配)。
如果 pshared 非零,那么信號量將在進程之間共享,并且信號量應該位于共享內存區域。
銷毀
如果無名信號量使用完成,可以調用 sem_destory 函數銷毀該信號量。函數說明如下:
#include <semaphore.h>int sem_destroy(sem_t *sem);Link with -pthread.返回值:若成功,返回 0 ;若出錯,返回-1 復制代碼注意:
- 銷毀其他進程或線程當前被阻塞的信號量會產生未定義的行為。
- 使用已銷毀的信號量會產生未定義的結果,除非使用 sem_init 重新初始化信號量。
- 一個無名信號量應該在它所在的內存被釋放前用 sem_destroy 銷毀。如果不這樣做,可能會導致某些實現出現資源泄漏。
例子
使用無名信號量實現有名信號量中的例子:
int main(){int pid1,pid2;int Cpid1,Cpid2=-1;int fd[2];//0為讀出段,1為寫入端char outpipe1[100],inpipe[200],outpipe2[100];void *shm = NULL;sem_t *shared;int shmid = shmget((key_t)(1234), sizeof(sem_t *), 0666 | IPC_CREAT);//創建一個共享內存,返回一個標識符if(shmid == -1){perror("shmat :");exit(0);}shm = shmat(shmid, 0, 0);//返回指向共享內存第一個字節的指針shared = (sem_t *)shm;sem_init(shared, 1, 0);//初始化共享內存信號量值為0pipe(fd);//建立一個無名管道pid1 = fork();if(pid1<0){printf("error in the first fork!");}else if(pid1==0){//子進程1Cpid1 = getpid();close(fd[0]);//關掉讀出端lockf(fd[1],1,0);//上鎖,則鎖定從當前偏移量到文件結尾的區域sprintf(outpipe1,"Child process 1 is sending a message!");write(fd[1],outpipe1,strlen(outpipe1));lockf(fd[1],0,0);//解鎖sem_post(shared);exit(0);}else{pid2 = fork();if(pid2<0){printf("error in the second fork!\n");}else if(pid2==0){sem_wait(shared);Cpid2 = getpid();close(fd[0]);lockf(fd[1],1,0);sprintf(outpipe2,"Child process 2 is sending a message!");write(fd[1],outpipe2,strlen(outpipe2));lockf(fd[1],0,0);//解鎖exit(0);}if(pid1 > 0 && pid2 >0){waitpid(pid2,NULL,0);//同步,保證子進程先寫父進程再讀close(fd[1]);//關掉寫端read(fd[0],inpipe,200);printf("%s\n",inpipe);exit(0);}}return 0; } 復制代碼總結
以上是生活随笔為你收集整理的进程间通信——POSIX 有名信号量与无名信号量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 806. Number of Lines
- 下一篇: 动手---sbt(2)