linux程序设计——信号量(第十四章)

58 篇文章 0 订阅
57 篇文章 9 订阅

第14章    信号量,共享内存和消息队列

在本章中,将介绍一组进程间通信的机制,它们最初是由AT&T System V.2版本的UNIX引入.由于这些机制都出现在同一个版本中并且有着相似的编程接口,所以它们又常被称为IPC(Inter-Process Communication,进程间通信)机制,或者被更常见的称为System V IPC.

14.1    信号量

当编写的程序使用了线程时,不管它是运行在多用户系统上,多进程系统上,还是运行在多用户多进程系统上,通常程序中存在着一部分临界代码,必须确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权.
第7章的第一个示例程序用dbm来访问数据库.如果有多个程序试图在同一时间更新这个数据库,数据就可能会遭到破坏.两个不同的程序要求不同的用户向数据库输入数据,这本身并没有错,问题只可能出现在对数据库进行更新的那部分代码上.这部分真正执行数据更新的代码需要独占式地执行,它们被称为临界区域.它们通常只在一个大型程序终止占据一小段的代码.
为了防止出现因多个程序同时访问一个共享资源而引起的问题,需要有一种方法,它们可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域.在第12章简单介绍了一些线程特定的方法,可以在使用线程的程序中通过互斥量或信号量来控制对临界区域的访问.在本章中,又回到信号量的主题上,但将对它们如何在不同的进程之间使用做更具普遍意义地介绍.
要想编写通用的代码,以确保程序对某个特定的资源具有独占式的访问权是非常困难的.虽然有一个名为Dekker算法的解决办法,但这个算法依赖于"忙等待"或"自旋锁".也就是说,一个进程要持续不断地运行以等待某个内存位置被改变.在像linux这样的多任务环境中,人们并不愿意使用这种浪费CPU资源的处理办法.但如果硬件支持独占式访问,那么情况就变得简单多了.一个硬件支持的例子就是,用一条指令以原子方式访问并增加寄存器的值,在这个读取/增加/写入操作执行的过程中不会有其他指令(甚至一个中断)发生.
前面见过的一种可能的解决办法是,使用带O_EXCL标志的open函数来创建锁文件,它提供了原子化的文件创建方法,它允许一个进程通过获取一个令牌(即新创建的文件)来取得成功,这个方法比较适合于处理简单的问题,但对于更复杂的例子,它就显得比较杂乱且缺乏效率.
荷兰计算机科学家Dijkstra提出的信号量概念是在并发编程领域迈出的重要的一步.信号量定义:它是一个特殊变量,只允许对它进行等待(wait)和发送信号(signal)这两种操作.
P(信号量变量):用于等待
V(信号量变量):用于发送信号

这两个字母分别来自于荷兰语单词 passeren(传递,就好像位于进入临界区域之前的检查点)和vrijgeven(给予或释放,就好像放弃对临界区域的控制权).
14.1.1    信号量的定义
最简单的信号量是只能取0和1的变量,即二进制信号量.可以取多个正整数的信号量被称为通用信号量.
PV操作的定义非常简单,假设有一个信号量变量sv,则这两个操作的定义如下表所示:
P(sv)    如果sv的值大于0,就给它减去1;如果它的值等于0,就挂起该进程的执行
V(sv)    如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有进程因等待sv而被挂起,就给它加1

还可以这样看信号量:当临界区域可用时,信号量变量sv的值是true,然后P(sv)操作将它减1使它变为false以表示临界区域正在被使用;当进程离开临界区域时,使用V(sv)操作将它加1,使临界区域再次变为可用.注意,只用一个普通变量进行类似的加减法是不行的,因为在C,C++,C#或几乎任何一个传统的变成语言中,都没有一个原子操作可以满足检测变量是否为true,如果是再将该变量设置为false的需要,这也是信号量操作如此特殊的原因.

14.1.2    一个理论性的例子

下面用一个简单的理论性的例子来说明其工作原理,假设有两个进程proc1和proc2,这两个进程都需要在其执行过程中的某一时刻对一个数据库进行独占式的访问.定义一个二进制信号量sv,该变量的初始值为1,两个进程都可以访问它,要想对代码中的临界区域进行访问,这两个进程都需要执行相同的处理步骤,事实上, 这两个进程可以只是同一个程序的两个不同执行实例.
两个进程共享信号量变量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区域,而第二个进程将被阻止进入临界区域,因为它试图执行P(sv)操作时,它会被挂起以等待第一个进程立刻临界区域并执行V(sv)操作释放信号量.
需要的伪代码对两个进程都是相同的,如下所示:
semaphore sv = 1;
loop forever (
    P(sv);
    critical code section;
    V(sv);
    noncritical code section;
}

14.1.3    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_flags是一组标志,它与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 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的好习惯.
函数返回值:
semop函数如果成功时则返回0;如果失败则返回-1.
附加:
semop调用的一切动作都是一次性完成的,这是为了避免出现因使用多个信号量而可能发生的竞争现象.
3.semctl函数
函数作用:
semctl函数用于直接控制信号量的值
函数原型:
int semctl(int sem_id, int sem_num, int command, ...);
函数参数:
第一个参数sem_id是由semget返回的信号量标识符
第二个参数sem_num是信号量编号,当需要用到成组的信号量时,就需要用到这个参数,它一般取值为0,表示这是第一个也是唯一的一个信号量
第三个参数command是将要采取的动作.其中两个最常用的值是SETVAL和IPC_RMID
SETVAL:用来把信号量初始化为一个已知的值.这个值通过union semun中的val成员设置.其作用是在信号量第一次使用之前对它进行设置
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符
第四个参数(如果有)将会是一个union semun结构,它至少包含以下几个成员:
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};
函数返回值
semctl函数将根据command参数的不同而返回不同的值.
对于SETVAL和IPC_RMID,如果成功则返回0,如果失败则返回-1.
             
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值