Linux信號(hào)量

一、引言

信號(hào)量是包含一個(gè)非負(fù)整數(shù)型的變量,并且?guī)в袃蓚€(gè)原子操作wait和signal。Wait還可以被稱為down、P或lock,signal還可以被稱為up、V、unlock或post。在UNIX的API中(POSIX標(biāo)準(zhǔn))用的是wait和post。

對(duì)于wait操作,如果信號(hào)量的非負(fù)整形變量S大于0,wait就將其減1,如果S等于0,wait就將調(diào)用線程阻塞;對(duì)于post操作,如果有線程在信號(hào)量上阻塞(此時(shí)S等于0),post就會(huì)解除對(duì)某個(gè)等待線程的阻塞,使其從wait中返回,如果沒(méi)有線程阻塞在信號(hào)量上,post就將S加1.

由此可見(jiàn),S可以被理解為一種資源的數(shù)量,信號(hào)量即是通過(guò)控制這種資源的分配來(lái)實(shí)現(xiàn)互斥和同步的。如果把S設(shè)為1,那么信號(hào)量即可使多線程并發(fā)運(yùn)行。另外,信號(hào)量不僅允許使用者申請(qǐng)和釋放資源,而且還允許使用者創(chuàng)造資源,這就賦予了信號(hào)量實(shí)現(xiàn)同步的功能??梢?jiàn)信號(hào)量的功能要比互斥量豐富許多。

POSIX信號(hào)量是一個(gè)sem_t類型的變量,但POSIX有兩種信號(hào)量的實(shí)現(xiàn)機(jī)制:無(wú)名信號(hào)量命名信號(hào)量。無(wú)名信號(hào)量只可以在共享內(nèi)存的情況下,比如實(shí)現(xiàn)進(jìn)程中各個(gè)線程之間的互斥和同步,因此無(wú)名信號(hào)量也被稱作基于內(nèi)存的信號(hào)量;命名信號(hào)量通常用于不共享內(nèi)存的情況下,比如進(jìn)程間通信。

同時(shí),在創(chuàng)建信號(hào)量時(shí),根據(jù)信號(hào)量取值的不同,POSIX信號(hào)量還可以分為:

  • 二值信號(hào)量:信號(hào)量的值只有0和1,這和互斥量很類似,若資源被鎖住,信號(hào)量的值為0,若資源可用,則信號(hào)量的值為1;
  • 計(jì)數(shù)信號(hào)量:信號(hào)量的值在0到一個(gè)大于1的限制值之間,該計(jì)數(shù)表示可用的資源的個(gè)數(shù)。

下面是POSIX信號(hào)量函數(shù)接口:


image

二、無(wú)名信號(hào)量

2.1 接口函數(shù)

信號(hào)量的函數(shù)都以sem_開(kāi)頭,線程中使用的基本信號(hào)函數(shù)有4個(gè),他們都聲明在頭文件semaphore.h中,該頭文件定義了用于信號(hào)量操作的sem_t類型:

【sem_init函數(shù)】:

該函數(shù)用于創(chuàng)建信號(hào)量,原型如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

該函數(shù)初始化由sem指向的信號(hào)對(duì)象,設(shè)置它的共享選項(xiàng),并給它一個(gè)初始的整數(shù)值。pshared控制信號(hào)量的類型,如果其值為0,就表示信號(hào)量是當(dāng)前進(jìn)程的局部信號(hào)量,否則信號(hào)量就可以在多個(gè)進(jìn)程間共享,value為sem的初始值。

該函數(shù)調(diào)用成功返回0,失敗返回-1。

【sem_destroy函數(shù)】:

該函數(shù)用于對(duì)用完的信號(hào)量進(jìn)行清理,其原型如下:

int sem_destroy(sem_t *sem);

成功返回0,失敗返回-1。

【sem_wait函數(shù)】:

該函數(shù)用于以原子操作的方式將信號(hào)量的值減1。原子操作就是,如果兩個(gè)線程企圖同時(shí)給一個(gè)信號(hào)量加1或減1,它們之間不會(huì)互相干擾。其原型如下:

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

sem指向的對(duì)象是sem_init調(diào)用初始化的信號(hào)量。調(diào)用成功返回0,失敗返回-1。

sem_trywait()則是sem_wait()的非阻塞版本,當(dāng)條件不滿足時(shí)(信號(hào)量為0時(shí)),該函數(shù)直接返回EAGAIN錯(cuò)誤而不會(huì)阻塞等待。

sem_timedwait()功能與sem_wait()類似,只是在指定的abs_timeout時(shí)間內(nèi)等待,超過(guò)時(shí)間則直接返回ETIMEDOUT錯(cuò)誤。

【sem_post函數(shù)】:

