进程间通信机制之二:信号量

当我们编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户多进程系统上,我们通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权。

信号量有着复杂的编程接口,但幸运的是,我们可以很轻松地为自己提供一个更简单的接口,它足够应付大多数信号量编程的问题。

要想编写通用的代码,以确保程序对某个特定的资源具有独占式的访问权限是非常困难的。虽然有一个名为Dekker算法的解决方法,但这个算法依赖于“忙等待”或“自旋锁”。也就是说,一个进程要持续不断地运行以等待某个内存位置被改变。

信号量的一个更正式的定义是:它是一个特殊变量,只允许对它进行等待(wait)和发送信号(signal)这两种操作。因为在Linux编程中,“等待”和“发送信号”都已具有特殊含义,所以我们将用原先定义的符号来表示这两种操作。

  • P(信号量变量):用于等待。
  • V(信号量变量):用于发送信号。

一.信号量的定义

最简单的信号量是只能取值0和1的变量,即二进制信号量。这也是信号量最常见的一种形式。可以取多个正整数的信号量被称为通用信号量。

P(sv)如果sv的值大于零,就给它减去1,;如果它的值等于零,就挂起该进程的执行
V(sv)如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有进程因等待sv而被挂起,就给它加1

还可以这样看信号量:当临界区域可用时,信号量变量sv的值是true,然后P(sv)操作将它减1使它变为false以表示临界区域正在被使用;当进程离开临界区域时,使用V(sv)操作将它加1,使临界区域再次变为可用。


二.Linux的信号量机制

Linux系统中的信号量接口经过了精心设计,它提供了比通常所需要更多的机制。所有的Linux信号量函数都是针对成组的通用信号量进行操作,而不是只针对一个二进制信号量。

信号量函数的定义如下:

#include <sys/sem.h>

int semctl(int sem_id, int sem_num, int command, ...);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

头文件sys/sem.h通常依赖于另两个头文件sys/types.h和sys/ipc.h。一般情况下,它们都会被sys/sem.h自动包含,因此不需要为它们明确添加相应的#include语句。

在逐个介绍这些函数时,请记住,这些函数都是用来对成组的信号量值进行操作的,这使得,对它们的操作要比单个信号量所需要的操作复杂得多。

参数key的作用很像一个文件名,它代表程序可能要使用的某个资源,如果多个程序使用相同的key值,它将负责协调工作。与此类似,由semget函数返回的并用在其他共享内存函数中的标识符也与fopen返回的FILE*文件流很相似,进程需要通过它来访问共享文件。此外,类似于文件的使用情况,不同的进程可以用不同的信号量标识符来指向同一个信号量。


1.semget函数

semget函数的作用是创建一个新信号量或取得一个已有信号量的键:

int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值,不相关的进程可以通过它访问同一个信号量。程序对所有信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标识符。只有semget函数才直接使用信号量键,所有其他的信号量函数都是使用由semget函数返回的信号量标识符。

有一个特殊的信号量键值IPC_PRIVATE,它的作用是创建一个只有创建者进程才可以访问的信号量,但这个键值很少有实际的用途。在创建新的信号量时,你需要给键提供一个唯一的非零整数。

num_sems参数指定需要的信号量数目。它几乎总是取值为1.

sem_flag参数是一组标志,它与open函数的标志非常相似。它低端的9个比特是该信号量得权限,其作用类似于文件的访问权限。此外,它们还可以和值IPC_CREAT做按位或操作,来创建一个新信号量。即使在设置了IPC_CREAT标志后给出的键是一个已有信号量的键,也不会产生错误。如果函数用不到IPC_CREAT标志,该标志就会被悄悄地忽略掉。我们可以通过联合使用标志IPC_CREAT和IPC_EXCL来确保创建出的是一个新的、唯一的信号量。如果该信号量以存在,它将返回一个错误。

semget函数在成功时返回一个正数(非零)值,它就是其他信号量函数将用到的信号量标识符。如果失败,则返回-1。

