Linux 進程通信IPC對象之信號量

什麼是信號量

信號量與其他IPC對象不同,它是一個計數器,用於多個進程對共享數據對象的訪問,它的本質是一種數據操作鎖,它不像消息隊列和管道那樣具有數據交換的功能,而是通過控制其他的通信資源(文件,外部設備)來實現進程間通信。

如何通過信號量來控制進程間通信

爲了獲得共享資源,進程需要執行下列操作:
(1)、測試控制該資源的信號量;
(2)、若此信號量爲正,則進程可以使用該資源,在這種情況下進程會將信號量值減一,表示它使用了一個資源單位;
(3)、若此信號量的值爲0,則進程進入休眠狀態,直至信號量值大於0,進程才被喚醒,此時又返回到步驟(1)。
注:信號量的測試和減一操作應當是原子操作,爲此信號量通常是在內核中實現的。

爲什麼要使用信號量

使用信號量是爲了防止因多個程序同時訪問一個共享資源而引發的一系列問題,而信號量就是這樣的一種方法,它可以保證任一時刻只能有一個執行線程訪問代碼的臨界區域(臨界區域是執行數據更新的代碼需要獨佔式的執行),也就是說信號量是用來協調進程對共享資源的訪問的。

操作系統是如何對信號量進行管理的

  1. 首先,內核爲每個信號量集合維護着一個semid_ds結構
    (semid_ds.png)

    這個結構中包含了每個IPC對象都會包含的成員那就是ipc_perm(這個結構主要規定了IPC對象的權限和所有者),另外還含有信號量集合中元素數量sem_nsems成員,以及信號量處理的相關時間。

  2. 每個信號量其實是由一個無名結構體所表示的,它包含下列成員

struct {
  unsigned short semval;
  pid_t                 sempid;
  unsigned short semncnt;
  unsigned short semzcnt;
}

另外,操作系統還提供了相關的信號量系統調用接口,來使用戶對信號量進行管理。

信號量接口函數

  • 信號量的創建與獲得
int semset(key_t key,int nsems,int flag);     
//若成功,返回信號量ID,若失敗,返回-1

當創建一個新的信號量集合時,要對semid_ds結構下列成員賦初值
sem_otime設置爲0; //sem_otime是最後一次semop()的時間 sem_ctime設置爲當前時間; //是最後一次調用semctl()的時間
sem_nsems設置爲nsems; //信號量集合中的信號量數,如果是創建則必須要指定nsems,如果引用現有的那麼將nsems設置爲0即可

  1. semctl包含了多種信號量操作
int semctl(int semid ,int semnum, int cmd,.../*union semun arg*/ );

該函數包含了對信號量的多種操作其中第四個參數是可選的,是否使用取決於cmd
如果使用了第四個參數,那麼它的類型是 union semun;
(semum)

cmd:cmd參數通常指定下列十種參數的一種
IPC_STAT:對此集合取semid_ds結構,並存儲在arg.buf中;
IPC_SET:按arg.buf指向的結構中的值,設置此集合相關的結構中的sem_perm.uid,sem_perm.gid和sem_perm.mode字段。
IPC_RMID:從系統中刪除該信號量的集合,刪除立即生效。
GETVAL:返回成員semnum的semval值,由arg.va指定(這條在對信號量的初始化中用到);
SETVAL:設置成員semnum的setval值;
GETPID:返回成員semnum的sempid值;
GETALL:取信號量集合中所有的信號量值,這些值存儲在arg.array中;
SETALL:將該集合中的所有信號量值設置成指向arg.array指向的數組中的值;

  1. semop函數
int semop(int semid, struct sembuf semoparry[], size_t nops);
//若成功,返回0,若出錯,返回-1

nops:nops,規定了該數組(semoparry[])中操作信號量的數量;
* semoparry*:是一個類型爲struct sembuf的數組,sembuf的成員如下
這裏寫圖片描述
sembuf是一個表示信號量操作的數組;

關於對信號量的操作主要與sembuf中的sem_op成員有關:
(1):若sem_op爲正值,此時對應於進程釋放的或佔用的資源數(所以信號量的V操作的sem_op一般爲正值,),sem_op值會加到信號量的值上;如果指定了undo標誌(對應於sem_flg成員的SEM_UNDO位),則也從該進程的此信號量調整值減去sem_op.
(2):若sem_op爲負值,則表示調用進程要獲取由該信號量控制的資源(所以我們的P操作的sem_op一般爲負值,並且這個值的絕對值要小於等於semval).
(3):若sem_op爲0:表示調用進程希望等待到該信號量的值變爲0:

SEM_UNDO

