十六、進程間通訊--信號量

一、相關基礎概念

(一)進程關係

1.進程同步:進程之間相互合作,協同工作的關係稱爲進程的同步。簡單來說就是多個相關進程在執行次序上的協調,誰先執行後執行都有順序。是一種直接制約關係
2.進程互斥:多個進程因爲爭奪臨界資源而互斥執行稱爲進程的互斥。是一種間接制約關係。

如多個人打籃球,籃球是臨界資源,形成互斥,對他們形成了一個間接制約關係。工廠上流水線工作,一道工序接着下一道,有明確的次序,形成同步,上一道的工序對下一道工序有直接影響,是一種直接制約關係。

(二)臨界資源&臨界區

1.臨界資源:在操作系統中,把那些可以被進程共享的資源(文件,打印機等),但是在同一時刻只允許一個進程/線程訪問的資源統稱爲臨界資源或共享變量。
2.臨界區:訪問臨界資源的那段代碼。

(三)原子操作

原子操作是指不會被進程/線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何進程切換。故原子操作要不然不做,要不然一直做到結束。

(四)PV操作

這兩個字母來源於荷蘭語單詞:passeren傳遞,就好像進入臨界區;vrijgeven釋放,退出臨界區。假設信號量爲SV,那麼P、V操作含義如下:

  • P(SV):如果SV的值大於1,進行減一操作,表示獲得資源;如果SV的值爲0,則掛起進程的執行,即阻塞,因爲目前已經沒有資源了。
  • V(SV):如果當前有掛起的進程在等待資源,那麼執行V就會將它喚醒;如果沒有,SV加一,即釋放資源。

P操作會出現阻塞,V操作永遠不會阻塞。

二、信號量概念

(一)定義

信號量和前面介紹的IPC(管道,消息隊列)不同,它是一個計數器,用於多線程對共享數據對象的訪問。和前面的有本質的不同,前面學的管道,消息隊列是爲了傳送數據,而信號量是以保護共享資源(臨界資源)或保證進程同步爲目標的,不存儲進程間的通信數據。故信號量機制一般用在進程或線程之間的同步與互斥。

信號量是一個特殊的計數器,一般取正數值。它的值代表允許訪問的資源數目。一般有兩種常用取值:

  • 信號量的值如果只取0,1,將其稱爲二元信號量,控制單個資源,初始值爲1。
  • 如果信號量的值大於1,則稱之爲計數信號量,說明有多個臨界資源單位可共享。

(二)使用

使用信號量獲得共享資源,進程需要執行下列操作:

  1. 測試控制臨界資源的信號量。
  2. 若此信號量的值爲正,則進程可以使用該資源,進程P操作將信號量減一,表示它使用了一個資源單位。
  3. 若此信號量的值爲0,則進程進入休眠狀態,因爲此時沒有資源可以獲取。如果進程被喚醒後,它返回至第(1)步。
  4. 當進程不再使用由一個信號量控制的臨界資源時,V操作將信號量值加1。如果有進程正在阻塞等待此信號量,則喚醒它。

上述操作均爲原子操作,所以信號量通常在內核中實現。

(三)特點


  • 本質是一個計數器,內存中有多少個臨界資源,信號量的數字就是多少。
  • 信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作
  • 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
  • 信號量不能傳遞複雜消息,只能用來同步。

三、信號量函數

內核爲每個信號量集合維護了一個semid_ds結構體,具體成員如下:

strcut semid_ds{
    struct ipc_perm sem_perm;  //權限結構體
    time_t          sem_otime; //最後一次操作時間
    time_t          sem_ctime; //最後一次改變時間
    unsigned long   sem_nsems; //在集合中信號量
};

我們要操作的信號量就包含在這個信號量集合中,信號量的操作大多數也是通過這個信號量集合來操作的。

(一)創建/獲取信號量集合semget函數

