1. 程式人生 > >從 0 開始學習 Linux 系列之「24.訊號量 semaphore」

從 0 開始學習 Linux 系列之「24.訊號量 semaphore」

訊號量 semaphore

訊號量(semaphore)與之前介紹的管道,訊息佇列的等 IPC 的思想不同,訊號量是一個計數器,用來為多個程序或執行緒提供對共享資料的訪問。

訊號量的原理

常用的訊號量是二值訊號量,它控制單個共享資源,初始值為 1,操作如下:
1. 測試該訊號量是否可用
2. 若訊號量為 1,則當前程序使用共享資源,並將訊號量減 1(加鎖)
3. 若訊號量為 0,則當前程序不可以使用共享資源並休眠,必須等待訊號量為 1 時程序才能繼續執行(解鎖)

要注意因為是使用訊號量來保護共享資源,所以訊號量本身的操作不能被打斷,即必須是原子操作,因此由核心來實現訊號量。

檢視訊號量

類似訊息佇列和共享記憶體,我們也可以使用 ipcs 命令來檢視當前系統的訊號量資源:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

目前我的系統中沒有訊號量,在後面例子中會使用這個命令來檢視建立的訊號量。

訊號量的基本操作

Linux 核心提供了一套對訊號量的操作,包括獲取,設定,操作訊號量,下面就來學習具體的 API。

1. 獲取訊號量

使用 semget 來建立或獲取一個與 key 有關的訊號量。

#include <sys/types.h>
#include <sys/ipc.h> #include <sys/sem.h> /* * key:返回的 ID 與 key 有關係 * nsems:訊號量的值 * semflg:建立標記 * return:成功返回訊號量 ID,失敗返回 -1,並設定 erron */ int semget(key_t key, int nsems, int semflg);

關於引數的詳細解釋參考 man semget

2. 操作訊號量

使用 semop 可以對一個訊號量加 1 或者減 1:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> /* * semid:訊號量 ID * sops:對訊號量的操作 * nsops:要操作的訊號數量 * return:成功返回 0,失敗返回 -1,並設定 erron */ int semop(int semid, struct sembuf *sops, size_t nsops);

sembuf 表示了對訊號量操作的屬性:

struct sembuf {
    /* 訊號量的個數,除非使用多個訊號量,否則設定為 0 */
    unsigned short sem_num;  

    /* 訊號量的操作,-1 表示 p 操作,1 表示 v 操作 */
    short          sem_op;   

    /* 通常設定為 SEM_UNDO,使得 OS 能夠跟蹤訊號量並在沒有釋放時自動釋放 */
    short          sem_flg;  
};

在進行訊號量的 pv 操作時都是使用這個結構作為引數,詳細解釋參考 man semop

3. 設定訊號量

使用 semctl 可以設定一個訊號量的初始值:

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

/*
 * semid:要設定的訊號量 ID
 * semnum:要設定的訊號量的個數
 * cmd:設定的屬性
 */
int semctl(int semid, int semnum, int cmd, ...);

第 4 個引數的型別是 union semun 結構:

union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
};

在使用訊號量時必須手動定義這個結構,並且在初始化設定訊號量(SETVAL)時需要使用這個引數,詳細解釋可以參考 man semctl

例子:使用訊號量進行程序間的同步

下面來學習一個實際使用訊號量來進行程序間通訊的例子,例子實現的功能是:一個程式的兩個例項同步訪問同一段程式碼,先來看看使用的關鍵的函式。

1. 獲取訊號量

在這個例子中將獲取訊號量包裝成一個函式 sem_get

// 建立或獲取一個訊號量
int sem_get(int sem_key) {
    int sem_id = semget(sem_key, 1, IPC_CREAT | 0666);

    if (sem_id == -1) {
        printf("sem get failed.\n");
        exit(-1);
    } else {
        printf("sem_id = %d\n", sem_id);
        return sem_id;
    }
}

建立或者獲取成功列印訊號量的 id,否則列印錯誤資訊。

2. 初始化訊號量

我們只初始化一個訊號量,並設定 val = 1

