1. 程式人生 > 實用技巧 >Linux訊號量(1)-SYSTEM V

Linux訊號量(1)-SYSTEM V

訊號量概念

訊號量本質上是一個計數器(不設定全域性變數是因為程序間是相互獨立的,而這不一定能看到,看到也不能保證++引用計數為原子操作),用於多程序對共享資料物件的讀取,它和管道有所不同,它不以傳送資料為主要目的,它主要是用來保護共享資源(訊號量也屬於臨界資源),使得資源在一個時刻只有一個程序獨享。

訊號量分類

因為各種原因,Linux下有多種訊號量實現機制,可以分別應用於不同的場合,分類如下:

使用者訊號量主要運行於使用者態,比如程序間都要訪問某個檔案,那麼只有獲得訊號量的程序才能開啟檔案,其他程序會進入休眠,我們也可以檢視當前訊號量的值,以判斷是否要進入臨界區。

核心訊號量主要運行於Linux核心,主要實現對核心臨界資源的互斥使用,比如某個裝置只能被某一個程序開啟,無法開啟裝置的例程會導致使用者空間的程序休眠。

POSIX有名訊號量

主要應用於執行緒。

 sem_t *sem_open(const char *name, int oflag, mode_t mode, int val);
    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    int sem_post(sem_t *sem);
    int sem_close(sem_t *sem);
    int sem_unlink(const char *name);

每個open的位置都要close和unlink,但只有最後執行的unlink生效

POSIX無名訊號量

主要應用於執行緒。

#include<semaphore.h>
sem_t sem;
int sem_init(sem_t *sem, int pshared, unsigned int val); //pshared為0則執行緒間共享,pshared為1則父子程序共享
int sem_wait(sem_t *sem); //阻塞
int sem_trywait(sem_t *sem); //非阻塞
int sem_post(sem_t *sem);
int sem_destroy(sem_t *sem);
程序間共享則sem必須放在共享記憶體區域(mmap, shm_open, shmget),父程序的全域性變數、堆、棧中儲存是不行的

核心訊號量:

#include<asm/semaphore.h>
void sema_init(struct semaphore *sem, int val);
void down(struct semaphore *sem); //可睡眠
int down_interruptible(struct semaphore *sem); //可中斷
int down_trylock(struct semaphore *sem); //m非阻塞
void up(struct semaphore *sem);

除此之外訊號量還有一種分類方法

二值訊號量(binary semaphore)和計數訊號量(counting semaphore)。
二值訊號量:
顧名思義,其值只有兩種0或1,相當於互斥量,當值為1時資源可用;而當值為0時,資源被鎖住,程序阻塞無法繼續執行。
計數訊號量:
其值是在0到某個限制值之間的訊號量。

訊號量的工作原理

訊號量只能進行兩種操作等待和傳送訊號,訊號量操作總結起來,其核心是PV操作,P(sv)和V(sv),他們的行為是這樣的:

(1)P(sv):
如果sv的值大於零,就給它減1;如果它的值為零,就掛起該程序的執行

(2)V(sv):
如果有其他程序因等待sv而被掛起,就讓它恢復執行,如果沒有程序因等待sv而掛起,就給它加1.

在訊號量進行PV操作時都為原子操作(因為它需要保護臨界資源)

注:原子操作:單指令的操作稱為原子的,單條指令的執行是不會被打斷的

System V IPC

講解System V訊號量之前,先了解下什麼是System V IPC。

System V IPC一共有三種類型的IPC合稱為System V IPC:

  1. System V訊號量
  2. System V訊息佇列
  3. System V共享記憶體

System V IPC在訪問它們的函式和核心為它們維護的資訊上有一些類似點,主要包括:

  1. IPC鍵和ftok函式
  2. ipc_perm結構
  3. 建立或開啟時指定的使用者訪問許可權
  4. ipcs和ipcrm命令

下表彙總了所有System V IPC函式。

訊號量 訊息佇列 共享記憶體
標頭檔案 sys/sem.h sys/msg.h sys/shm.h
建立或開啟IPC的函式 semget msgget shmget
控制IPC操作的函式 semctl msgctl shmctl
IPC操作函式 semop msgsnd msgrcv shmat shmdt