對信號量進行操作,我們先創建或獲得一個信號量ID,那麼調用semget函數,函數原型如下:

#include<sys/sem.h>
int semget(key_t key,int nsems,int flag);
               成功返回信號量ID,失敗出錯返回-1

參數:

  • key: 有兩種辦法可以獲得,在消息隊列講解中我們已詳細寫出兩種辦法,兩個進程使用相同key值,就可以使用同一個信號量。
  • nsems:如果是創建新信號量集合,那麼nsems代表新信號量集合中的信號量的數目。如果是獲取當前存在的信號量集合,那麼此參數爲0。
  • flag: 和消息隊列的設置一樣,標識函數的行爲信號量集合的權限,取值如下:
取值 含義
IPC_CREATE 創建信號量集合
IPC_EXCL 檢測信號量集合是否存在
位或權限位 可以設置信號量集合的訪問權限,和其他兩個參數可以或表示,取值和open函數的open_t一樣,一般爲0664

如果semget函數用來創建一個新集合,那麼內核會自動把新信號量集合的strut semid_ds結構體做以下初始化:

  • 初始化ipc_perm結構體,該結構體中的mode成員按semget函數的flag參數中的相應權限位設置。
  • sem_otime設置爲0。
  • sem_ctime設置爲當前時間。
  • sem_qnsems設置爲semget函數的nsems參數的值。

(二)設置信號量集合semctl函數

semctl函數可以對信號量集合/信號量進行不同的操作,如初始化,刪除等,函數原型爲:

# include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,……/*union semun arg*/);
                cmd參數爲SETAVL,IPC_RMID參數,函數返回值成功爲0,失敗返回-1

參數:

  • semid: 信號量集合ID

  • semunm: 用來指定該信號量集合中的某一特定信號量成員,也就是信號量對應的ID。

  • cmd參數: 用來指定對信號量集合的操作,通常用下面兩個值:
    (1) SETVAL: 在信號量第一次使用之前,把信號量初始一個已知的值,通過自定義union semun中的val成員設置。
    (2)IPC_RMID: 刪除信號量集合,立即發生。

  • arg參數: 此參數可選的,取決於請求的命令,一般初始化時使用,如果使用該參數,必須要自己定義聯合體,表示信號量的相關信息。

    union semun{
       int              val;   //信號量的值
       struct semid_ds* buf;   // ipc_stat,ipc_set的緩衝區
       unsigned short* array;  //SETALL,GETALL數組
       struct seminfo* _buf;   //ipc_info緩衝區(Linux專用) 
    };
    

    注意:arg指向的聯合體名稱,成員均不固定,可以自行選擇需要的。一般聯合體成員包含信號量值即可。

(三)操作信號量semop函數

利用semop函數可以改變信號量的值,其函數原型爲:

# include<sys/sem.h>
int semop(int semid,struct sembuf* sops,unsigned nsops);
                   成功返回0,出錯返回-1

semop函數具有原子性,要不然執行所有操作,要不然一個也不執行。
參數:

  • semid: 信號量集合ID。

  • sops參數: 參數爲一個指針,指向一個由struct sembuf結構表示的信號量操作數組,數組中的每個sembuf結構體對應一個信號量ID,以及對該信號量ID進行操作的標誌:

    struct sembuf{
         unsigned short sem_num;//信號量ID
         short          sem_op; //信號量操作
         short          sem_flg;//操作標誌
    };
    

    其中結構體中成員的取值有不同的使用:
    (1)sem_op:

    1.如果sem_op爲正值,如1,表示釋放sem_num對應的信號量的資源(信號量的值增加),如同V操作。
    2.如果sem_op爲負值,如-1,表示獲取sem_num對應的信號量資源(信號量的值減少),如同P操作。若此時已經沒有資源了進行-1操作:

    • 如果指定了IPC_NOWIT,會出錯返回。
    • 未指定,進程會被掛起,知道有資源或捕捉到信號結束掛起。

    3.如果sem_op爲0,表示調用進程希望該信號量值變爲0。如果當前信號量是0,則此函數立即返回。

    (2)sem_flag:

    • 默認填0。
    • SEM_UNDO:進程退出後,該進程對sem進行的操作都被撤銷,例如對信號量值進行加1或減1操作,則進程退出後這些操作都被撤銷。
  • nsops參數:
    對應參數sops數據的元素個數。

