進程間通信-信號量


信號量實質上是一個用來描述臨界資源數目的計數器。本身不具有數據交換的功能,而是通過控制其他的通信資源(文件,外部設備)來實現進程間通信,它本身只是一種外部資源的標識。信號量在此過程中負責數據操作的互斥、同步等功能。

當進程不再使用一個信號量控制的共享資源時,信號量的值+1,對信號量的值進的增減操作均爲原子是操作,這是由於信號量主要的作是維護資源的互斥或多進程的同步訪問。在信號量的創建及初始化上,不能保證操作均爲原性。

爲什麼要用到信號量呢?

當一個臨界資源同時被多個進程訪問時容易出現錯誤,那麼就必須有一種機制來讓代碼的臨界區域只能有一個線程來訪問。臨界區域是指執行數據更新的代碼需要獨佔式地執行。也就是說信號量實際上是來協調臨界資源被進程訪問的。其中進程間通行的另一種方式共享內存就要使用採用信號量。

信號量常見的操作有P()操作和V()操作。

P(sv):如果sv的值等於零,就給它減1;如果它的值爲零,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1. 


舉例說明:一個教室一次只能有一個人學習。當幾個人同時想進教室學習,但是最多隻能進一個,這時管理教室的人就會讓最先到達的人進教室學習,然後教室的信息(容量)由1變爲0;後面的人依次排隊,等第一個人出來之後信息由0變爲1;這裏的進教室就相當於P()操作,出教室相當於V()操作,而等待的人相當於進程掛起!


下來看看如何創建一個信號量,這裏用到的函數是semget();先來看看他的參數


key值和前面的消息隊列key一樣,是一個唯一的標識,第二個參數是第多少個信號量,第三個參數和管道的格式是一個IPC_CREAT和IPC_EXCL。


信號量的銷燬函數爲semctl();各個參數如下


其中semun是一個聯合體對象,調用的時候在頭文件裏聲明一下。


調用PV所用到的函數是semop函數,先來看看semop參數


第一個參數就是要操作的信息量ID,第二個參數是一個結構體的地址,第三個第四個分別是結構體裏的元素。

這裏如果要進行P操作,實參傳的就是-1,如果是V操作參數傳的就是1;

具體的在代碼裏看(採用接口封裝方式)

comm.h

#ifndef _COMM_H_
#define _COMM_H_

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>



#define PATHNAME "."
#define PROJ_ID 0x6666

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *_buf;
};

int initSemset(int semid, int num, int val);
int creatSemset(int nums);
int getSemset(int nums);
int destroySemset(int semid);
int P(int semid, int nums);
int V(int semid, int nums);

#endif

comm.c

#include "comm.h"

static int commSemset(int nums, int flags)
{
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if(_key < 0){
        perror("ftok");
        return -1;
    }
    int semid = semget(_key, nums, flags);
    if(semid <  0){
        perror("semget");
        return -2;
    }
    return semid;
}

int initSemset(int semid, int num, int val){
   union semun _un;
    _un.val = val;
    if(semctl(semid, num, SETVAL, _un) < 0){
        perror("semstl");
        return -1;
    }
    return 0;
}



int creatSemset(int nums){
    return commSemset(nums, IPC_CREAT|IPC_EXCL);
}


int getSemset(int nums){
    return commSemset(nums, IPC_CREAT);
}

int destroySemset(int semid){
    if(semctl(semid, 0, IPC_RMID) < 0){
        perror("semctl");
        return -1;
    }
    return 0;
}

static int commPV(int semid ,int nums, int op){
    struct sembuf _sf;
    _sf.sem_num = nums;
    _sf.sem_op = op;
    _sf.sem_flg = 0;

    if(semop(semid, &_sf, 1) < 0){
        perror("semop");
        return -1;
    }
    return 0;
}


int P(int semid, int nums){
    return commPV(semid, nums, -1);
}

int V(int semid, int nums){
    return commPV(semid, nums, 1);
}


如果要是不用信號量直接想通過父子進程直接往標準輸出裏輸出成對的A和成對的B,那麼會出現什麼現象呢?


可以看到打印出來的A和B是隨機輸出的,那是因爲兩個進程防僞同一塊臨界區域時,先後次序是內核隨機讀取的,所以會隨機的輸出。當採用信號量來加以約束時,會將臨界區域保護起來,等一個進程訪問完後,另一個進程再進行訪問!將會出現A和B成對的出現!即信號量滿足原子性!


test_sem.c

#include "comm.h"

int main()
{
    int semid = creatSemset(12);
    initSemset(semid, 0, 1);

    if(fork() == 0){
        int _semid = getSemset(0);
        while(1){
            P(_semid, 0);
            printf("A");
            fflush(stdout);

            usleep(124556);
            printf("A ");
            fflush(stdout);
            usleep(545);
            V(_semid, 0);
        }
    }
    else{
        while(1){
            P(semid, 0);
            printf("B");
            fflush(stdout);
            usleep(124556);
            printf("B ");
            fflush(stdout);
            usleep(345);
            V(semid, 0);
        }
    }
    return 0;
}



當採用了信號量的P操作後,相當於已經有一個進程正在訪問它了,但是如果正在訪問臨界區域的這個進程意外死掉之後,信號量只進行了-1操作,沒有+1,。那麼這塊臨界區域相當於被鎖死了,打開臨界區域的鑰匙已經隨進程的死亡而消失了,這時候就會造成臨界區域被永久鎖死的現象,以後這塊區域不會再被訪問到了!

 

爲了防止這種問題的出現,semop函數會指定一個標誌sem_undo,每個內核對IPC信號量資源所執行的可撤銷操作,都存放在一個叫sem_undo的數據結構中,比如上個例子中semop()做的+1,-1這兩個操作,在進程結束時,內核就做了一個sem_undo的數據結構中記錄的數值的反向操作,來達到IPC信號量計數器回滾的目的。


IPC_UNDO標誌保證進程終止後,它對信號量的修改都撤銷,好像它從來沒有操作過信號量一樣。

發佈了43 篇原創文章 · 獲贊 47 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章