IPC鍵和ftok函式

三種類型的System V IPC都使用IPC鍵作為它們的標識,IPC鍵是一個key_t型別的整數,該型別在sys/types.h中定義。
IPC鍵通常是由ftok函式賦予的,該函式把一個已存在的路徑名pathname和一個非0整數id組合轉換成一個key_t值,即IPC鍵。

#include <sys/ipc.h>

//成功返回IPC鍵,失敗返回-1
key_t ftok(const char *pathname, int id);

引數說明:

  • pathname在是程式執行期間必須穩定存在,不能反覆建立與刪除
  • id不能為0,可以是正數或者負數

ipc_perm結構

核心給每個IPC物件維護一個資訊結構,即struct ipc_perm結構,該結構及System V IPC函式經常使用的常值定義在sys/ipc.h標頭檔案中。

struct ipc_perm
{
    uid_t   uid;   //owner's user id
    gid_t   gid;   //owner's group id
    uid_t   cuid;  //creator's group id
    gid_t   cgid;  //creator's group id
    mode_t  mode;  //read-write permissions
    ulong_t seq;   //slot usage sequence number
    key_t   key;   //IPC key
};

建立與開啟IPC物件

建立或開啟一個IPC物件使用相應的xxxget函式,它們都有兩個共同的引數:

  • 引數key,key_t型別的IPC鍵
  • 引數oflag,用於指定IPC物件的讀寫許可權(ipc_perm.mode),並選擇是建立一個新的IPC物件還是開啟一個已存在的IPC物件

對於引數key,應用程式有兩種選擇:

  • 呼叫ftok,給它傳pathname和id
  • 指定key為IPC_PRIVATE,這將保證會建立一個新的、唯一的IPC物件,但該標誌不能用於開啟已存在的IPC物件,只能是新建

對於引數oflag,如上所述,它包含讀寫許可權、建立或開啟這兩方面資訊:

  • 可以指定IPC_CREAT標誌,其含義和Posix IPC的O_CREAT一樣
  • 還可以設定為下表所示的常值來指定讀寫許可權

ipcs和ipcrm命令

由於System V IPC的三種類型不是以檔案系統路徑名標識的,因此無法使用ls和rm命令檢視與刪除它們
ipcs和ipcrm分別用於檢視與刪除系統中的System V IPC
usage : ipcs -asmq -tclup 
    ipcs [-s -m -q] -i id
    ipcs -h for help.
usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
          [-Q msgkey] [-M shmkey] [-S semkey] ... ]

SYSTEM V 訊號量

SystemV訊號量並不如Posix訊號量那樣“好用”,但相比之下它的年代更加久遠,但是SystemV使用的卻更加廣泛(尤其是在老系統中)。

System V訊號量是指的計數訊號量集(set of counting semaphores),是一個或多個訊號量的集合,其中每個都是計數訊號量。(注:System V 訊號量是計數訊號量集,Posix 訊號量是單個計數訊號量。)

所有函式共用標頭檔案

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

建立訊號量

int semget(key_t key,int nsems,int flags)
//返回:成功返回訊號集ID,出錯返回-1
  • (1)第一個引數key是長整型(唯一非零),系統建立IPC通訊 ( 訊息佇列、 訊號量和 共享記憶體) 時必須指定一個ID值。通常情況下,該id值通過ftok函式得到,由核心變成識別符號,要想讓兩個程序看到同一個訊號集,只需設定key值不變就可以。

  • (2)第二個引數nsem指定訊號量集中需要的訊號量數目,它的值幾乎總是1。

  • (3)第三個引數flag是一組標誌,當想要當訊號量不存在時建立一個新的訊號量,可以將flag設定為IPC_CREAT與檔案許可權做按位或操作。
    設定了IPC_CREAT標誌後,即使給出的key是一個已有訊號量的key,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以建立一個新的,唯一的訊號量,如果訊號量已存在,返回一個錯誤。一般我們會還或上一個檔案許可權