// 初始化訊號量
int set_sem(int sem_id) {
    union semun sem_union;  
    sem_union.val = 1;  
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1) {
        fprintf(stderr, "Failed to set sem\n");  
        return 0;  
    }
    return 1;  
}

主要使用了 union semun 作為第 4 個引數,其中 sem_union.val = 1,並且第 3 個引數必須為 SETVAL

3. 刪除訊號量

雖然可以指定 OS 自動釋放訊號量,但這個還是要介紹手動釋放的方法:

// 刪除訊號量  
void del_sem(int sem_id) {  
    union semun sem_union;  
    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)  
        fprintf(stderr, "Failed to delete sem, sem has been del.\n");  
}

第 3 個引數指定 IPC_RMID 來刪除訊號量。

4. 訊號量的 PV 操作

下面的函式將訊號量的 val 減 1,實現了 PV 操作:

// 減 1,加鎖,P 操作
void sem_down(int sem_id) {
    if (-1 == semop(sem_id, &sem_lock, 1))
        fprintf(stderr, "semaphore lock failed.\n");
}

// 加 1,解鎖,V 操作
void sem_up(int sem_id) {
    if (-1 == semop(sem_id, &sem_unlock, 1))
        fprintf(stderr, "semaphore unlock failed.\n");
}

5. main 函式

最後來看看主程式的邏輯,先建立或獲取訊號量,然後在第一次呼叫時初始化,接著執行 PV 操作,最後在第二次呼叫後刪除訊號量:

int main(int argc, char **argv) {
    int sem_id = sem_get(12);

    // 第一次呼叫多加一個引數,第二次呼叫不加引數,僅在第一次呼叫時建立訊號量
    if (argc > 1 && (!set_sem(sem_id))) {
        printf("set sem failed.\n");
        return -1;
    }

    // P 操作
    sem_down(sem_id);
    printf("sem lock...\n");

    printf("do something...\n");
    sleep(10);

    // V 操作
    sem_up(sem_id);
    printf("sem unlock...\n");

    // 第二次呼叫後刪除訊號量
    if (argc == 1)
        del_sem(sem_id);

    return 0;
}

6. 編譯,執行,測試

先編譯:

gcc sem.c -o sem

在第一個終端執行,我們多加一個無用的引數來表示這是第一次執行:

./sem 1

sem_id = 0
sem lock...
do something...
# 10 s 等待
sem unlock...

我們使用 ipcs -s 檢視一下當前系統中的訊號量:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
0x0000000c 0          orange     666        1         

看到使用者 orange 已經成功建立了一個許可權為 666 ,ID 為 0 的訊號量了,再開啟第二個終端,不加額外的引數再執行一次:

./sem

sem_id = 0
# 第一個終端列印完 sem unlock 後
sem lock...
do something...
# 10 s 等待
sem unlock...

因為是第二次執行,所以最後訊號量會被刪除,我們再來看看 ipcs -s 的結果:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

可以看到訊號量被成功刪除了,這個效果親自執行測試後可以理解的更加深刻,這兩個程序是同步訪問 do something 這部分程式碼的,第二個程序會等待第一個程序 unlock 後再執行,建議你[下載程式碼]({{ site.url }}/file/sem/sem.c)實際執行一下。

拓展:訊號量在 Linux 核心中的實現機制

最後,我們再來簡單分析下訊號量在 Linux 核心中的實現機制,瞭解機制可以幫助我們更好的理解和使用訊號量。其實核心中的共享記憶體,訊息佇列和訊號量的實現機制幾乎是相同的,訊號量也是開闢一片記憶體,然後對連結串列進行操作。

1. glibc 訊號量函式分析

int semget (key, nsems, semflg)
key_t key;
int nsems;
int semflg;
{
    return INLINE_SYSCALL (ipc, 5, IPCOP_semget, key, nsems, semflg, NULL);
}

semget 函式直接使用 INLINE_SYSCALL 進行系統呼叫陷入核心,semopsemctl 也是類似,下面來看看核心中的實現。

2. semget 分析

semget 函式為訊號量開闢一片新的記憶體,核心中的呼叫如下,也是使用了 ipc_ops 這個資料結構:

semget

其中回調了 newary 這個函式,它完成訊號量的建立和獲取:

