从 0 开始学习 Linux 系列之「24.信号量 semaphore」

信号量

版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

信号量 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 都有具体的执行逻辑,有兴趣可以详细分析。

结语

本次就简单地介绍了信号量的基本操作和内核的实现机制,对与信号量的应用并没有介绍太多,更多的应用方法还需要在实际工作中去实践。建议你将共享内存,消息队列和信号量自己总结对照分析一遍,看看它们的实现机制是不是几乎相同的,这可以加深你对他们的理解,了解些原理总是有些好处的。那我们下次再见,谢谢你的阅读。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容