該函數(shù)用于以原子操作的方式將信號(hào)量的值加1,其原型如下:

int sem_post(sem_t *sem);

與sem_wait一樣,sem指向的對(duì)象是由sem_init調(diào)用初始化的信號(hào)量。調(diào)用成功時(shí)返回0,失敗返回-1。

【sem_getvalue函數(shù)】:

該函數(shù)返回當(dāng)前信號(hào)量的值,通過(guò)restrict輸出參數(shù)返回。如果當(dāng)前信號(hào)量已經(jīng)上鎖(即同步對(duì)象不可用),那么返回值為0,或?yàn)樨?fù)數(shù),其絕對(duì)值就是等待該信號(hào)量解鎖的線程數(shù)。

int sem_getvalue(sem_t *restrict, int *restrict);

2.2 使用實(shí)例

【實(shí)例1】:

#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <signal.h>
#include <semaphore.h>
    
sem_t sem;
    
#define handle_error(msg)   do { \
                                perror(msg); \
                                exit(EXIT_FAILURE); \
                            }while (0)
    
static void handler(int sig)
{
    write(STDOUT_FILENO, "sem_post() from handler\n", 24);
    if(sem_post(&sem) == -1)
    {
        write(STDERR_FILENO, "sem_post() failed\n", 18);
        _exit(EXIT_FAILURE);
    }
}
    
int main(int argc, char *argv[])
{
    int s;
    struct timespec ts;
    struct sigaction sa;
    
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <alarm-secs> <wait-secs>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if (sem_init(&sem, 0, 0) == -1)
        handle_error("sem_init");
    
    /* Establish SIGALRM handler; set alarm timer using argv[1] */
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGALRM, &sa, NULL) == -1)
        handle_error("sigaction");
    
    alarm(atoi(argv[1]));
    
    /* Calculate relative interval as current time plus
      number of seconds given argv[2] */
    
    if (clock_gettime(CLOCK_REALTIME, &ts) == -1)
        handle_error("clock_gettime");
    
    ts.tv_sec += atoi(argv[2]);
    
    printf("main() about to call sem_timedwait()\n");
    while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
        continue;       /* Restart if interrupted by handler */
    
    /* Check what happened */
    if (s == -1)
    {
        if (errno == ETIMEDOUT)
            printf("sem_timedwait() timed out\n");
        else
            perror("sem_timedwait");
    }
    else
    {
        printf("sem_timedwait() succeeded\n");
    }
    
    exit((s == 0) ? EXIT_SUCCESS : EXIT_FAILURE);
}

【實(shí)例2】:

#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <signal.h>
#include <semaphore.h>
    
sem_t sem;
    
void *func1(void *arg)
{
    sem_wait(&sem);
    int *running = (int *)arg;
    printf("thread func1 running : %d\n", *running);
    
    pthread_exit(NULL);
}
    
void *func2(void *arg)
{
    printf("thread func2 running.\n");
    sem_post(&sem);
    
    pthread_exit(NULL);
}
    
int main(void)
{
    int a = 3;
    sem_init(&sem, 0, 0);
    pthread_t thread_id[2];
    
    pthread_create(&thread_id[0], NULL, func1, (void *)&a);
    printf("main thread running.\n");
    sleep(10);
    pthread_create(&thread_id[1], NULL, func2, (void *)&a);
    printf("main thread still running.\n");
    pthread_join(thread_id[0], NULL);
    pthread_join(thread_id[1], NULL);
    sem_destroy(&sem);
    
    return 0;
}

三、命名信號(hào)量

之所以稱為命名信號(hào)量,是因?yàn)樗幸粋€(gè)名字、一個(gè)用戶ID、一個(gè)組ID和權(quán)限。這些是提供給不共享內(nèi)存的那些進(jìn)程使用命名信號(hào)量的接口。命名信號(hào)量的名字是一個(gè)遵守路徑名構(gòu)造規(guī)則的字符串。

3.1 接口函數(shù)

【sem_open函數(shù)】:

該函數(shù)用于創(chuàng)建或打開(kāi)一個(gè)命名信號(hào)量,其原型如下:

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
                mode_t mode, unsigned int value);

參數(shù)name是一個(gè)標(biāo)識(shí)信號(hào)量的字符串。參數(shù)oflag用來(lái)確定是創(chuàng)建信號(hào)量還是連接已有的信號(hào)量。

oflag的參數(shù)可以為0,O_CREAT或O_EXCL:如果為0,表示打開(kāi)一個(gè)已存在的信號(hào)量;如果為O_CREAT,表示如果信號(hào)量不存在就創(chuàng)建一個(gè)信號(hào)量,如果存在則打開(kāi)被返回,此時(shí)mode和value都需要指定;如果為O_CREAT|O_EXCL,表示如果信號(hào)量存在則返回錯(cuò)誤。