newary

可以看出,整個過程與訊息佇列和共享記憶體幾乎相同。

3. semop 分析

semop 對訊號量進行 PV 操作,其中主要是對 sem_op 進行加 1 或者減 1,大體的過程如下:

semop

4. semctl 分析

semctl 對訊號量進行控制,主要是使用 switch 來判斷當前的命令然後執行相應的操作:

semctl

要注意的是,主要的處理邏輯在 semctl_main 這個函式中,其中每個 cmd 都有具體的執行邏輯,有興趣可以詳細分析。

結語

本次就簡單地介紹了訊號量的基本操作和核心的實現機制,對與訊號量的應用並沒有介紹太多,更多的應用方法還需要在實際工作中去實踐。建議你將共享記憶體,訊息佇列和訊號量自己總結對照分析一遍,看看它們的實現機制是不是幾乎相同的,這可以加深你對他們的理解,瞭解些原理總是有些好處的。那我們下次再見,謝謝你的閱讀。

本文原創釋出於微信公眾號「cdeveloper」,程式設計、職場,人生,關注並回復關鍵字「linux」、「機器學習」等獲取免費學習資料。


相關推薦

0 開始學習 Linux 系列24.訊號 semaphore

訊號量 semaphore 訊號量(semaphore)與之前介紹的管道,訊息佇列的等 IPC 的思想不同,訊號量是一個計數器,用來為多個程序或執行緒提供對共享資料的訪問。 訊號量的原理 常用的訊號量是二值訊號量,它控制單個共享資源,初始值為 1,操作

0 開始學習 Linux 系列25.Posix 執行緒

多執行緒概念 多執行緒技術是應用開發中非常重要的技術之一,幾乎大型的應用軟體都使用這個技術,這次一起來學習下 Linux 中的多執行緒開發基礎(其他的系統中概念也是類似的)。 在 Linux 中,一個簡單的程序可以看成只有一個單執行緒(主執行緒),因為只有一

0 開始學習 Linux 系列27.Socket 程式設計基礎(TCP,UDP)

Socket 介面簡介 Socket 套接字是由 BSD(加州大學伯克利分校軟體研發中心)開發的一套獨立於具體協議的網路程式設計介面,應用程式可以用這個介面進行網路通訊。要注意:Socket 不是一套通訊協議(HTTP,FTP 等是通訊協議),而是程式設計的介

0 開始學習 Linux 系列08.15 個 gdb 除錯基礎命令

gdb 簡介 gdb 是 UNIX 及 UNIX-like 下的除錯工具,在 Linux 下一般都直接在命令列中用 gdb 來除錯程式,相比 Windows 上的整合開發環境 IDE 提供的圖形介面除錯,一開始使用 gdb 除錯可能會讓你感到生無可戀,但是隻要

0開始學習 GitHub 系列05.Git 進階

關於 Git 相信大家看了之前一系列的文章已經初步會使用了, 但是關於Git還有很多知識與技巧是你不知道的,今天就來給大家介紹下一些 Git 進階的知識。 1. 使用者名稱和郵箱 我們知道我們進行的每一次commit都會產生一條log,這條log標記了提交人的姓名與郵箱,以便其他人方便的檢視與聯絡提交

0開始學習 GitHub 系列06.團隊合作利器 Branch

Git 相比於 SVN 最強大的一個地方就在於「分支」,Git 的分支操作簡直不要太方便,而實際專案開發中團隊合作最依賴的莫過於分支了,關於分支前面的系列也提到過,但是本篇會詳細講述什麼是分支、分支的具體操作以及實際專案開發中到底是怎麼依賴分支來進行團隊合作的。 1. 什麼是分支? 我知道讀者中肯定有

Java0開始學習系列路(6)

前言--- 明天週末了,打算用來整理一下資料庫,Cisco命令和Linux,這篇部落格寫完之後就打算滾回宿舍休息了,路上順便買下水果,補充補充維C。 前言補充---- 突然被管教學樓的老師給清出教室了,由於教室明後天要當某證書的考場,這種做事被打擾的感覺實在是難受。不過

HTML5-0開始學習HTML5標籤,屬性,與元素之間的關係

