信號量的無序競爭和有序競爭

来源:https://www.cnblogs.com/englyf/archive/2022/09/01/16645135.html
-Advertisement-
Play Games

以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16645135.html 信號量的無序競爭和有序競爭 在linux的多進程(或者多線程,這裡以進程為例)開發里經常有進程間的通信部分,常見的技術手段有信號量 ...


以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16645135.html


信號量的無序競爭和有序競爭

在linux的多進程(或者多線程,這裡以進程為例)開發里經常有進程間的通信部分,常見的技術手段有信號量、消息隊列、共用記憶體等,而共用記憶體和信號量就像襯衫和外套一樣搭配才算完整。

信號量的使用可以使得對資源的訪問具有排它性,單一時刻只允許同一個進程訪問,而其它的進程統統排隊等候或者取消行程打道回府。

對資源的訪問權既然要有排它性,那麼訪問權的獲得就必然有競爭關係。競爭關係,又會使得結果是有順序的,包括有序和無序。無序就是,競爭是公平的,對資源的訪問權獲取是隨機的。而有序則是,對競爭的結果有刻意的安排,出現固定的順序,比如數據生產消費模型里,數據一般是安排先在生產端輸出,然後才輪到消費端訪問。

好了,扯得太長太陽都快出來了。

信號量的使用庫有System V庫和POXIS庫兩種,這裡僅簡單介紹System V庫和相關API,太詳細會讓人睡著的。

函數原型 備註
int semget(key_t key, int nsems, int semflg) 獲取或者創建一個信號量集的標識符,一個信號量集可以包含有多個信號量,nsems代表信號量數量,key可以通過ftok獲取(也可以直接使用IPC_PRIVATE,但是僅能用於父子進程間通信),semflg代表信號量集的屬性
int semctl(int semid, int semnum, int cmd, union semun arg) 設置或者讀取信號量集的某個信號量的信息,semid代表semget返回值,semnum代表信號量的序號,類型union semun在某些系統中不一定存在(如有需要可以自定義)
int semop(int semid, struct sembuf *sops, unsigned nsops) 執行PV操作,P是對資源的占用,V是對資源的釋放,類型struct sembuf包含了操作的具體內容,nsops代表操作信號量的個數(一般僅用1)
struct sembuf {
    short sem_num;   //指定信號量,信號量在信號量集中的序號,從0開始
    short sem_op;    //小於0,就是執行P操作,對信號量減去sem_op的絕對值;大於0,就是執行V操作,對信號量加上sem_op的絕對值;等於0,等待信號量值歸0
    short sem_flg;   //0,IPC_NOWAIT,SEM_UNDO(方便於調用進程崩潰時對信號量值的自動恢復,防止對資源的無用擠占)
}

下麵介紹一下信號量的兩種使用方式。

信號量的無序競爭

信號量最簡單的使用方式就是無序的競爭方式。比如在獲取資源時,只使用一個信號量,各個進程公平競爭上崗。預設其中一個特定進程啟動後,初始化信號量的值為1(調用semctl實現)。然後當所有進程其中的一個需要搶占資源時,P操作對信號量值減1,信號量值歸0,調用進程搶占資源成功,資源使用完成後,V操作對信號量值加1,信號量值變為1,釋放資源。

當信號量值歸0後,其它進程如果需要搶占資源,對信號量執行P操作會導致調用進程掛起並等待,這是調用進程堵塞了。如果執行P操作時,semop的sem_flg用了IPC_NOWAIT,則直接返回-1,通過errno可以獲取到錯誤代碼EAGAIN。

PV操作就是通過semop函數對信號量的值檢查再加減操作。

老是覺得話太多還不如幾行代碼來得直接明瞭。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>

void P(int sid)
{
    struct sembuf sem_p;
    sem_p.sem_num = 0;
    sem_p.sem_op = -1;
    sem_p.sem_flg = 0;

    if (semop(sid, &sem_p, 1) == -1) {
        perror("p fail");
        exit(-1);
    }
}

void V(int sid)
{
    struct sembuf sem_v;
    sem_v.sem_num = 0;
    sem_v.sem_op = 1;
    sem_v.sem_flg = 0;

    if (semop(sid, &sem_v, 1) == -1) {
        perror("v fail");
        exit(-1);
    }
}