刪除和初始化訊號量

int semctl(int semid, int semnum, int cmd, ...);

功能:
訊號量控制操作。
引數:
semid標示操作的訊號量集;semnum標示該訊號量集內的某個成員(0,1等,直到nsems-1),semnum值僅僅用於GETVAL,SETVAL,GETNCNT,GETZCNT,GETPID,通常取值0,也就是第一個訊號量;cmd:指定對單個訊號量的各種操作,IPC_STAT,IPC_GETVAL,IPC_SETVAL,IPC_RMID;arg: 可選引數,取決了第三個引數cmd。
返回值:
若成功,根據cmd不同返回不同的值,IPC_STAT,IPC_SETVAL,IPC_RMID返回0,IPC_GETVAL返回訊號量當前值;出錯返回-1.

如有需要第四個引數一般設定為union semnu arg;定義如下

union semun
{ 
  int val;  //使用的值
  struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的快取區
  unsigned short *arry;  //GETALL,、SETALL 使用的陣列
  struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的快取區
};
  • (1)sem_id是由semget返回的訊號量識別符號
  • (2)semnum當前訊號量集的哪一個訊號量
  • (3)cmd通常是下面兩個值中的其中一個
    SETVAL:用來把訊號量初始化為一個已知的值。p 這個值通過union semun中的val成員設定,其作用是在訊號量第一次使用前對它進行設定。
    IPC_RMID:用於刪除一個已經無需繼續使用的訊號量識別符號,刪除的話就不需要預設引數,只需要三個引數即可。

結構體

由於system v訊號量是伴隨著核心的啟動而生成,我們可以在原始碼檔案sem.c中看到static struct ipc_ids sem_ids;它是system v訊號量的入口,因此在系統執行過程中是一直存在的。它所儲存的資訊是資源(在sem中是訊號量集,也可以是msg,shm)的資訊。如:

   struct ipc_ids {
      int in_use;//說明已分配的資源個數
      int max_id;/在使用的最大的位置索引
      unsigned short seq;//下一個分配的位置序列號
      unsigned short seq_max;//最大位置使用序列
      struct semaphore sem; //保護 ipc_ids的訊號量
      struct ipc_id_ary nullentry;//如果IPC資源無法初始化,則entries欄位指向偽資料結構
      struct ipc_id_ary* entries;//指向資源ipc_id_ary資料結構的指標
    };

它的最後一個元素 entries指向struct ipc_id_ary這樣一個數據結構,它有兩個成員:

 struct ipc_id_ary {
 int size;//儲存的是陣列的長度值
 struct kern_ipc_perm *p[0];//它是個指標陣列 ,陣列長度可變,核心初始化後它的值為128
};

正如我們在上圖看到的,sem_ids.entries->p指向sem_array這個資料結構,為什麼呢?

我們看訊號量集sem_array這個資料結構:

/* One sem_array data structure for each set of semaphores in the system. */
struct sem_array {
   struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
   time_t   sem_otime; /* last semop time */
   time_t   sem_ctime; /* last change time */
   struct sem  *sem_base; /* ptr to first semaphore in array */指向訊號量佇列
   struct sem_queue *sem_pending; /* pending operations to be processed */指向掛起佇列的首部
   struct sem_queue **sem_pending_last; /* last pending operation */指向掛起佇列的尾部
   struct sem_undo  *undo;  /* undo requests on this array */訊號量集上的 取消請求
   unsigned long  sem_nsems; /* no. of semaphores in array */訊號量集中的訊號量的個數
};

這樣sem_ids.entries就跟訊號量集sem_array關聯起來了,但是為什麼要通過kern_ipc_perm關聯呢,為什麼不直接由sem_ids指向sem_array呢,這是因為訊號量,訊息佇列,共享記憶體實現的機制基本差不多,所以他們都是通過ipc_id_ary這個資料結構管理,而通過kern_ipc_perm,他們與各自的資料結構關聯起來。這樣就清楚了!在後面我們來看核心函式sys_semget()是如何進行建立訊號量集,並將其加入到sem_ids.entries中的。