上次我介紹了HTML5的結構,如下圖所示 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewpo

HTML5-0開始學習表單屬性詳解

什麼是表單? 表單是網頁中資料採集的工具。 表單組成部分由三部分組成: (一)表單標籤<form> (二)表單域<input> (三)表單按鈕<button> 現在我們來逐步解釋這三部分。 (一)表單標籤<form>

開始Linux系統Vi/Vim操作

命令模式 nmap 快捷 大小 users vim 修改 忽略大小寫 unmap vi/vim:編輯模式 <-:—-命令模式 —-a、i、o A、I、O —> 插入模式 設置行號 :set nu :set nonu 行號移動: gg G nG/

開始學習算法歸並排序[1](2.2歸並排序)

並排 步驟 blog ++ 序列 else [1] 操作 歸並排序 歸並排序思想為將序列每相鄰兩個數字進行歸並操作(merge),形成floor(n/2)個序列,排序後每個序列包含兩個元素將上述序列再次歸並,形成floor(n/4)個序列,每個序列包含四個元素重復步驟2,直

學習筆記:0開始學習大資料-20. 機器學習spark ml演算法庫應用練習

作為大資料初學者,機器學習演算法的運用,只是hello world知道個123,以後專案需要再深入 Mahout,spark MLlib,spark ML三個演算法庫,根據網上了解比較,採用spark ml演算法庫作為學習物件。 本次學習只是除錯能執行網上的例子 程式碼案例網址: h

學習筆記:0開始學習大資料-19. storm開發及執行環境部署

一.eclipse strom開發環境 1. eclipse waven開發環境支援storm java程式開發很簡單,只要pom.xml 加入依賴即可 <dependency>     <groupId>org.apache.storm</

學習筆記:0開始學習大資料-18.kettle安裝使用

Kettle是一款國外開源的ETL工具,純java編寫,可以在Windows、Linux、Unix上執行,資料抽取高效穩定。 Kettle 中文名稱叫水壺,該專案的主程式設計師MATT 希望把各種資料放到一個壺裡,然後以一種指定的格式流出。 Kettle這個ETL工具集,它允許你管理來自不同資料庫的

學習筆記:0開始學習大資料-17.Redis安裝及使用

Redis 是一個高效能的key-value資料庫。 redis的出現,很大程度補償了memcached這類key/value儲存的不足,在部 分場合可以對關係資料庫起到很好的補充作用。 1. 下載 wget http://download.redis.io/releases/redis-5

學習筆記:0開始學習大資料-16. kafka安裝及使用

kafka是訊息處理服務的開源軟體,高效高可用。可以作為大資料收集的工具或資料的管道。 1. 下載  http://kafka.apache.org/downloads 根據scala版本,我下載的是Scala 2.12  - kafka_2.12-2.1.0.tgz (as

學習筆記:0開始學習大資料-15. Flume安裝及使用

上節測試了spark  程式設計,spark sql ,spark streaming 等都測試可用了,接下來是資料來源的收集,Flume的安裝使用,其實很簡單,但作為完整,也寫個記錄筆記 1.下載  wget http://archive.cloudera.com/cd

學習筆記:0開始學習大資料-14. java spark程式設計實踐

上節搭建好了eclipse spark程式設計環境 在測試執行scala 或java 編寫spark程式 ,在eclipse平臺都可以執行,但打包匯出jar,提交 spark-submit執行,都不能執行,最後確定是版本問題,就是你在eclipse除錯的spark版本需和spark-submit

學習筆記:0開始學習大資料-13. Eclipse+Scala+Maven Spark開發環境配置

上節配置好了spark執行環境,可以通過 spark-shell  在scala語言介面互動執行spark命令 可以參照( https://blog.csdn.net/u010285974/article/details/81840413   Spark-shell執行計算)

學習筆記:0開始學習大資料-12. spark安裝部署

為了教學方便,考慮ALL IN ONE,一臺虛擬機器構建整個實訓環境,因此是偽分散式搭建spark  環境:   hadoop2.6.0-cdh5.15.1   jdk1.8   centos7 64位 1. 安裝scala環境 版本是scala-2.12.7,官網下載