资源共享是UNIX多用户系统的一个重要特征,信号量(SEMAPHORE)则是防止两个或多个进程同时访问共享资源的一种机制。在信号量机制实现之前,通常采用加锁文件的方法,其算法描述如下:
⑴加锁算法
int lock(lockfile)
/*返回值0代表成功,其它为失败*/
char *lockfile; /*加锁文件名*/
{
intfd,ret=0;
extern int errno;
if((fd=open(lockfile,O_WRONLY|O_CREAT|O_EXCL,0666))==-1
&&errno==EEXIST) ret=1;
return(ret);
}
⑵解锁算法
unlock(lockfile)
char *lockfile; /*锁文件名*/
{
unlink(lockfile);
}
这种方法对访问共享资源次数较少的进程是可行的,但对重载的使用则开销太大了,况且一旦加锁失败则进程不知何时可以再试;当系统崩溃或重启动时,加锁文件可能会被忘掉了。
Dijkstra发表的Dekker算法给出了信号量的一种实现,为整值对象定义了两个了原语操作:P和V。其C描述如下:
void P(sem)
int *sem;
{
while (*sem<=0);
(*sem)--;
}
void V(sem)
int *sem;
{
(*sem)++;
}
但上述算法不能在用户空间编程,因为①sem指向的信号量变量不能在进程间共享,它们有自己的数据段;②函数非原子执行,内核可在任何时候中断一个进程;③若sem为0,进程并不释放CPU。
所以信号量必须由内核提供,它可在进程间共享数据,可执行原子操作(即一组操作要么全部执行,要么都不执行),可在一个进程阻塞时将CPU给另外一个进程。
UNIXSYSTEMV以一个长整数的键值作为信号量集合的唯一标识,信号量通常由下列元素组成:
①信号量的值,
②操作该信号量的最后一个进程的进程标识,
③等待增加该信号量的值的进程数,
④等待该信号量的值为0的进程数。
与之有关的系统调用如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key,count,flags)
/*获取信号量集合的标识符*/
key_tkey; /*信号量集合的键*/
intcount; /*信号量集合中元素个数*/
intflags; /*任选参数*/
/*返回信号量集合标识符,若出错则返回-1*/
int semop(sid,ops,nops) /*信号量操作*/
int sid; /*信号量集合标识符*/
struct sembuf *ops; /*信号量操作结构的指针*/
intnops; /*信号量操作结构的个数*/
/*返回运算完成前该组信号量中最后一个被运算的信号量的
值,若出错则返回-1*/
int semctl(sid,semnum,cmd,arg)
/*控制信号量操作*/
intsid; /*信号量集合标识符*/
intsemnum; /*信号量元素编号*/
intcmd; /*控制命令*/
union semun{
intval;
struct semid_ds *buf;
ushort*array;} arg; /*命令参数*/
系统调用semget用来把信号量集合的键值译成代表信号量集合的标识符,该集合中有count个元素,其存取权限定义与文件相同,由flags定义。若flags的IPC_CREAT位被置位,则当该集合不存在时系统就创建之。因此各进程可都用置IPC_CREAT位的flags参数来获取信号量集合的标识符,不需要由某一进程事先创建。若flags为IPC_PRIDVATE则不管同键值的信号量集合是否存在系统都建立之,并返回下一个可用的标识符。
系统调用semctl在一组信号量上做各种控制操作,诸如信号量集合的初始化、删除和状态查询等。常用的操作及相关的命令格式如下:
①取消信号量集合
int semctl(sid,count,IPC_RMID,0)
int sid; /*信号量集合标识符*/
int count; /*信号量集合中元素个数*/
②设置信号量集合的初值(初始化)
信号量集合刚建立时,各信号量的初值不确定,需要设定初值。初值的设定可用SETALL或SETVAL命令。若用SETALL命令,其格式为:
int semctl(sid,count,SETALL,arg)
int sid; /*信号量集合标识符*/
int count; /*信号量集合中元素个数*/
ushort *arg; /*命令参数*/
该命令把数组arg中的前count个值依次赋给集合中各信号量,一次可设定多个信号量的初值。
若用SETVAL命令,其格式为:
int semctl(sid,semnum,SETVAL,arg)
int sid; /*信号量集合标识符*/
int semnum; /*信号量元素编号*/
int arg; /*命令参数*/
该命令将arg的值赋给集合中第semnum个信号量,一次仅能设定一个信号量的初值。
③查询信号量集合的当前值
查询信号量集合的当前值可用GETALL或GETVAL命令。若用GETALL命令,其格式为:
int semctl(sid,count,GETALL,arg)
int sid; /*信号量集合标识符*/
int count; /*信号量集合中元素个数*/
ushort *arg; /*命令参数*/
该命令把信号量集合中各信号量的当前值返回到数组arg中。
若用GETVAL命令,其格式为:
int semctl(sid,semnum,GETVAL,0)
int sid; /*信号量集合标识符*/
int semnum; /*信号量元素编号*/
该命令把集合中第semnum个信号量的当前值作为调用的返回值。
④查询某个信号量的等待进程数
当一个进程要执行信号量操作时若条件不具备则被阻塞,有关信号量的等待进程数也相应变化。
通过GETNCNT命令可查询等待信号量增值的进程数,其格式如下:
int semctl(sid,semnum,GETNCNT,0)
int sid; /*信号量集合标识符*/
int semnum; /*信号量元素编号*/
该命令把等待第semnum个信号量增值的进程数作为调用的返回值。
通过GETZCNT命令可查询等待信号量值为0的进程数,其格式如下:
int semctl(sid,semnum,GETZCNT,0)
int sid; /*信号量集合标识符*/
int semnum; /*信号量元素编号*/
该命令把等待第semnum个信号量值为0的进程数作为调用的返回值。
至于其它的控制命令,因不常用而不再累述。
系统调用semop用来对信号量集合中的一个或多个信号量进行操作,操作命令由用户提供的操作结构数组来定义,该结构如下:
struct sembuf{
short sem_num; /*信号量在集合中的下标*/
short sem_op; /*操作值*/
short sem_flg; /*操作标志*/
};
系统从用户地址空间读信号量操作结构数组,并核实信号量下标的合法性及进程是否具备读或修改信号量所必需的权限。若权限不够则调用失败;若进程必须睡眠,则它将已操作过的信号量恢复为该系统调用开始时的值,然后它就睡眠,直到它等待的事件发生时再重新执行该系统调用。由于系统将操作数组保存在一个全局数组中,因此若它必须重新执行该调用的话,它必须重新从用户空间读该数组。这样,操作按原语方式执行--或一次做完或根本不做。
系统根据操作值来改变信号量的值:①若操作值为正,系统就增加信号量的值并唤醒所有等待信号量增值的进程;②若操作值是0,系统就检查信号量的值:如果为0,就继续数组中的其它操作;否则把等待信号量的值为0的睡眠进程数加1,然后睡眠;③若操作值为负且其绝对值不超过信号量的值,系统就把操作值(一个负数)加到信号量值上,如果结果为0则系统就唤醒所有等待信号量的值为0的睡眠进程;④若信号量的值小于操作值的绝对值,系统就让进程睡眠在"等待信号量增值"这一事件上。
当进程在信号量操作过程中睡眠时,它睡眠在可中断级上,因此当它接收到软中断信号时就被唤醒了。用户可在操作标志中设置IPC_NOWAIT标志以防止进程睡眠。
如果进程执行了一个信号量操作,锁住了某些资源,却没有恢复信号量的值就退出了(如收到kill信号),那么就可能出现危险情况。为了避免这类问题,用户可在操作标志中设置SEM_UNDO标志。当进程退出时,系统便撤除该进程做过的每个信号量操作的影响。
值得指出的是,当你使用两个或多个信号量时,死锁总是可能的,系统并不能检查多个信号量间的死锁。
本文所用算法及调用格式均已在SCOUNIX3.2、SCOOpenSever3.X及5.X上运行通过。