int main(int argc, char *argv[])
{
    int fd = open("semtest", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(-1);
    }

    key_t key = ftok("semtest", 'a');
    if (key == -1) {
        perror("ftok");
        exit(-1);
    }

    int sid = semget(key, 1, IPC_CREAT | 0666);
    if (sid == -1) {
        perror("semget");
        exit(-1);
    }

    if (semctl(sid, 0, SETVAL, 1) == -1) {
        perror("semctl");
        exit(-1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(-1);
    } else if (pid == 0) {
        // child process
        while (1) {
            P(sid);
            printf("child get\n");
            sleep(1);
            printf("child release\n");
            V(sid);
        }
    } else {
        // parent process
        printf("parent pid %d child pid %d\n", getpid(), pid);
        while (1) {
            P(sid);
            printf("parent get\n");
            sleep(1);
            printf("parent release\n");
            V(sid);
        }
    }

    return 0;
}

然後看看結果輸出,第一次可能是這樣子的

parent pid 13156 child pid 13157
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...

第二次可能就是這樣子了

parent pid 12873 child pid 12874
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...

很明顯這就是信號量的無序競爭結果,就像永遠猜不到下一個出現的會是如花姐姐還是白雪公主。

信號量的有序競爭

其實,進程間對資源的使用方式常常是有刻意順序的,比如數據的生產消費模型使用場景。我們去茶樓喝茶,都是要先下好單等廚房的師傅們弄好端出來,我們才下筷吃起來,這裡邊就有既定的順序啦。

那麼怎麼實現信號量的有序操作呢?如果僅僅使用一個信號量,對於各個進程來說,同一個信號量的值,你知我知大家知,大伙處在同一起跑線上,明顯一個信號量是不夠了。那麼可以嘗試使用多個信號量,畢竟人多力量大,大力出奇跡?(玩笑,給個評價____)

假設有兩個進程(A和B)競爭使用同一個資源,使用資源的順序要求先是A,然後B,如此迴圈。每個進程各分配一個代表的信號量(semA/semB)。由於信號量的值預設是0的,那麼可以在最優先的進程(A)中對信號量(semA)的值初始化為1,其它信號量(semB)初始化為0,而在其它進程中不需要再對信號量的值作初始化了。

當進程(A)需要搶占資源時,P操作信號量(semA),信號量(semA)的值歸0,搶占資源成功。進程(A)使用完需要釋放資源時,V操作信號量(semB),信號量(semB)的值變為1,釋放完成。在進程(A)中,資源釋放後,這時如果再次嘗試搶占資源,則P操作信號量(semA),檢查信號量(semA)的值,發現已為0,搶占資源失敗,進程(A)掛起等待資源。

在進程(A)釋放資源後,如果進程(B)嘗試搶占資源,P操作信號量(semB),信號量(semB)的值歸0,搶占資源成功。進程(B)使用完需要釋放資源時,V操作信號量(semA),信號量(semA)的值變為1,釋放完成。如果進程(A)未曾搶占資源並且釋放,這時進程(B)嘗試搶占資源,P操作信號量(semB),檢查信號量(semB)的值,發現已為0,搶占資源失敗,進程(B)掛起等待資源。

這樣就實現了資源總是先給到進程(A),待進程(A)釋放資源後,進程(B)才有資格獲取到。

下麵是代碼,look一look

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>
#include <string.h>
#include <errno.h>

void P(int sid, int index)
{
    struct sembuf sem_p;
    sem_p.sem_num = index;
    sem_p.sem_op = -1;
    sem_p.sem_flg = 0;

    if (semop(sid, &sem_p, 1) == -1) {
        printf("%d p fail: %s", index, strerror(errno));
        exit(-1);
    }
}

void V(int sid, int index)
{
    struct sembuf sem_v;
    sem_v.sem_num = index;
    sem_v.sem_op = 1;
    sem_v.sem_flg = 0;

    if (semop(sid, &sem_v, 1) == -1) {
        printf("%d v fail: %s", index, strerror(errno));
        exit(-1);
    }
}

int main(int argc, char *argv[])
{
    int fd = open("semtest", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(-1);
    }

    key_t key = ftok("semtest", 'a');
    if (key == -1) {
        perror("ftok");
        exit(-1);
    }

    int sid = semget(key, 2, IPC_CREAT | 0666);
    if (sid == -1) {
        perror("semget 2");
        exit(-1);
    }

    if (semctl(sid, 0, SETVAL, 1) == -1) {
        perror("semctl 0");
        exit(-1);
    }

    if (semctl(sid, 1, SETVAL, 0) == -1) {
        perror("semctl 1");
        exit(-1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(-1);
    } else if (pid == 0) {
        // child
        while (1) {
            P(sid, 1);
            printf("child get\n");
            sleep(1);
            printf("child release\n");
            V(sid, 0);
        }
    } else {
        // parent
        printf("parent pid %d child pid %d\n", getpid(), pid);
        while (1) {
            P(sid, 0);
            printf("parent get\n");
            sleep(1);
            printf("parent release\n");
            V(sid, 1);
        }
    }

    return 0;
}

編譯執行,看看輸出。

parent pid 271 child pid 272
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...

無論執行多少遍這程式,發現parent永遠是最先搶占資源的。不信的話,還可以在parent的while迴圈之前加個延時,再看看輸出結果(治好你的小雞咕嚕。。。)。你會發現parent這隻小兔子無論故意睡多久的懶覺,還是會第一個衝出屏幕(不是終點線)。

// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
sleep(10);
while (1) {
    P(sid, 0);
    printf("parent get\n");
    sleep(1);
    printf("parent release\n");
    V(sid, 1);
}

如果把上面信號量初始化的代碼改一改(會不會單車變摩托?想多了。。。)

改成:子進程的代表信號量值初始化為1,父進程的代表信號量初始化為0。

if (semctl(sid, 0, SETVAL, 0) == -1) {
    perror("semctl 0");
    exit(-1);
}

if (semctl(sid, 1, SETVAL, 1) == -1) {
    perror("semctl 1");
    exit(-1);
}

編譯後再執行程式看看輸出,發現最先搶占資源的變成永遠是child了

parent pid 298 child pid 299
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...

好了,介紹到這裡,期待你的一鍵三連(⊙o⊙)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原文連接:https://www.zhoubotong.site/post/78.html 開發中對於http請求是經常遇到,一般可能網路延遲或介面返回超時,對於發起客戶端的請求, 除了設置超時時間外,請求重試是很有必要考慮的,我們不用重覆造輪子,可以使用 https://github.com/ra ...
  • 哈嘍兄弟們 之前經常編寫Python腳本來進行數據處理、數據傳輸和模型訓練。隨著數據量和數據複雜性的增加,運行腳本可能需要一些時間。在等待數據處理完成時可以同時做一些其他工作。 為了達到這個目的,編寫了一組用於解決這個問題的Python腳本。使用這些腳本向手機發送流程更新、可視化和完成通知。當偶爾擁 ...
  • “一個空Object對象的占多大空間?” 一個工作了5年的Java程式員直接被搞蒙了。 大家好,我是Mic,一個工作了14年的Java程式員。 我把這個問題的文字版本整理到了15W字的面試文檔里,大家可以掃描文章尾端領取。 下麵看看高手的回答。 高手: 在開啟了壓縮指針的情況下,Object預設會占 ...
  • 1、名詞理解 切麵(Aspect): 含有前置通知,後置通知,返回通知,異常拋出通知,環繞通知等方法的類; 通知(Advice): 對原方法進行添加處理(如日誌等)的方法; 切入點(PointCute): 通知需要在哪些方法上執行的表達式;(可以唯一匹配或模糊匹配); 連接點(JoinPoint): ...
  • 哈嘍兄弟們! 近年來,Python 宛如一匹黑馬,一騎絕塵,橫掃 TIOBE、Stack Overflow 等榜單,如今在 IEEE Spectrum 發佈的第九屆年度頂級編程語言榜單中,Python 依然是 C、C++、C#、Java 等老牌語言無法比擬的。 關於編程語言的優劣,眾說紛紜。不過,在 ...
  • 來源:juejin.cn/post/7110110794188062727 下午愜意時光,突然產品小姐姐走到我面前,打斷我短暫的摸魚time,企圖與我進行深入交流,還好我早有防備沒有閃,打開瑞star的點單頁面,暗示沒有一杯coffee解決不了的需求,需求是某些介面返回的信息,涉及到敏感數據的必須進 ...
  • 迪傑斯特拉(Dijkstra)演算法是典型最短路徑演算法,用於計算一個節點到其他節點的最短路徑。 它的主要特點是以起始點為中心向外層層擴展(廣度優先遍歷思想),直到擴展到終點為止 貪心演算法(Greedy Algorithm) 貪心演算法,又名貪婪法,是尋找最優解問題的常用方法,這種方法模式一般將求解過程分 ...
  • Alpine Alpine介紹 alpine是一個輕量級的Linux發行版本,輕量級不僅體現在其占用空間的大小,還因為他沒有圖形化界面,只有命令行界面。 這個發行版本與我們常見的發現版本不同,其他版版本基本在安裝完基本配置之後就可以使用了,而且基本配置如:磁碟,時區等都可以通過圖形化的方式去點擊進行 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...