(四)封裝系統調用函數實現一系列功能

在進行進程同步控制時,經常說到P、V操作,那麼如何用上述的系統調用封裝一系列的函數,讓我們使用起來方便。根據需求我們將其分爲4類函數:創建/獲取信號量;P操作;V操作;刪除信號集合。我們將表示信號量信息的union semun聯合體(因爲業務簡單,所以聯合體中只包含信號量的值val即可)和函數聲明寫在自己創建的.h文件中。

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>


//保存信號量值的聯合體
union semval
{
    int val;
};
//創建/獲得信號量集合
int CreateSem(int key,int init_val[],int len);
//減1操作
void SemP(int semid,int index);
//加1操作
void SemV(int semid,int index);
//刪除信號量集合
void DeleteSem(int semid);

我們獲取key值不通過ftok函數獲取,而是手動強轉。

(key_t)k;//k爲正整數,如1234,強轉爲key值

1. 創建/獲取信號量集合函數: 主要使用系統調用函數semget獲取/創建信號量集合,如果創建信號量集合需要用semctl對信號量集合進行初始化。我們可以實現函數的流程進行說明:

int CreateSem(int key,int init_val[],int len);
//key爲信號量集合標識符,強轉;init_val[]爲信號量集合,len爲長度,即信號量個數。
  • 首先嚐試獲取信號量集合ID: 用semget函數,key強轉爲key_t類型,傳參數nsems爲0,不指定信號量集合方式,只指定獲取權限爲0664。如果返回-1,表示當前沒有信號量集合,否則我們將返回的信號量集合ID返回即可。
  • 進行信號量集合創建: 利用semget函數,參數nsems爲len,即信號量個數,指定方式爲IPC_CREATE,打開權限爲0664。
  • 進行信號量集合初始化: 循環信號量數組,將信號量的值給semun聯合體中的val,通過函數semctl,用semun聯合體對創建出來的信號量集合進行初始化,cmd參數爲SETVAL。
  • 初始化完成,返回信號量集合的標識符。

2. P操作:主要利用semop函數,對參數sops進行設置實現:

void SemP(int semid,int index);
//semid表示信號量集合標識符ID,index表示進行P操作的信號量ID
  • 創建sembuf類型的結構體buf,對其參數進行設置,井sem_op設置爲-1,表示執行semop函數就會進行資源獲取,-1操作。
  • 調用函數semop對信號量進行-1操作。

3. V操作:主要利用semop函數,對參數sops進行設置實現:

void SemV(int semid,int index);
//semid表示信號量集合標識符ID,index表示進行V操作的信號量ID
  • 創建sembuf類型的結構體buf,對其參數進行設置,井sem_op設置爲1,表示執行semop函數就會進行資源釋放,+1操作。
  • 調用函數semop對信號量進行+1操作。

4. 刪除整個信號量集:利用函數semctl,對cmd參數設置實現:

void DeleteSem(int semid);
//semid表示信號量集合標識符ID
  • 直接調用semctl函數,將semunm參數設置爲0,表示刪除所有信號,cmd參數爲IPC_RMID。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"