mode參數(shù)用于創(chuàng)建信號(hào)量時(shí)指定信號(hào)量的權(quán)限位,和open函數(shù)一樣,包括:S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。

value表示創(chuàng)建信號(hào)量時(shí),信號(hào)量的初始值。

【sem_close函數(shù)】:

該函數(shù)用于關(guān)閉命名信號(hào)量:

int sem_close(sem_t *);

單個(gè)程序可以用sem_close函數(shù)關(guān)閉命名信號(hào)量,但是這樣做并不能將信號(hào)量從系統(tǒng)中刪除,因?yàn)槊盘?hào)量在單個(gè)程序執(zhí)行之外是具有持久性的。當(dāng)進(jìn)程調(diào)用_exit、exit、exec或從main返回時(shí),進(jìn)程打開(kāi)的命名信號(hào)量同樣會(huì)被關(guān)閉。

【sem_unlink函數(shù)】:

sem_unlink函數(shù)用于在所有進(jìn)程關(guān)閉了命名信號(hào)量之后,將信號(hào)量從系統(tǒng)中刪除:

int sem_unlink(const char *name);

【信號(hào)量操作函數(shù)】:

與無(wú)名信號(hào)量一樣,操作信號(hào)量的函數(shù)如下:

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *restrict, int *restrict);

3.2 使用實(shí)例

#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <signal.h>
#include <semaphore.h>
    
#define SEM_NAME " /sem_name"
    
sem_t *p_sem;
    
void *testThread(void *ptr)
{
    sem_wait(p_sem);
    sleep(2);
    pthread_exit(NULL);
}
    
int main(void)
{
    int i = 0;
    pthread_t pid;
    int sem_val = 0;
    p_sem = sem_open(SEM_NAME, O_CREAT, 0555, 5);
    
    if(p_sem == NULL)
    {
        printf("sem_open %s failed!\n", SEM_NAME);
        sem_unlink(SEM_NAME);
        return -1;
    }
    
    for(i = 0; i < 7; i++)
    {
        pthread_create(&pid, NULL, testThread, NULL);
        sleep(1);
        // pthread_join(pid, NULL);  // not needed, or loop
        sem_getvalue(p_sem, &sem_val);
        printf("semaphore value : %d\n", sem_val);
    }
    
    sem_close(p_sem);
    sem_unlink(SEM_NAME);
    
    return 0;
}

四、命名和無(wú)名信號(hào)量的持續(xù)性

命名信號(hào)量是隨內(nèi)核持續(xù)的。當(dāng)命名信號(hào)量創(chuàng)建后,即使當(dāng)前沒(méi)有進(jìn)程打開(kāi)某個(gè)信號(hào)量,它的值依然保持,直到內(nèi)核重新自舉或調(diào)用sem_unlink()刪除該信號(hào)量。

無(wú)名信號(hào)量的持續(xù)性要根據(jù)信號(hào)量在內(nèi)存中的位置確定:

  • 如果無(wú)名信號(hào)量是在單個(gè)進(jìn)程內(nèi)部的數(shù)據(jù)空間中,即信號(hào)量只能在進(jìn)程內(nèi)部的各個(gè)線程間共享,那么信號(hào)量是隨進(jìn)程的持續(xù)性,當(dāng)進(jìn)程終止時(shí)他也就消失了;
  • 如果無(wú)名信號(hào)量位于不同進(jìn)程的共享內(nèi)存區(qū),因此只要該共享內(nèi)存區(qū)仍然存在,該信號(hào)量就會(huì)一直存在;所以此時(shí)無(wú)名信號(hào)量是隨內(nèi)核的持續(xù)性。

五、信號(hào)量-互斥量-條件變量

很多時(shí)候信號(hào)量、互斥量和條件變量都可以在某種應(yīng)用中使用,那這三者的差異有哪些呢?下面列出了這三者之間的差異:

  • 互斥量必須由給它上鎖的線程解鎖;而信號(hào)量不需要由等待它的線程進(jìn)行掛出,可以在其他進(jìn)程進(jìn)行掛出操作;
  • 互斥量要么被鎖住,要么被解開(kāi),只有這兩種狀態(tài);而信號(hào)量的值可以支持多個(gè)進(jìn)程/線程成功的進(jìn)行wait操作;
  • 信號(hào)量的掛出操作總是被記住,因?yàn)樾盘?hào)量有一個(gè)計(jì)數(shù)值,掛出操作總會(huì)將該計(jì)數(shù)值加1,然而當(dāng)條件變量發(fā)送一個(gè)信號(hào)時(shí),如果沒(méi)有線程等待在條件變量,那么該信號(hào)就會(huì)丟失。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容