程式間通訊——POSIX 有名訊號量與無名訊號量

ACool發表於2019-03-04

原文地址: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 。

如果訊號量存在,則呼叫第二個函式會忽略後面兩個引數(即 modevalue )。

釋放

當完成訊號量操作以後,可以呼叫 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按順序向管道寫一段文字,最後父程式從管道將子程式寫入的內容讀出來,要保證程式執行的先後順序,可以用有名訊號量來解決。

#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<semaphore.h>
#include<sys/sem.h>
#include <sys/stat.h>

#include<fcntl.h>

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){//子程式1
        resource1=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!
");
        }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
",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 銷燬。如果不這樣做,可能會導致某些實現出現資源洩漏。

例子

使用無名訊號量實現有名訊號量中的例子:

#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<semaphore.h>
#include<sys/sem.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include<fcntl.h>

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);//初始化共享記憶體訊號量值為0
    pipe(fd);//建立一個無名管道

    pid1 = fork();
    if(pid1<0){
        printf("error in the first fork!");
    }else if(pid1==0){//子程式1

        Cpid1 = 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!
");
        }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
",inpipe);

                exit(0);
        }

    }
    return 0;
}
複製程式碼

相關文章