//創建/獲得信號量集合
int CreateSem(int key,int init_val[],int len)
{
    //
    int semid=semget((key_t)key,0,0664);
    if(semid!=-1)
    {
        return semid;
    }
    //
    semid=semget((key_t)key,len,IPC_CREAT|0664);
    if(semid==-1)
    {
        perror("Create Sem Error\n");
        return -1;
    }
    //
    int i=0;
    for(;i<len;i++)
    {
        union semval data;
        data.val=init_val[i];
        if(semctl(semid,i,SETVAL,data)==-1)
        {
            perror("Init Sem Value Fail\n");
            return -1;
        }
    }
    return semid;
}
//減1操作
void SemP(int semid,int index)
{
    struct sembuf buf;
    buf.sem_num=index;
    buf.sem_op=-1;//
    buf.sem_flg=SEM_UNDO;

    if(semop(semid,&buf,1)==-1)
    {
        perror("Sem P Error\n");
    }
}
//加1操作
void SemV(int semid,int index)
{
    struct sembuf buf;
    buf.sem_num=index;
    buf.sem_op=1;
    buf.sem_flg=SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        perror("Sem V Error\n");
    }
}
//刪除信號量集合
void DeleteSem(int semid)
{
    if(semctl(semid,0,IPC_RMID)==-1)
    {
        perror("Delete Error\n");
    }

}

四、實例

例題: 進程a和進程b模擬訪問打印機,進程a輸出第一個字符’a’表示開始使用打印機,輸出第二個字符‘a’表示結束打印機;b進程的操作和a進程相同,每個進程循環使用打印機5次。現在打印機爲臨界資源,所以在某一時刻它只能被一個進程使用,所以輸出的結果不應該出現:abab,abba等情況。

1.我們不對這兩個進程採取信號量進行同步控制,
代碼如下:b.c打印b,a.c代碼一樣,只不過打印a。

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"

int main()
{

    while(1)
    {
        printf("b\n");
        sleep(5);
        printf("b\n");
        sleep(2);
    }
    exit(0);
}

在這裏插入圖片描述

輸出了在我們預料之中的混亂數據,這就是沒有加同步控制出現的結構,並不是我們想要的,那麼我們現在進行一個同步控制。

2.採取信號量進行同步控制:
打印機輸入臨界資源,那麼設定一個信號量來管理打印機:

  • 它只有一臺,所以初始值爲1,在a進程使用時,我們對其進行一個P操作,即讓a獲取打印機,此時信號量爲0,如果b來訪問就會被掛起。
  • 在a使用完打印機後,我們釋放打印機資源,進行V操作,這是信號量爲1,b進程這時就可以訪問打印機,b也是一樣的,訪問前進行一次P操作獲取,打印完畢進行一次V操作釋放。

代碼如下:

a.c:

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"

int main()
{
    int val=1;//信號量初始值
    int semid=CreateSem(1234,&val,1);//a先運行,創建信號集合IDkey爲1234,值爲1,1個信號量的信號集合
    assert(semid!=-1);

    int count=0;//使用打印機次數

    while(1)
    {
        SemP(semid,0);//P,佔有打印機
        printf("a\n");
        sleep(5);
        printf("a\n");
        SemV(semid,0);//V,釋放打印機
        sleep(2);
        count++;
        if(count==5)
        {
            break;
        }
    }
    exit(0);
}

b.c:

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<string.h>
# include "./sem.h"

int main()
{
    int val=1;//信號量初始值
    int semid=CreateSem(1234,&val,1);//b後運行,直接獲取到信號量
    assert(semid!=-1);

    int count=0;//使用打印機次數

    while(1)
    {
        SemP(semid,0);//P,佔有打印機
        printf("b\n");
        sleep(5);
        printf("b\n");
        SemV(semid,0);//V,釋放打印機
        sleep(2);
        count++;
        if(count==5)
        {
            break;
        }
    }
    exit(0);
}

進行編譯時爲:

gcc -o a a.c sem.c //需要加上自己寫的頭文件

在這裏插入圖片描述
我們觀察結果,發現沒有出現abab這種a,b進程混亂使用打印機的現象,都是a/b進程打印完了,釋放資源,a/b進程再進行獲取打印。

加油哦!💪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章