System V信号量不是用来在进程间传输数据的,相反,他们是用来同步进程的动作,信号量的一个常见用途时同步对一块共享内存的访问以防止出现一个进程在访问共享内存的同时另一个进程更新这块内存。
一个信号量是一个由内核维护的整数,其值被限制为大于或等于0;在一个信号量上可以执行各种操作(即系统调用),包括:
- 将信号量设置为一个绝对值,
- 在信号量的当前值的基础上加上一个数量
- 在信号量当前值的基础上减去一个数量
- 等待信号量的值等于0
上面操作中的后两个可能会导致调用进程阻塞,当减少一个信号量的值时,内核会将所有试图将信号量值降到0之下的操作阻塞。类似的如果当前信号量值不为0,那么等待信号量等于0的调用进程就会发生阻塞。不管是何种情况,调用进程会一直保持阻塞直到其他一些进程将信号量的值修改为一个允许这些操作继续向前的值,在那个时刻内核会唤醒被阻塞的进程。下图显示了使用一个信号量来同步两个交替将信号量的值在0和1之间切换的进程的动作
在控制进程的动作方面,信号量本身没有任何意义,他的意义仅由使用信号量的进程赋予其的关联关系来确定,一般来讲,进程之间会达成协议将一个信号量与一种共享资源关联起来
47.1概述
- 使用System V信号量的常规步骤如下。
- 使用semget()创建或打开信号量集。
- 使用semctl()SETVAL或SETALL操作初始化集合中的信号量。(只有一个进程需要完成这个任务)
- 使用semop()操作信号量值。使用信号量的进程通常会使用这些操作来表示i中共享资源的获取和释放
- 当所有进程都不再需要使用信号量集之后使用semctl()IPC_RMID操作删除这个集合(只有一个进程需要完成这个任务)
大多数操作系统都为应用程序提供了一些信号量原语。但是System V信号量表现出了不同寻常的复杂性,因为他们的分配是以被称为信号量集的组为单位进行的。在使用semget()系统调用创建集合的时候需要指定集合中的信号量数量。虽然在同一时刻通常只操作一个信号量,但通过semop()系统调用可以原子地在同一个集合中的多个信号量之上执行一组操作。
由于System V信号量的创建和初始化是在不同的步骤之后完成的,因此当两个进程都试图执行这两个步骤时就会出现竞争条件。要描述清楚这种竞争条件以及如何避免出现这种情况需要先对semctl()进行介绍,然后再对semop进行介绍.
#include <sys/types>
#include<sys/sem.h>
#include <sys/stat.h>
union semun { /* Used in calls to semctl() */
int val;
struct semid_ds * buf;
unsigned short * array;
#if defined(__linux__)
struct seminfo * __buf;
#endif
};
char *
currTime(const char *format)
{
static char buf[BUF_SIZE]; /* Nonreentrant */
time_t t;
size_t s;
struct tm *tm;
t = time(NULL);
tm = localtime(&t);
if (tm == NULL)
return NULL;
s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);
return (s == 0) ? NULL : buf;
}
int main(int argc,char *argv[])
{
int semid;
if(argc<2 || argv >3 || strcmp(argv[1],"-help") ==0)
{
printf("%s init-value or:%s semid operation\n",argv[0],argv[1]);
return -1;
}
if(argc ==2){
union semun arg;
semid = semget(IPC_PRIVATE,1,S_IRUSR | S_IWUSR);
if(semid == -1)
perror("semid");
arg.val = atoi(argv[1]);
if(semctl(semid,0,SETVAL,arg) == -1)
{
printf("Semphore ID = %d\n",semid);
}
}else{ //perform an operation on firrst semaphore
struct smbuf sop; //structure defining operation
semid = atoi(arv[1]);
sop.sem_num = 0;
sop.sem_op = atoi(argv[2]);
printf("%ld:about to semop at %s\n",(long)getpid);
if(semop(semid,&sop,1) == -1)
perror("semop:");
}
return 0;
}
$./sem 0
sempehore ID = 0;
//然后执行一个后台命令将信号量减去2
$./sem 0 -2 &
//这个命令会阻塞因为无法将信号量的值捡到小于0 加下来执行一个信号量加上3
$ ./sem 0 +3
47.2创建或打开一个信号量集
semget() 系统调用创建一个新信号量集获取一个既有集合的标识符。
#include <sys/types.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);
return semaphore set identifier on sussess or -1 on error
如果使用semget()创建一个新信号量集,那么nsems会指定集合中信号量的数量,并且其值必须大于0,,如果使用semget()来获取一个既有集合的标识符,那么nsems必须要小于或等于集合的大小(否则会发生EINVAL错误)。无法修改一个既有集中的信号量数量。
semget()系统调用在成功时会返回新信号量集或者既有信号量集的标识符。后续引用单个信号量的系统调用必须要同时指定信号量集标识符和信号量在集合中的序号。一个集合中的信号量从0开始技术,
47.3 信号量控制操作
semctl()系统调用在一个信号量或集合中的的单个信号量上执行各种控制操作。
#include <sys/types.h>
#include<sys/sem.h>
int semctl(int semid,int sennum, int cmd,.../*union semnu arg*/)
returns nonnegative integer on success(see text);return -1 on error
semid 参数时操作所施加的信号量集的标识符。对于那些在单个信号量上执行的操作semnum参数标识出了集合中的具体信号量对于其他操作则会忽略该参数
一些特定的操作需要传入第四个参数,在本节余下的部分中将这个参数命名为arg,这个参数是一个union,在程序中必须显示定义这个union.
虽然将semnu union 的定义放入标准头文件中是比较明智的做法,但是SUSv3要求程序员显示的定义这个union
//semnu union 定义 semnu.h
#ifndef SEMNU_H
#define SEMNU_H
#include <sys/type.h>
#include <sys/sem.h>
union semnu{
int val;
struct semid_ds *buf;
unsigned short *array;
#if defined(__linux__)
struct seminfo *buf;
#endif
};
#endif
SUSv2和SUSv3 规定semctl的最后一个参数是可选的饿,但一些(主要是较早之前的)UNIX实现(以及glibc的较早版本)将semctl的原型定义如下
int semctl(int semid,int semnum,int cmd,union semnu arg);
这就意味着第四个参数是必需的,即使在那些不需要用到这个参数的情况下也是如此(如IPC_RMID/GETVAL操作)。为使程序能够完全移植,在那些无需最后一个参数的semctl()调用中需要传入一个哑参数。
常规控制操作
下面的操作与可应用于其他类型的System VIPC对象上的操作是一样的。所有这些操作都会忽略semnum参数
- IPC_RMID:立即删除信号量集及其相关联的semid_ds数据结构,所有因在semop()调用中等待这个集合中的信号量而阻塞的进程会立即被唤醒。semop()或报告错误EIDRM
- IPS_STAT:在arg.buf指向的缓冲器中放置一份与这个信号量相关联的semid_ds数据结构的副本。
- IPC_SET:使用arg.buf指向的缓冲器中的值来更新与这个信号量集关联的smeid_ds数据结构中选中的字段。
获取和初始化信号量值
下面的操作可以获取或初始化一个集合中单个或所有信号量的值。==获取一个信号量的值需具备在信号量省的饿读权限
- GETVAL:semctl()返回由semid指定的信号量集中第semnum个信号量的值,这个操作无需arg参数
- SETVAL:semctl()将由semid指定的信号量集中第semnum个信号量值初始化为arg.val;
- GETALL:获取由semid指向的信号量集中所有信号量的值并将它们放在arg.array中,程序员必须要确保该数组具备足够的空间(通过IPC_STAT操作返回的semid_ds数据结构中的sem_nsmes字段可以获取集合中的信号量数量。)这个操作忽略semnum参数
- SETALL:使用arg.array指向的数组中的值初始化semdid指向的集合中的所有信号量,这个操作将忽略semnum参数
如过存在一个进程正在等待由STVAL或SETALL操作所修改的信号量上执行一个操作并且对信号量所作的变更将允许该操作继续向前执行,那么内核就会唤醒该进程。
== 注意SETVAL和GETALL返回的信息在调用进程使用他们时可能已经发过期了==。
获取单的信号量的信息
下面这些操作返回(通过函数结果值,)semid引用的集合中第semum个信号量的信息。所有的这些操作都需要在信号量集合中具备读权限。
- GETPID:返回上一个在该信号量上执行semop()的进程ID;这个值被称为semid值。如果还没有进程在该信号量上执行过semop,那么就返回0.
- GETNCNT:返回当前等待该信号量的值增长的进程数,这个值被称为semncnt;
- GETZCNT:返回当前等待该信号量变成0的进程数,这个值被称为semzcnt值,与上面介绍的GETVAL和GETALL操作一样,GETPID,GETNCNT,GETZCNT操作返回的信息在调用进程使用他们时可能已经过期了(不知道什么场景下会使用可能过期的值)
47.4 信号量关联数据结构
每个信号量集都有一个关联的semid_dss数据结构,清醒时如下:
struct semid_ds{
struct ipcperm sem_perm; //ownship and permissions
time_t sem_otime; //time of last semop()
time_t sem_ctime; //time of last changes
unsigned lone sem_nsems; //Number of semaphores;
};
各种信号量系统调用会隐式的更新semid_ds结构中的字段,使用semctl() IPC_SET操作能够显式的更新sem_perm中的特定子字段。
- sem_perm: 在创建信号量时,按照45.3节中所描述的那样初始化这个字段,通过IPC_SET来更新uid,gid以及mode字段
- sem_otime:在创建信号量集时会将这个字段设置为0,然后每次成功的semop()调用或者当信号量值因SEM_UNDO操作而发生变更时将这个字段设置为当前时间(见47.8节),这个字段和sem_ctime的类型为time_t,他们存储自新纪元到现在的秒数。
- sem_ctime:再创建信号量时,以及每个成功的IPC_SET,SETVAL.SETALL操作完成之后将这个字段设置为当前时间(在一些UNIX实现上 SETALL,SETVAL操作不会修改sem_ctime)
- sem_nsems:再创建集合时这个字段的值将初始化为集合中信号量的数量。
一个信号量的监控程序 -------sem_mon.c
#include <sys/types.h>
#include <sys/sem.h>
#include <time.h>
union semun { /* Used in calls to semctl() */
int val;
struct semid_ds * buf;
unsigned short * array;
#if defined(__linux__)
struct seminfo * __buf;
#endif
};
int main(int argc,char *argv[])
{
struct semid_ds ds;
union semnu arg,dummy;
int semid = 0;
int j = 0;
if(argc !=2 || strcmp(argv[1],"--help")==0)
printf("%s semid\n",argv[0]);
semid = atoi(argv[1]);
arg.buf = &ds;
if(semctl(semid,0,IPC_STAT,arg) == -1)
perror("semctl:");
printf("Semphore changed:%s",ctime($ds.ctime));
printf("last semop(): %s",ctime(&ds.ctime));
/*Display per-semaphore information*/
arg.array = calloc(ds.sem_nsems,sizeof(arg.array[0]));
if(arg.array ==NULL)
{
perror("calloc:");
}
if(semctl(semid,0,GETALL,arg) == -1)
{
perror("semctl-GETALL:");
}
printf("Sem # Value SEMPID SEMNCNT SEMZCNT\n");
for(j = 0;j<ds.sem_nsems;j++)
{
printf("%3d %5d %5d %5d %5d\n",j,arg.array[j],semctl(semid,j,GETPID,dummy),
semctl(semid,j,GETNCNT,dummy),semctl(semid,j,GETZCNT,dummy),);
}
return 0;
}
初始化一个集合中的所有信号量
#include <sys/types.h>
#include <sys/sem.h>
union semun { /* Used in calls to semctl() */
int val;
struct semid_ds * buf;
unsigned short * array;
#if defined(__linux__)
struct seminfo * __buf;
#endif
};
int main(int argc,char *argv[])
{
struct semid_ds ds;
union semnu arg;
int j,semid;
if(argc <3 || strcmp(argv[1],"--help") == 0)
{
printf("%s semid val......\n",argv[0]);
}
semid = atoi(argv[1]);
//obtain sizeof semaphore set
arg.buf = &ds;
if(semctl(semid,0,IPC_STAT) == -1)
{
perror("semctl:");
}
if(ds.sem_nsems != argc-2)
{
printf("Set contains %ld semaphores,but %d valued were supplied\n",(long)ds.sem_nsems,argc-2);
}
//Set up array of values;perform semaphore initialization
arg.array = calloc(ds.sem_nsems,sizeof(arg.array[0]));
if(arg.array == NULL)
{
perror("calloc:");
}
for(j = 2;j<argc;j++)
{
arg.array[j-2] = atoi(argv[j]);
}
if(semctl(semid,0,SETALL,arg) == -1)
{
perror("semctl-SETALL");
}
return 0;
}
47.5信号量初始化
根据SUSv3的要求,实现无需对由semget()创建的集合中的信号量值进行初始化,相反程序员必须要使用semctl()系统调用显式地初始化信号量。(在linuxs上,semget()返回的信号量实际上会被初始化为0,但是为取得移植性不能依赖于此。)前面层级提及过,信号量的创建和初始化必须要通过单的的系统调用而不是单个原子步骤完成的事实可能会导致在初始化一个信号量时出现竞争条件。本节将详细介绍竞争的本质,
假设一个应用程序由多个地位平等的进程构成,这些进程使用一个信号量来协调相互之间的动作,由于无法保证哪个进程会先使用信号量(这就是地位平等的含义),因此每个进程都必须要做好在信号量不存在时创建和初始化信号量的准备
错误地初始化了一个System V信号量 --sem_bad_init.c
//Creste a set contains 1 semphore
semid = semget(key,1,IPC_CREAT|IPC_EXCL|perms);
if(semid !=-1)
{
union semnu arg; //successfully created the semaphore
//XXXX
arg.val = 0;//初始化信号量
if(semctl(semid,0,SETVAL,arg) == -1)
{
perror("semctl:");
}
}else{
if(errno !=EEXIST)
{
perror("semget:");
}
semid = semget(key,1,perms);//retrive ID of existing set
if(semid ==-1)
{
perror("semget:");
}
}
//NOW perform some operation on semaphore
sops[0].sem_op = 1; //add 1..
sops[0].sem_num = 0; //to semphore 0
sops[0].sem_flag = 0;
if(semop(semid,sops,1) == -1)
{
perror("semsops:");
}
上述代码村存在的问题是如果两个进程同时执行,如果第一个进程的的时间片在代码中标记为==//XXXX==处期满,那么就可能出现47-2图中给出的顺序,这个顺序之所以存在问题有两个原因。首先进程B在一个未初始化的信号量(其值是一个任意值)上执行了一个semop().其次,进程A中的semctl()调用覆盖了进程B所做的变更。
这个问题的解决方案依赖于一个现已称为标准的特性,即与这个信号量相关联的semid_ds数据结构中的sem_otime字段的初始化。在一个信号量集首次被创建时,sem_otime字段会初始化0,并且且只有后续的semop()调用才会修改这个值,因此可以利用这个特性来修改上面描述的竞争条件,即只需要插入额外的代码来强制第二个进程(即没有创建信号的那个进程)等待直到第一个进程既初始化了信号量又执行了一个更新sem_otime字段但不修改信号量的值的semop调用为止。
遗憾的是,正文中描述的初始化问题方案无法在所有UNIX实现上正常工作,在一些BSD衍生版中。,semop不会更新sem_otime字段
semid = semget(key,1,IPC_DREAT|IPC_EXCEL|perms);
if(semid !=-1)
{
union semnu arg;
struct sembuf sop;
arg.val =0;
if(semctl(semid,0,SETVAL,arg) == -1)
{
perror("semctl:");
}
//Perform a "no op" semaphore opertion -changes sem_otime so other processes can see we 've initialized the set
sop.sem_num = 0; //opreate on semaphore 0
sop.sem_op = 0; //wait for value = 0;
sop.sem_falg =0
if(semop(semid,&sop,1) == -1)
perror("semop");
}else{
const int MAX_TRIES = 10;
int j;
union semun arg;
struct semid_ds ds;
if(errno != EEXIST)
{
perror("semget:");
}
semid = semget(key,1,perms);
if(semid == -1)
{
perror("semget second");
}
//wait until another process has called semop
arg.buf = &ds;
for(j = 0;j<MAX_TRIES;j++)
{
if(semctl(semid,0,IPC_STAT,arg) == -1)
perror("semctl:");
if(ds.sem_otime !=0) // semop perform?
break; //yes quit loop
sleep(1); // not wait and retry
}
if(ds.sem_otime ==0)
printf("not initialized:")
}
//NOW perform some operation on the semaphore
使用上述代码给出的技术的各种变体可以确保一个集合中的多个信号量正确的被初始化以及一个信号量被初始化一个非0值。
并不是所有应用程序都需要使用这个极其负责的解决方案来解决竞争问题,==如果能够确保一个进程在其他进程使用信号量之前创建和初始化信号量就无需使用这个解决方案。==如父进程在创建与其共享信号量的子进程之前先创建和初始化信号量。在这种情况下,让第一个进程在调用完senget()之后执行一个semctl()SETVAL SETALL 操作就足够了。
47.6信号量操作
semop()系统调用在semid标识的信号量集中的信号量上执行一个或多个操作。
#include <sys/types.h>
#include <sys/sem.h>
int semop(int semid,struct sembuf *sops,unsigned int nsops);
Return 0 on success,or -1 on error
sops 是一个指向数组的指针,数组中包含了需要执行的操作,nops参数给出了数组的大小(数组至少包含一个元素)操作将会按照在数组的顺序以原子的方式被执行,sos数组中元素形式如下结构
struct sembuf{
unsinged short sem_num; //semaphore number
short sem_op; //operation to be performed
short sem_flag; //operation flags(IPC_NOWAIT and SEM_UNDO)
};
sem_num 字段表示出了在集合中那个信号量上执行操作,sem_op字段指定了需要执行的操作
-
如果 sem_op大于0,那么就将sem_op,的值加到信号量上,其结果是其他等待减小信号量值的进程可能会被唤醒,并执行他们的操作,调用进程必须要具备在信号量上的修改权限,
-
如果sem_op等于0,那么对信号量值进行检查以确定它当前是否等于0,如果等于0,那么操作立即结束,否则semop()就会阻塞直到信号量变为0为止,调用进程必须要具备在信号量上的读权限
-
如果sem_op小于0,那么就将信号量减去sem_op,如果信号量的当前值大于或等于sem_op的绝对值,那么操作会立即结束,。否则semop信号量阻塞到直到信号量增长到在执行操作之后不会导致出现负值的情况为止,调用进程必须要具备在该信号量上的修改权限。
==从语义上来讲,增加信号量值对应于使一种资源变得可用,以便其他进程可以使用他,而减少信号量值对应于预留(互斥地)进程需使用的资源。==在减少一个信号量值时,如果信号量的值太低–即其他进程已经预留了这个资源,那么操作就会阻塞。
当semop()调用阻塞时,进程会保持阻塞直到发生下列某种情况为止。 -
另一个进程修改了信号量值,使得待执行的操作能够继续向前。
-
一个信号中断了semop()调用。发生这种情况时,会返回EINTR错误
-
另一个进程删除了semid引用的信号量,发生这种情况时,sem_op()会返回EIDRM错误
在特定信号量上执行一个操作时可以通过在相应的sem_flg中指定IPC_NOWAIT标记防止sem_op()阻塞,此时如果sem_op(),本来要阻塞的话就会立即返回EAGAIN错误,
尽管通常一次只会操作一个信号量,但也可以通过一个sem_op()系统调用在一个集合中的多个信号量上执行操作,这里需要指出的关键一点是这一组操作是原子的,即sem_op()要么立即执行所有操作,要么就阻塞直到能够同时执行所有操作
如下程序演示了sem_op()在一个集合中的三个信号量上执行操作,根据信号量的当前值不同,在信号量0和2上可能无法立即前往执行,如果无法执行在信号量0上的操作,那么所有的请求都不会执行,sem_op()会被阻塞。另一方面如果可以执行在信号量0上的操作,但无法立即执行在信号量2上的所有操作,那么由于指定了IPC_NOWAIT标记–所有请求的操作都不会被执行并且semop()会立即返回EAGIN错误
//使用sem_op()在多个systemV上进行操作
struct sem_buf sops[3];
sops[0].sem_num = 0; //subtract 1 from semaphore 0
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
sops[1].sem_num = 0; //add 2 to semaphore 1
sops[1].sem_op = 2;
sops[1].sem_flg = 0;
sops[2].sem_num = 0; //wait for semaphore 2 to eqal 0
sops[2].sem_op = 0;
sops[2].sem_flg = IPC_NOWAIT;
if(semop(semid,sops,3 ==-1))
{
if(errno == EAGAIN)
printf("operation would have blocked\n");
else
perror("semop:");
}
semtimeop()系统调用与semop()执行的任务一样,但是他多了一个timeout参数,通过这个参数可以指定调用所阻塞的时间上限,
#define _GNU_SOUCE_
#include <sys/types.h>
#include<sys/sem.h>
int semtimeop(int semid,struct sembuf *sops,unsigned int nsops,struct timespec *timeout);
Return 0 on success -1 on error;
timeout 参数是一个指向timespec结构的指针,通过这个时间机构能够将一个时间间隔表示秒数或者纳秒数,如果在信号量完成操作之前所等待的时间一金超过了规定的时间间隔,那个semtimop()会返回EAGAIN错误。如果将timeout指定为NULL,那么semtimeop()与semop()完全一样了。
与使用settimer()和semop来比较,semtimeop()系统调用提供了一种更加高效的方式来为信号量操作设置一个超时时间,对于那些经常需要执行此类操作的应用程序(特别是一些数据库系统)这种方式所带来的性能上的提升是显著的。
//使用semop()执行system V信号量操作,
周六完成
47.7多个阻塞信号量操作的处理
如果多个因尖笑一个信号量值而发生阻塞的进程对该信号量减去的值是一样的,那么当条件允许时到底那个进程手下会被允许执行是不确定的(即哪个进程能够执行操作依赖于哥哥内核自己的而进程调度算法),
另一方面,如果多个因减小一个信号量值而发生阻塞的进程对该信号量减去的值是不同的,那么会按照新满足条件先服务的顺序来执行,假设一个信号量的当值为0,进程A将信号量值减去2,然后进程B请求将信号量值减去1,如果第三个进程将信号量值加上了1,那么B进程会首先被解除阻塞并执行他的操作,即使进程A首先请求在该信号量上进行操作,也一样,在一个糟糕的应用程序设计中,这种产奖会导致饿死情况发生,即一个进程因一个信号量的状态无法满足锁清秋的操作继续往前执行的条件而永远阻塞。回到之前的例子考虑多个进程交替地调整信号量使其值永远不会出现大于1的情况,这就导致进程A永远保持阻塞。
当一个进程师徒在多个信号量上执行操作而发生阻塞时也可能会初夏饿死的情况。考虑下面的这一些在一组信号量上执行的操作,两个信号量初始值都为0,
- 进程A请求将信号量0和1的值减去1(阻塞)
- 进程B请求将信号量0的值减去1(阻塞)
- 进程C将信号量0的值加上1,
此刻进程B解除阻塞并完成了他的请求,即使他发出的请求的时间要晚于A,同样也可是设计出一个让进程A饿死的同时让其他进程调整和阻塞于单个信号量的场景。
47.8信号量撤销值
假设一个信号量在调整完一个信号量值(如减少信号量值使之等于0)之后终止了,不管是有意终止还是意外终止,在默认情况下,信号量值将不会发生变化。这样就可能会给其他使用这个信号量的进程带来问题,因为他们可能因等待这个信号量而阻塞着,–即等待已经被终止的进程撤销对信号量所做的变更,
为避免这种问题的发生,再通过semop()修改一个信号量值时可以使用SEM_UNDO标记,当指定这个标记时,内核会记录信号量操作的效果,然后在进程终止时撤销这个操作,不管进程是正常终止还是非正常终止,撤消操作都会发生
SEM_UNDO的限制
最后需要指出的是,SEM_UNDO其实并没有一开始看起来那样有用,原因有两个。一个原因是y偶遇修改一个信号量通常对于请求或释放一些资源,因此仅仅使用SEM_UNDO可能不足以允许一个多进程应用程序在一个进程一场终止时恢复。除非进程终止会原子地将共享资源的状态返回到一个一致的状态(在很多情况下是不可能的),否则撤销一个信号量操作可能不足以允许应用程序恢复,
第二个影响SEM_UNDO的实用性因素实在一些情况下,当进程终止是无法对信号量进行调整,考虑下面应用于初始值为0的信号量上的操作。
1,进程A将信号量值加2,并未该操作指定SEM_UNDO标记,
2,进程B将信号量值减去1,因袭信号量值将变成1,
3,进程A终止
此时就无法撤销进程A 在第一步中的操作中所产生的效果,因为信号量的值太小了。解决这个问题的潜在方法有三种:
- 强制进程阻塞直到完成所有信号量调整,
- 尽可能的减小信号量的值(即减到0),并退出
- 退出,不执行信号量任何操作
- 第一个解决方案是不可行的,,因为他可能会导致一个即将终止的进程永远阻塞。linux采用了第二种解决方案。其他一些unix实现采纳了第三种解决方案。SUSv3并没有规定一个实现应该使用哪个方案.
47.9 实现一个而二元号量协议
System V 信号量的API是比较复杂的,之所以会这样既因为对信号量值的调整可以是任意的,又因为信号量的分配和操作是几何为单位的,单页正因为这些特性,System V 信号量提供的功能要多于常规应用程序的功能,因此以System V信号量为基础实现一个更加简单的协议(APIs)是非常有用的
一种常见的协议就是二元信号量。一个而原信号量有两个值:可用(空闲)或预留(使用中)。而原信号量有两个操作。
- 预留:试图预留这个信号量以便互斥的使用,如果信号量已经被另一个进程预留了,那么就会直到信号量被释放位置
- 释放:释放一个当前被预留的信号量,这样另一个进程就可以预留这个信号量了,
在一些学校教授的教科书里,这两个操作通常被称为P和V,POOSIX将这两个操作成为post wait
有时候还会定义第三种,有条件的预留:非阻塞地尝试预留这两个信号量以便互斥地使用。如果信号量已经别预留了那么立即返回一个状态标示出这个信号量不可用。
在实现一个二元信号量时必须要选择如何表示可用和预留状态以及如何实现上面的操作。读者稍微思考一下就会发现表示这些状态的方式是使用值1标示空闲和值0标示预留,同时预留和释放的操作分别将信号量的值减1和加1;
//使用System V信号量实现二元信号量
#include <sys/type.h>
#include <sys/sem.h>
union semun { /* Used in calls to semctl() */
int val;
struct semid_ds * buf;
unsigned short * array;
#if defined(__linux__)
struct seminfo * __buf;
#endif
};
#define FALSE 0
#define TRUE 1
typedef unsignd char Boolean;
Boolean bsUseSemUndo = FALSE;
Bollean bsRetryOnEintr = TRUE;
int initSemAvailable(int semId,int SemNum)
{
union semnu arg;
arg.val = -1;
return semctl(semId,semnum,SETVAL,arg);
}
int initSemAvailable(int semId,int SemNum)
{
union semnu arg;
arg.val = 0;
return semctl(semId,semnum,SETVAL,arg);
}
int reserveSem(int semId,int semNum)
{
struct sembuf sops;
sops.sem_num = semNum;
sops.sem_op = -1;
sem.sem_flg = bsUseSemUndo?SEM_UNDO:0;
while(semop(semId,&sops,1) == -1)
{
if(errno !=EINTR || !bsRetryOnintr)
return -1;
}
return 0;
}
int releaseSem(int semId,int semNum)
{
struct sembuf *sops;
sops.sem_num = semNum;
sops.sem_op = 1;
sem.sem_flg = bsUseSemUndo?SEM_UNDO:0;
return semop(semId,&sops,1);
}
47.10信号量限制
大多数UNIX实现都对System V信号量的操作都进行了各种各样的限制,下面里除了linux在信号量的限制,括号中给出了当限制达到时的系统调用及其返回的错误。
SEMAEM
在semadj总和中能够记录的最大值,SEMAEM,的值与SEMVMX(稍后介绍)的值是一样的(semop(),EARGE)
SEMMNI,这是一个系统级别的限制,它限制了所能创建的信号量标识符的数量(换句话说是信号量集)。(semget,ENOSPC)
SEMMSL
一个信号量集中能分配的信号量的最大数量。
SEMMNS
这是系统级别的一个限制,它限制了所有信号量集张的信号量数量。系统上信号量的数量还受SEMMNI和SEMMSL的限制,实际上SEMMNS的默认值就是这两个限制默认值的乘积,(semget(),ENOSPC)
SEMOPM
每个semop()调用能够执行的操作的最大数量(semop() E2BIG)
semvmx
一个信号量能取的最大值(semop(),ERANGE)
表2,
限制 | 最大值 (x86-32) |
---|---|
SEMMNI | 32768 |
SEMSL | 65536 |
SEMMNI | 2147483647 INT_MAX |
SEMMNI | 参见正文 |
- 可以讲SEMMSL的值设置为一个大于65536的值,并且所创建的信号量机中最多可包含该数量的信号量。但是无法使用semop()调整集合中第65536之后的元素
- SEMNS实际最大值使用系统桑可用的RAM来控制的。
- SEMOP限制的最大值室友内核做使用的内存分配原语来决定的,建议的最大值是1000,在实际使用中在单个semop调用中执行过多的操作没有太大的用处。
47.11System V信号量的缺点
- System V信号量存在的很多缺点与消息队列的缺点是一样的包括以下几点。
信号量是通过标识符而不是大所属UNIX I/O和IPC所采用的文件描述符来引用的。这使得执行诸如同时等待一个文件IO和文件描述符的输入之类的操作就会变得比较困难(不能使用I/O模型来同时检测) - 使用键而不是文件名来标识信号量增加了额外的变成难度。
- 创建和初始化号量需要使用单独的系统调用意味着在一些情况下必须要做一些额外的编程工作来防止在初始化一个信号量时出现竞争条件
- 内核不会维护引用一个信号量集的进程数量。这就给确定何时删除一个信号量增加难度并且难以确保一个不再使用的信号量集会被删除。
- System V提供的编程接口过于复杂,在通常情况下一个程序只会操作一个信号量。同时操作集合中多个信号量的能力有时候是多余的
- 信号量的操作有许多限制,这些限制是可配置的。
不管怎样,与消息队列不同的是,替代System V信号量的方案不多,在很多情况下需要用到他们,linux 从内核2.6及之后的版本开始支持使用POSIX信号量来进行进程同步。
47.12总结
System V 信号量允许进程同步他们的动作,这在一个进程必须要获取对默写共享资源的互斥性访问时是比较有用的。
信号量的创建和操作是以集合为单位的,一个集合包含一个或多个信号量,集合中的信号量都是一个整数,其值永远大于0,semop()系统调用者在一个信号量上加上一个整数,从一个信号量减去一个整数,或者等待一个信号量等于0,后两个操作可能回导致调用者阻塞。
信号量实现无需对一个新信号量集中的成员进行初始化,因此应用程序必须要在城建完成之后对它们进行初始化操作,当一些地位平等的进程中任意一个进程试图创建和初始化信号量时就需要特别小心以防止这两个步骤是通过单独的系统调用来完成的而可能出现的竞争条件。
如果多个进程对该信号量减去的值是一样的,那么当条件允许时到底那个进程会首先被允许执行操作是不确定的,但是如果多个进程对信号量减去的值是不同的,那么会按照先满足条件先服务的顺序来进行并且需要小心避免出现一个进程因信号量永远无法达到允许进程继续操作继续往前执行的值而饿死的情况。
SEM_UNDO标记允许一个进程的信号量在进程终止时自动撤销。这对于防止意外终止而引起的信号量处于一个回导致其他进程因等待已终止的进程修改信号量值而永远阻塞的情况是比较有用的,
System V分配和操作信号量是以集合为单位的,并且对其增加和减少的数量可以使任意的。他们提供的功能要多于大多数应用程序所需的功能,对信号量常见的要求是单个二元信号量,他的值只能取0和1.相当于POSIX 信号量的P V操作。