1. 程式人生 > >Linux程序間通訊(四)訊號量

Linux程序間通訊(四)訊號量

雖然本文是記錄使用訊號量保證程序的同步與互斥的,但是其實也可以看做是程序之間的通訊問題,為了與前面的保持一致,所以還是叫做 Linux程序間通訊了 (強迫症...)

訊號量

基本概念

程序間通訊的方式有管道、訊息佇列、共享記憶體這些都是程序間的資訊通訊,而訊號量可以理解為程序使用的臨界資源的狀態說明,訊號量主要用於保證同步與互斥

  • 臨界資源:兩個程序看到的一份公共資源稱之為臨界資源
  • 臨界區:各個程序中訪問臨界資源的程式碼叫做臨界區
  • 互斥:每個程序訪問臨界資源的時候必須是獨佔式的(排他式的),只能自己一個人訪問
  • 同步:防止不間斷的佔有資源和釋放資源,這樣的話其他程序就會長時間得不到資源,這樣會造成程序的飢餓問題

由此可見我們之前用於程序間通訊的管道,訊息佇列,共享記憶體都是臨界資源,管道是核心已經提供了同步與互斥,但是訊息佇列和共享記憶體都是不保證同步與互斥的

訊號量PV原語

訊號量:本質上是一把計數器
如果一個訊號只有0或者1,那麼這個就是二元訊號量,所以二元訊號量可以實現互斥鎖

P操作:計數器 --
V操作:計數器 ++
訊號量本身也是臨界資源,所以P、V操作必須是原子的

訊號量集結構

struct ipc_perm { 
       key_t __key;    // 提供給 semget()的鍵 
       uid_t uid;      // 所有者有效 UID  
       gid_t gid;      // 所有者有效 GID 
       uid_t cuid;     // 建立者有效 UID 
       gid_t cgid;     // 建立者有效 GID
       unsigned short mode;     // 許可權 
       unsigned short __seq;    // 序列號
}; 
//訊號量集的結構
struct semid_ds {
    struct ipc_perm sem_perm;   // 所有者和許可權
    time_t sem_otime;           // 上次執行semop的時間  
    time_t sem_ctime;           // 上次更新時間 
    unsigned short sem_nsems;   // 在訊號量集合裡的索引
}

訊號量API

semget

作用:用於建立訊號量集

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

int semget(key_t key, int nsems, int semflg);

key 訊號集的名字,這個與再建立管道、訊息佇列、共享記憶體等用的key是一致的

nsems 訊號集中訊號量的個數,一般為1(訊號集底層就是陣列)

semflg 同建立訊息佇列等一樣的許可權,sem_flags取兩個值,IPC_CREATEIPC_EXCL,需要配許可權使用

  • IPC_CREATE 表示若訊號量已存在,返回該訊號量識別符號
  • IPC_EXCL 表示若訊號量已存在,返回錯誤

return 成功返回一個非負整數,即該訊號集的標識碼;失敗返回-1

num_sems:訊號量的數目,

shmctl

作用:用於控制訊號量集

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int semnum, int cmd, ...);

shmid 這個就是要控制的訊號量集

semnum 這個是具體要控制的訊號量,因為shmid只能指明是哪一個訊號量集(陣列),而semnum就是陣列下標

cmd 將要採取的動作(有三個可取值)

  • SETVAL (常用) 用來把訊號量初始化為一個已知的值。p這個值通過union semun中的val成員設定,其作用是在訊號量第一次使用的時候
  • GETVAL 獲取訊號量集中的訊號量計數值
  • IPC_STAT 把semid_ds結構中的資料設定為訊號量集的當前關聯值
  • IPC_SET 在程序有足夠許可權的情況下,把訊號量集的當前關聯值設定為semid_ds資料結構中給出的值
  • IPC_RMID (常用) 刪除訊號量集

semop

作用:修改訊號量集中的值

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

int semop(int semid, struct sembuf *sops, unsigned nsops);

semid 這個就是要修改的訊號量集

sops 如下結構體的指標,這個結構體是這樣的:

