在操作完共享资源后,需释放信号量,以便另外的进程来获得资源。获得和释放应该成对出现。
获得信号量集,需要注意的是,获得的是一个集合,而不是一个单一的信号量。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
1: int semget(key_t key,int nsems,int semflg);
key:系统根据这个值来获取信号量集。
nsems:此信号集包括几个信号量。
semflg:创建此信号量的属性。 (IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR)
成功则返回该信号量集的ID。
注:
既指定IPC_CREAT又指定IPC_EXCL时,如果系统中该信号量集已经存在,则马上返回。
如果需要获得存在的信号量,则将此参数置0.
2: int semctl(int semid,int senum,int cmd....)
semid:信号量ID。
senum:对信号量集中的第几个信号量进行控制。(从0开始)
cmd:需要进行的操作。(SETVAL是其中的一个)。
根据cmd的不同可能存在第四个参数,cmd=SETVAL时,表示同时信号量可以被获得几次,如第四个参数
num=1表示只能被获得一次,既被信号量保护的资源只能同时被一个程序使用。
该系统调用,是在对信号量初始化时用的。
-3: “3”前面加了"-"表示当需要使用互斥资源时应该做这步。
int semop(int semid,struct sembuf *sem,int num_elements);
struct sembuf {
unsigned short sem_num; //该信号量集中的第几个信号量。
int sem_op;//需要获得还是释放信号量
int sem_flg;//相关动作
};
num_elements:需要对该信号量集中的多少个信号量进行处理。
获得信号量时,将sembuf结构提初始化为:
sem_num = 0; //该信号量集中的首个信号量
sem_op = -1; //获得信号量
sem_flag = IPC_NOWAIT; //如果不能获得信号量,马上返回。
semop(semid,_sem,1);
同理释放信号量时,将sem_op设为1.
现在我们已经了解了什么是信号量以及他们在理论上是如何工作的,现在我们可以来了解一下这些特性在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);
事实上,为了获得我们特定操作所需要的#define定义,我们需要在包含sys/sem.h文件之前通常需要包含sys/types.h与sys/ipc.h文件。而在某些情况下,这并不是必须的。
因为我们会依次了解每一个函数,记住,这些函数的设计是用于操作信号量值数组的,从而会使用其操作向比单个信号量所需要的操作更为复杂。
注意,key的作用类似于一个文件名,因为他表示程序也许会使用或是合作所用的资源。相类似的,由semget所返回的并且为其他的共享内存函数所用的标识符与由fopen函数所返回 的FILE*十分相似,因为他被进程用来访问共享文件。而且与文件类似,不同的进程会有不同的信号量标识符,尽管他们指向相同的信号量。key与标识符的用法对于在这里所讨论的所有IPC程序都是通用的,尽管每一个程序会使用独立的key与标识符。
semget
semget函数创建一个新的信号量或是获得一个已存在的信号量键值。
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是一个用来允许不相关的进程访问相同信号量的整数值。所有的信号量是为不同的程序通过提供一个key来间接访问的,对于每一个信号量系统生成一个信号量标识符。信号量键值只可以由semget获得,所有其他的信号量函数所用的信号量标识符都是由semget所返回的。
还有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是创建一个只有创建进程可以访问的信号量。这通常并没有有用的目的,而幸运的是,因为在某些Linux系统上,手册页将IPC_PRIVATE并没有阻止其他的进程访问信号量作为一个bug列出。
num_sems参数是所需要的信号量数目。这个值通常总是1。
sem_flags参数是一个标记集合,与open函数的标记十分类似。低九位是信号的权限,其作用与文件权限类似。另外,这些标记可以与IPC_CREAT进行或操作来创建新的信号量。设置IPC_CREAT标记并且指定一个已经存在的信号量键值并不是一个错误。如果不需要,IPC_CREAT标记只是被简单的忽略。我们可以使用IPC_CREAT与IPC_EXCL的组合来保证我们可以获得一个新的,唯一的信号量。如果这个信号量已经存在,则会返回一个错误。
如果成功,semget函数会返回一个正数;这是用于其他信号量函数的标识符。如果失败,则会返回-1。
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)通常情况下中使用两个值,-1是我们的P操作,用来等待一个信号量变得可用,而+1是我们的V操作,用来通知一个信号量可用。
最后一个成员,sem_flg,通常设置为SEM_UNDO。这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没有释放这个信号量,如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。将sem_flg设置为SEM_UNDO是一个好习惯,除非我们需要不同的行为。如果我们确实变我们需要一个不同的值而不是SEM_UNDO,一致性是十分重要的,否则我们就会变得十分迷惑,当我们的进程退出时,内核是否会尝试清理我们的信号量。
semop的所用动作会同时作用,从而避免多个信号量的使用所引起的竞争条件。我们可以在手册页中了解关于semop处理更为详细的信息。
semctl
semctl函数允许信号量信息的直接控制:
int semctl(int sem_id, int sem_num, int command, ...);
第一个参数,sem_id,是由semget所获得的信号量标识符。sem_num参数是信号量数目。当我们使用信号量数组时会用到这个参数。通常,如果这是第一个且是唯一的一个信号量,这个值为0。command参数是要执行的动作,而如果提供了额外的参数,则是unionsemun,根据X/OPEN规范,这个参数至少包括下列参数:
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}
许多版本的Linux在头文件(通常为sem.h)中定义了semun联合,尽管X/Open确认说我们必须定义我们自己的联合。如果我们发现我们确实需要定义我们自己的联合,我们可以查看semctl手册页了解定义。如果有这样的情况,建议使用手册页中提供的定义,尽管这个定义与上面的有区别。
有多个不同的command值可以用于semctl。在这里我们描述两个会经常用到的值。要了解semctl功能的详细信息,我们应该查看手册页。
这两个通常的command值为:
SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:当信号量不再需要时用于删除一个信号量标识。
semctl函数依据command参数会返回不同的值。对于SETVAL与IPC_RMID,如果成功则会返回0,否则会返回-1。
使用信号量
正如我们在前面部分的描述中所看到的,信号量操作是相当复杂的。这是最不幸的,因为使用临界区进行多进程或是多线程编程是一个十分困难的问题,而其拥有其自己复杂的编程接口也增加了编程负担。
幸运的是,我们可以使用最简单的二值信号量来解决大多数需要信号量的问题。在我们的例子中,我们会使用所有的编程接口来创建一个非常简单的用于二值信号量的P
与V类型接口。然后,我们会使用这个简单的接口来演示信号量如何工作。
要试验信号量,我们将会使用一个简单的程序,sem1.c,这个程序我们可以多次调用。我们将会使用一个可选的参数来标识这个程序是负责创建信号量还是销毁信号量。
我们使用两个不同字符的输出来标识进入与离开临界区。使用参数调用的程序会在进入与离开其临界区时输出一个X,而另一个程序调用会在进入与离开其临界区时输出一个O。因为在任何指定的时间内只有一个进程能够进入其临界区,所以所有X与O字符都是成对出现的。
试验--信号量
1在#include语句之后,我们定义函数原型与全局变量,然后我们进入main函数。在这里使用semget函数调用创建信号量,这会返回一个信号量ID。如果程序是第一次调用(例如,使用一个参数并且argc >1来调用),程序就会调用set_semvalue来初始化信号量并且将op_char设置为X。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include "semun.h"
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
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);
}
2 然后我们使用一个循环代码进入并且离开临界区10次。此时会调用semaphore_p函数,这个函数会设置信号量并且等待程序进入临界区。
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);
3 在临界区之后,我们调用semaphore_v函数,在随机的等待之后再次进入for循环之后,将信号量设置为可用。在循环之后,调用del_semvalue来清理代码。
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);
}
4 函数set_semvalue在一个semctl调用中使用SETVAL命令来初始化信号量。在我们使用信号量之前,我们需要这样做。
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;
}
5 del_semvalue函数几乎具有相同的格式,所不同的是semctl调用使用IPC_RMID命令来移除信号量ID:
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");
}
6 semaphore_p函数将信号量减1(等待):
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flag = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed/n");
return 0;
}
return 1;
}
7 semaphore_v函数将sembuf结构的sem_op部分设置为1,从而信号量变得可用。
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flag = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed/n");
return 0;
}
return 1;
}
注意,这个简单的程序只有每个程序有一个二值信号量,尽管如果我们需要多个信号量,我们可以扩展这个程序来传递多个信号量变量。通常,一个简单的二值信号量就足够了。
我们可以通过多次调用这个程序来测试我们的程序。第一次,我们传递一个参数来通知程序他并不负责创建与删除信号量。另一次调用没有传递参数。
下面是两次调用的示例输出结果:
$ ./sem1 1 &
[1] 1082
$ ./sem1
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
1083 - finished
1082 - finished
$
正如我们所看到了,O与X是成对出现的,表明临界区部分被正确的处理了。如果这个程序在我们的系统上不能正常运行,也许我们需要在调用程序之前使用命令stty -tostop来保证生成tty输出的后台程序不会引起信号生成。