2.semop函数

semop函数用于改变信号量的值,定义如下:

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数sem_id是由semget返回的信号量标识符。第二个参数sem_ops是指向一个结构数组的指针,每个数组元素至少包含以下几个成员:

struct sembuf {
    short sem_num;
    short sem_op;
    short sem_flg;
};
第一个成员sem_num是信号量编号,除非你需要使用一组信号量,否则它的取值一般为0.sem_op成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是P操作,它等待信号量变为可用;一个是+1,也就是V操作,它发送信号表示信号量现在已可用。

最后一个成员sem_flg通常被设置为SEM_UNDO。它将使得操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号的情况下终止,操作系统将自动释放该进程持有的信号量。除非你对信号量的行为有特殊的要求,否则应该养成设置sem_flg为SEM_UNDO的好习惯。如果决定使用一个非SEM_UNDO的值,那就一定要注意保持设置的一致性,否则你很可能会搞不清楚内核是否会在进程退出时清理信号量。

sem_op调用的一切动作都是一次性完成的,这是为了避免出现因使用多个信号量而可能发生的竞争现象。sem_op的处理细节可以在手册中找到。

3.semctl函数

semctl函数用来直接控制信号量信息。定义如下:

int semctl(int sem_is, int sem_num, int command, ...);
第一个参数sem_id是由semget返回的信号量标识符。sem_num参数是信号量编号,当需要用到成组的信号量时,就要用到这个参数,它一般取值为0,表示这是第一个也是唯一的一个信号量。command参数是将要采取的动作。如果还有第四个参数,它将会是一个union semun结构,根据X/Open规范的定义,它至少包含以下几个成员:

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
}
semctl函数中的command参数可以设置许多不同的值,但只有下面介绍的两个值最常用。

  • SETVAL:用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置。其作用是在信号量第一次使用之前对它进行设置。
  • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

semctl函数将根据command参数的不同而返回不同的值。对于SETVAL和IPC_RMID,成功时返回0,失败时返回-1。
使用举例:

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

static int set_semvalue(void);/*将semctl调用的command参数设置为SETVAL来初始化信号量*/
static void del_semvalue(void);/*将semctl调用的command设置为IPC_RMID来删除信号量ID*/
static int semaphore_p(void);/*对信号量做减1操作(等待)*/
static int semaphore_v(void);/*对信号量做加1操作(释放)*/

static int sem_id;

int main(int argc, char *argv[])
{
    int i;
    int pause_time;
    char op_char = 'O';
    srand((unsigned int)getpid());
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);/*创建新的信号量*/
    
    if (argc > 1) 
    {
        if (!set_semvalue()) 
       {
           fprintf(stderr, "Failed to initialize semaphore\n");
           exit(EXIT_FAILURE);
       }
       op_char = 'X';
       sleep(2);
    }
    for(i = 0; i < 10; i++) 
    {        
        if (!semaphore_p()) 
            exit(EXIT_FAILURE);
        printf("%c", op_char);
        fflush(stdout);
        
        pause_time = rand() % 3;
        sleep(pause_time);
        printf("%c", op_char);
        fflush(stdout);
        
        if (!semaphore_v()) 
            exit(EXIT_FAILURE);
        pause_time = rand() % 2;
        sleep(pause_time);
     }    
     printf("\n%d - finished\n", getpid());
     if (argc > 1) 
     {    
         sleep(10);
         del_semvalue();
     }

     exit(EXIT_SUCCESS);
}

static int set_semvalue(void)
{
    union semun sem_union;
    sem_union.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) 
        return(0);
    return(1);
}

static void del_semvalue(void)
{
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore\n");
}

static int semaphore_p(void)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1; /* P() */
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) 
    {
        fprintf(stderr, "semaphore_p failed\n");
        return(0);
    }
    return(1);
}

static int semaphore_v(void)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1; /* V() */
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) 
    {
        fprintf(stderr, "semaphore_v failed\n");
        return(0);
    }
    return(1);
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值