SEM_UNDO是semun中的sem_flg成員的標誌位,設定該標誌位主要是用來異常退出進程時進行調整,操作系統所設置的調整如下:
由於信號量的生命週期是“隨系統”,即如果我們創建了信號量集合,但是沒有對其進行刪除,那麼信號量便會一直存在在操作系統中,直到操作系統關閉或者用戶使用命令刪除爲止,而如果我們爲信號量設定了SEM_UNDO標誌,如果sem_op的值小於0(即此時調用進程佔有臨界區域),當該進程終止時,內核都會檢驗進程是否還有尚未處理的信號量調整值,如果有則進行相應的調整。
 當操作信號量(semop)時,sem_flg可以設置SEM_UNDO標識;SEM_UNDO用於將修改的信號量值在進程正常退出(調用exit退出或main執行完)或異常退出(如段異常、除0異常、收到KILL信號等)時歸還給信號量。

測試用例

我們在父進程中用fork創建子進程,然後父進程向屏幕上打印AA,子進程向屏幕上打印BB,觀察在設置信號量和沒有設置信號量時所出現的不同的情況,案例源代碼如下:

信號量頭文件

/*************************************************************************
    > File Name: MySem.h
    > Author: LZH
    > Mail: [email protected] 
    > Created Time: 2017年02月16日 星期四 02時47分13秒
 ************************************************************************/

#ifndef __SEM_H__
#define __SEM_H__
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define PATHNAME "."
#define PROJID 0x6666

union semun {

    int val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */
};

int CreatSem(int nsems); 
int InitSem(int semid);
int GetSemID();
int P(int semid,int which);
int V(int semid,int which);
int DestorySem(int semid);

#endif //__SEM_H__

信號量源文件

/*************************************************************************
    > File Name: MySem.c
    > Author: LZH
    > Mail: [email protected] 
    > Created Time: 2017年02月16日 星期四 02時49分08秒
 ************************************************************************/

#include "MySem.h"

int InitSem(int semid)
{
    union semun un;
    un.val=1;
    int ret = semctl(semid,0,SETVAL,un);
    if(ret < 0){
        perror("semctl ...\n");
        return -1;
    }
    return 0;
}

static int CommSem(int nsems,int flags)
{
    key_t _k=ftok(PATHNAME,PROJID);
    if(_k < 0){
        perror("ftok error..\n");
        return -1;
    }
    int semid=semget(_k,nsems, flags);
    if(semid < 0){
        perror("semget error..\n");
        return -2;
    }

    return semid;
}

int CreatSem(int nsems)
{
  return  CommSem(nsems,IPC_CREAT | IPC_EXCL |0666);
}

int GetSemID()
{
    return CommSem(0,0);
}

int SemOp(int semid,int op,int num)
{
    struct sembuf buf;
    buf.sem_op=op;
    buf.sem_num=num;
    buf.sem_flg=0;
    int ret = semop(semid,&buf,1);
    if(ret < 0){
        perror("Semop..\n");
        return -1;
    }
    return 0;
}

int P(int semid,int which)
{
    return SemOp(semid,-1,which);
}

int V(int semid,int which)
{
    return  SemOp(semid,1,which);
}

int DestorySem(int semid)
{
    int ret = semctl(semid,0,IPC_RMID);
    if(ret < 0){
        perror("semctl..\n");
    }
    return 0;
}

* 案例測試文件*

#include "MySem.h"

int main()
{
    int semid=CreatSem(10);
    printf("Semid:%d\n",semid);
    InitSem(semid); 
    pid_t id = fork();
    if(id==0){
        while(1){
            P(semid,0);
            usleep(5300);
            printf("A ");
            fflush(stdout);
            usleep(5000);
            printf("A ");
            usleep(10000);
            fflush(stdout);
            V(semid,0);
        }
    }
    else{
        while(1){
            P(semid,0);
            usleep(10300);
            printf("B ");
            fflush(stdout);
            usleep(10000);
            printf("B ");
            usleep(10000);
            fflush(stdout);
            V(semid,0);
        }
    }
    DestorySem(semid);  
    return 0;
}

如果我們沒有對父子進程的臨界區域(打印字符的代碼)使用信號量的相關操作,測試結果如下:
這裏寫圖片描述
我們會發現不像我們想象的那樣出現成對的AA和BB,這時因爲父子進程對輸出屏幕這個共享競爭的結果,如果我們使用了信號量,所得到的測試結果如下:
這裏寫圖片描述
這時輸出屏幕上出現了成對的AA和BB,這樣就體現了信號量原子性操作的價值,原子性操作即要麼不做,要做就一次做完

總結

本文我們主要提到了IPC對象之一——信號量的實現機制和相關操作,我們要能明白是信號量不是進程之間傳遞消息的共享資源,而是管理進程之間交換數據的資源的計數器,此外內核還提供了相關的接口函數來使用戶能夠用信號量管理臨界區域,避免出現多個程序同時訪問一個共享資源所引發的一系列問題,在這些接口函數中,我們特別要注意semop函數,它保證了信號量進行的是原子操作。

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