本文将为你说明IPC的另一种方式:信号量。在操作系统原理课程中,我们对信号量机制有所了解,即实现进程对共享资源的互斥访问以及进程间的同步。与其他几种通信方式相比,信号量更突出的是对共享资源的访问控制。接下来,我们一边回忆以前学过的信号量机制,一边学习如何用系统调用函数来实现相关操作。
1.创建信号量集
注意这里使用的是信号量集,而不是创建一个信号量。通常我们用伪代码实现进程间的互斥或同步时,都定义一个整形变量作为信号量,初值一般都为0或1。实际应用时,我们每次都需要创建一个信号量集,即使此集合只包含一个信号量。一般我们通过下面函数去创建或者打开一个信号量集。
int semget(key_t key,int nsems,int semflg);
我们这里需要注意的是,此函数何时创建一个信号量集,何时打开一个信号量集。当semflg=IPC_CREATE时,如果当前系统中不存在此信号量集合(key值不存在),那么semget函数完成一个信号量的创建;否则,semget函数打开这个已存在的信号量集。当semflg=IPC_CREATE|IPC_EXCL时,只会完成创建,如果key值对应的信号量集合以存在,那么直接返回错误,错误代码为EEXIST。这并不难理解,和open文件的情况类似。此函数成功执行返回信号量集的标示符,否则为-1。
另外关于我们所创建的这个信号量集,有结构体struct semid_ds与之对应,这个结构体中存储信号量集中的相关属性。
2.PV操作的实现
对于信号量机制,我们最熟悉的应该便是PV操作了。以前我们在写进程互斥或同步的伪代码时,都是直接来使用P(semaphore)和V(semaphore)。那么我们如何具体实现?
int semop(int semid,struct sembuf *sops,size_t nsops);
现在我要告诉你,P和V操作均通过上述函数来完成。你肯定马上会想,P和V两个截然不同的操作如何使用上述函数来完成?这其实取决于sops参数。请先看这个参数的类型:
struct sembuf
{
ushort sem_num;/*信号在信号量集中的索引*/
short sem_op; /*操作类型,是P还是V*/
short sem_flg;/*操作标志*/
}
我们可以发现,通过semid和sem_num两个字段就可以确定信号量集中的指定信号量。sem_op取不同的值就会产生不同的操作。特别的,如果其值为0,则此时semop操作的作用是测试信号量的值是否为0。下面的代码是经典PV操作的实现方法。
int p(int semid,int index)
{
struct sembuf buf={0,-1,IPC_NOWAIT};
if(index<0)
{
printf("error:index of the semaphore is invalid\n");
exit(1);
}
buf.sem_num=index;
if(semop(semid,&buf,1)==-1)
{
printf("P operation failed\n");
exit(1);
}
return 0;
}
int v(int semid,int index)
{
struct sembuf buf={0,+1,IPC_NOWAIT};
if(index<0)
{
printf("error:index of the semaphore is invalid\n");
exit(1);
}
buf.sem_num=index;
if(semop(semid,&buf,1)==-1)
{
printf("V operation failed\n");
exit(1);
}
return 0;
}
我们这里特别要说明的是struct sembuf结构中的sem_flg字段,如果其值为IPC_NOWAIT,根据sem_op的不同意义也不同。当sem_op为0时,如果设置IPC_NOWAIT,那么调用进程直接返回;否则调用进程进入睡眠状态直至信号量为0。当sem_op<0时,表示申请资源,如果没有设置IPC_NOWAIT,那么调用进程阻塞至此资源被释放;否则进程直接返回。
3.信号量的控制
上述两部分内容——如何创建信号量集,如何PV操作——似乎已经可以诠释信号量了,但是这毕竟不是我们写伪代码,我们必须对信号量进行相关控制,比如如何对信号量设置初值,如何得到信号量当前的值等等。因此掌握好控制函数semctl尤为重要。
int semctl(int semid,int semnum,int cmd,union semun);
注意第四个参数类型是不确定的,由cmd的类型来确定。第四个参数所有可能会用到的变量都被定义在一个共用体中。
union semun
{
int val;
struct semid_ds *buf;
ushort *array;
void *pad;
}
下面我们对几种常用的cmd进行说明。
如果我们想打印某个已存在信号量集的各个属性信息时,那么可以参考下面代码。
union semun semop;
struct semid_ds seminfo;
semop.buf=&seminfo;
if(semctl(semid,0,IPC_STAT,semop)==-1)
{
printf("error:get the info of semaphroe failed\n");
exit(1);
}
因为此时候cmd为IPC_STAT,那么semctl会自动从semop中取走buf字段。此操作成功完成后,信号量集的信息便存储在seminfo中。注意,在使用此操作前,一定要将buf指向一个struct semid_ds类型的变量,否则buf将是一个”野指针”。
如果想为某个指定的信号量设置初值,那么下面代码就是实现将标示符为semid的信号量集中索引为index的信号量赋值为5。
union semnu semop;
semop.val=5;
semctl(semid,index,SETVAL,semop);
想得到某个信号量当前的值时,就可以参考下面代码,注意第四个参数为0,因为cmd为GETVAL,所以semctl函数会忽略第四个参数的取值。
int semval;
semval=semctl(semid,index,GETVAL,0);
关于信号量的基本知识就是这些,下篇文章将用上述内容来实现一个进程互斥的例子。