struct sembuf{  
    short sem_num;//除非使用一組訊號量,否則它為0  
    short sem_op;//訊號量在一次操作中需要改變的資料,通常是兩個數,一個是-1,即P(等待)操作,一個是+1,即V(傳送訊號)操作。  
    short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤訊號,並在程序沒有釋放該訊號量而終止時,作業系統釋放訊號量
}; 

nsops 訊號量的個數

return 成功返回0,失敗返回1

訊號量使用示例

makefile

test:comm.c main.c
    gcc -o [email protected] $^

.PHONY:clean
clean:
    rm -rf [email protected]

comm.c && comm.h && main.c

#ifndef __COMM_H__
#define __COMM_H__

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

#define PATHNAME "."
#define PROJ_ID 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 */
};

int createSemSet(int nums);

int initSem(int semid, int nums, int initVal);

int getSemSet(int nums);

int P(int semid, int who);

int V(int semid, int who);

int destorySemSet(int semid);

#endif //!__COMM_H__


//---------------------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 createSemSet(int nums){
  return commSemSet(nums, IPC_CREAT|IPC_EXCL|0666);  
}

int getSemSet(int nums){
  return commSemSet(nums, IPC_CREAT);
}

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

static int commPV(int semid, int who, int op){
  struct sembuf _sf;
  _sf.sem_num = who;
  _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 who){
  return commPV(semid, who, -1);
}

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

int destorySemSet(int semid){
  int ret = semctl(semid, 0, IPC_RMID);
  if(ret < 0){
    perror("semctl");
    return -1;
  }
  return ret;
}
//----------------------------main.c----------------------
#include "comm.h"

int main(){
  int semid = createSemSet(1);
  initSem(semid, 0, 1);

  pid_t id = fork();

  if(id == 0){
    int _semid = getSemSet(0);
    while(1){
      //P(_semid, 0);
      printf("A");
      fflush(stdout);
      usleep(100000);
      printf("A");
      fflush(stdout);
      usleep(100000);
      //V(_semid, 0);
    }
  }
  else{
    while(1){
      //P(semid, 0);
      printf("B");
      fflush(stdout);
      usleep(100000);
      printf("B");
      fflush(stdout);
      usleep(100000);
      //V(semid, 0);
    }
    wait(NULL);
  }

  destorySemSet(semid);
  return 0;
}

開啟PV操作時與未開啟時的對比:
在這裡插入圖片描述
同樣的使用ipcs -s命令即可檢視訊號量,使用ipcrm -s即可釋放訊號量資源

程序間通訊總結

管道

  • 資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道
  • 匿名管道只能用於具有親緣關係的程序,否則使用命名管道
  • 管道內部保證同步機制,從而保證訪問資料的一致性。
  • 管道是面向位元組流的
  • 管道生命週期隨程序,程序在管道在,程序消失管道對應的埠也關閉,兩個程序都消失管道也消
  • 管道讀端關閉,作業系統向寫端發訊號終止寫端程序
  • 每寫一塊資料的最大長度是有上限的

System V

訊息佇列

  • 訊息佇列提供了一個從一個程序向另外一個程序傳送一塊資料的方法
  • 每個資料塊都被認為是有一個型別,接收者程序接收的資料塊可以有不同的型別值
  • 每寫一塊資料的最大長度是有上限的,這點與管道一致
  • 每個訊息佇列的總的位元組數是有上限的(MSGMNB),系統上訊息佇列的總數也有上限
  • 訊息佇列生命週期隨核心
  • 不保證同步與互斥

共享記憶體

  • 共享記憶體區是最快的IPC形式,無需核心干預,直接對映同一塊實體記憶體
  • 作為IPC資源存在,共享記憶體生命週期同樣隨核心
  • 不保證同步與互斥

訊號量

  • 主要提供對程序間共享資源訪問控制機制。相當於記憶體中的標誌,程序可以根據它判定是否能夠訪問某些共享資源,同時,程序也可以修改該標誌。除了用於訪問控制外,還可用於程序同步。
  • 二元訊號量:最簡單的訊號量形式,訊號燈的值只能取0或1,類似於互斥鎖

Socket

  • 兩臺計算機相互通訊本質上也是兩個不在同一個計算機上的程序之間的通訊(事實上本機之間的程序通過Socket通訊也屬於這個範疇),以後再說!