改變訊號量的值

int semop(int semid, struct sembuf *sops, size_t nops);

功能:
操作訊號量,P,V 操作

引數:
semid:訊號量集識別符號;nops是opstr陣列中元素數目,通常取值為1;opstr指向一個結構陣列
nsops:進行操作訊號量的個數,即sops結構變數的個數,需大於或等於1。最常見設定此值等於1,只完成對一個訊號量的操作
sembuf的定義如下:

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

返回值:
成功返回訊號量識別符號,出錯返回-1

一般程式設計步驟:

  1. 建立訊號量或獲得在系統中已存在的訊號量
    1). 呼叫semget().
    2). 不同程序使用同一個訊號量鍵值來獲得同個訊號量
  2. 初始化訊號量
    1).使用semctl()函式的SETVAL操作
    2).當使用二維訊號量時,通常將訊號量初始化為1
  3. 進行訊號量PV操作
    1). 呼叫semop()函式
    2). 實現程序之間的同步和互斥
  4. 如果不需要該訊號量,從系統中刪除
    1).使用semctl()函式的IPC_RMID操作

 

例項

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#define USE_SYSTEMV_SEM 1
#define DELAY_TIME 2
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};
// 將訊號量sem_id設定為init_value
int init_sem(int sem_id,int init_value) {
    union semun sem_union;
    sem_union.val=init_value;
    if (semctl(sem_id,0,SETVAL,sem_union)==-1) {
        perror("Sem init");
        exit(1);
    }
    return 0;
}
// 刪除sem_id訊號量
int del_sem(int sem_id) {
    union semun sem_union;
    if (semctl(sem_id,0,IPC_RMID,sem_union)==-1) {
        perror("Sem delete");
        exit(1);
    }
    return 0;
}
// 對sem_id執行p操作
int sem_p(int sem_id) {
    struct sembuf sem_buf;
    sem_buf.sem_num=0;//訊號量編號
    sem_buf.sem_op=-1;//P操作
    sem_buf.sem_flg=SEM_UNDO;//系統退出前未釋放訊號量,系統自動釋放
    if (semop(sem_id,&sem_buf,1)==-1) {
        perror("Sem P operation");
        exit(1);
    }
    return 0;
}
// 對sem_id執行V操作
int sem_v(int sem_id) {
    struct sembuf sem_buf;
    sem_buf.sem_num=0;
    sem_buf.sem_op=1;//V操作
    sem_buf.sem_flg=SEM_UNDO;
    if (semop(sem_id,&sem_buf,1)==-1) {
        perror("Sem V operation");
        exit(1);
    }
    return 0;
}
int main() {
    pid_t pid;
#if USE_SYSTEMV_SEM
    int sem_id;
    key_t sem_key;
    sem_key=ftok(".",'A');
    printf("sem_key=%x\n",sem_key);
    //以0666且create mode建立一個訊號量,返回給sem_id
    sem_id=semget(sem_key,1,0666|IPC_CREAT);
    printf("sem_id=%x\n",sem_id);
    //將sem_id設為1
    init_sem(sem_id,1);
#endif
    if ((pid=fork())<0) {
        perror("Fork error!\n");
        exit(1);
    } else if (pid==0) {
#if USE_SYSTEMV_SEM
        sem_p(sem_id); //    P操作
#endif
        printf("Child running...\n");
        sleep(DELAY_TIME);
        printf("Child %d,returned value:%d.\n",getpid(),pid);
#if USE_SYSTEMV_SEM
        sem_v(sem_id); //    V操作
#endif
        exit(0);
    } else {
#if USE_SYSTEMV_SEM
        sem_p(sem_id); //    P操作
#endif
        printf("Parent running!\n");
        sleep(DELAY_TIME);
        printf("Parent %d,returned value:%d.\n",getpid(),pid);
#if USE_SYSTEMV_SEM
        sem_v(sem_id); //    V操作
        waitpid(pid,0,0);
        del_sem(sem_id);
#endif
        exit(0);
    }
}

執行結果如下:

獲取更多關於Linux的資料,請關注公眾號「一口Linux」