当我们编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户进程系统上,我们通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权。比如多个程序试图在同一时间更新某个数据库,数据就可能会遭到破坏。两个不同的程序要求不同的用户向数据库输入数据,这本身没有问题,但对数据库进行更新的那部分代码可能就有问题。这部分真正执行数据更新的代码需要独占式地执行,它们被称为临界区域。它们通常只在一个大型程序中占据一小段的代码。
为了防止出现因多个程序同时访问一个共享资源而引发的问题,需要有一种方法可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域(线程可以通过互斥量或信号量来控制对临界区域的访问)。这里将介绍信号量的方法来规避这一问题(注意,这里的信号量函数比用于线程的信号量函数更通用)。
要想编写通用的代码,以确保程序对某个特定的资源具有独占式的访问权是非常困难的,虽然有一个叫Dekker算法的解决方法,但这个算法依赖于“忙等待”或“自旋锁”。也就是说,一个进程要持续不断地运行以等待某个内存位置被改变。在像Linux这样的多任务环境中,人们并不愿意使用这种浪费CPU资源的处理方法。但如果硬件支持独占式访问(一般是通过特定的CPU指令的形式),那么情况就变得简单多了。一个硬件支持的例子就是,用一条指令以原子方式访问并增加寄存器的值,在这个读取/增加/写入操作执行的过程中不会有其它指令(甚至一个中断)发生。我们可能还见过另一种解决方法是,使用带O_EXCL标志的open函数来创建锁文件,它提供了原子化的文件创建方法。它允许一个进程通过获取一个令牌(即新创建的文件)来取得成功。这个方法比较适合于处理简单的问题,但对于更复杂的例子,它显得比较杂乱且缺乏效率。所以荷兰计算机科学家Edsger Dijkstra提出的信号量概念在并发编程领域迈出了很重要的一步。
信号量的原理是一种数据操作锁的概念,它本身不具备数据交换的功能,而是通过控制其他的信号资源(如文件、外部设备等)来实现进程间的通信。信号量本身不具备数据传输的功能,它只是一种外部资源的标识。
信号量的一个更正式的定义是:它是一个特殊变量,只允许对它进行等待(wait)和发送信号(signal)这两种操作。因为在Linux编程中,“等待”和“发送信号”都已具有特殊的含义,所以我们将用原先定义的符号来表示这两种操作。
- P(信号量变量):用于等待。
- V(信号量变量):用于发送信号。
6.1 信号量的概念
struct semid_ds
{
struct ipc_perm sem_perm; /* opeation permission struct */
struct sem *sem_base; /* ptr to first semaphore in set */
unsigned short sem_nsems; /* numbers of semaphores in set */
time_t sem_otime; /* last semop time */
time_t sem_ctime; /* last change time */
};
sem结构记录了一个信号量的信息,其定义如下:
struct sem
{
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last operation */
unsigned short semncent; /* numbers of processes awaiting semval > currval */
unsigned short semzcnt; /* numbers of processes awaiting semval = 0 */
};
接下来用一个简单的理论性的例子来继续阐明信号量的工作原理。假设有两个进程proc1和proc2,这两个进程都需要在其执行过程中的某一时刻对一个数据库进行独占式的访问。我们定义一个二进制信号量sv,该变量的初始值为1,两个进程都可以访问它。要想对代码中的临界区域进行访问,这两个进程都需要执行相同的处理步骤,事实上,这两个进程可以只是同一个程序的两个不同执行实例。两个进程共享信号量变量sv。一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区域。而第二个进程将被阻止进入临界区域,因为当它试图执行P(sv)操作时,它会被挂起以等待第一个进程离开临界区域并执行V(sv)操作释放信号量。需要的伪代码对两个进程都是相同的,如下所示:
semaphore sv = 1;
loop forever {P(sv);critical code section;V(sv);nocritical code section;}
6.2 信号量集的相关操作
6.2.1 创建或打开信号量集
#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semget (key_t key, int nsems, int semflg);
6.2.2 对信号量集的操作
#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semop (int semid, struct sembuf *sops, size_t num_sem_ops);
struct sumbuf
{
short sem_num;
short sem_op;
short sem_flg;
};
sem_num是信号量编号(其值是一个从0到相应的信号量集的资源总数,即ipc_perm.sem_nsems之间的整数),除非你需要使用一组信号量,否则它的取值一般为0。sem_op成员的值是信号量在依次操作中需要改变的数值(你可以用一个非1的数值来改变信号量的值)。通常只会用到两个值,一个是-1,也就是P操作,它等待信号量变为可用;一个是+1,也就是V操作,它发送信号表示信号量现在已可用。最后一个成员sem_flg通常被设置为SEM_UNDO。它将使得操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。除非你对信号量的行为有特殊的要求,否则应该养成设置sem_flg为SEM_UNDO的好习惯。如果决定使用一个非SEM_UNDO的值,那就一定要注意保持设置的一致性,否则你很可能会搞不清楚内核是否会在进程退出时清理信号量。
- sem_op > 0:表示进程对资源使用完毕,释放相应的资源数,并将sem_op的值加到信号量的值上。
- sem_op = 0:进程阻塞直到信号量的相应值为0,当信号量已经为0,函数立即返回。如果信号量的值不为0,则依据sem_flg的IPC_NOWAIT位决定函数动作。sem_flg指定IPC_NOWAIT,则semop函数出错返回EAGAIN。sem_flg没有指定IPC_NOWAIT,则将信号量的semncnt值减1,然后进程挂起直到下述情况发生。信号量值为0,将信号量的semncnt值减1,函数semop成功返回;此信号量被删除(只有超级用户或创建用户进程拥有此权限),函数semop出错返回EIDRM;进程捕捉到此信号,并从信号处理函数返回,在此情况下将信号量的semncnt值减1,函数semop出错返回EINTR。
- sem_op < 0:其取sem_op的绝对值的资源,如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。当相应的资源数不能满足请求时,这个操作与sem_flg有关。sem_flg指定IPC_NOWAIT,则semop函数出错返回EAGAIN。sem_flg没有指定IPC_NOWAIT,则该信号量的semncnt值加1,然后进程挂起直到下述情况发生。当相应的资源数可以满足请求时,该信号的值减去sem_op的绝对值,成功返回;此信号量被删除(只有超级用户或创建用户进程拥有此权限),函数semop出错返回EINRM;进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR。
6.2.3 信号量集的控制
#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semctl (int semid, int semnum, int cmd, ...);
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
虽然X/Open规范中指出,semun联合结构必须由程序员自己定义,但大多数Linux版本会在某个文件(一般是sem.h)中给出该结构的定义。如果你发现确实需要自己来定义该结构,可以查阅semctl的手册页,看手册中是否已给出了定义。如果有,建议使用手册中给出的定义。
取值 | 含义 |
GETALL | 获得信号量集semid中信号量的个数,并将该值赋值给无符号短整数arg.array |
GETVAL | 获得信号量集semid中semnum所指定信号量的值semval |
GETNCNT | 获得信号量集semid中等待给定信号量锁的进程数目,即semid_ds结构中sem.semncnt的值 |
GETPID | 获得信号量集semid中最后一个使用semop函数的进程ID,即semid_ds结构中sem.sempid的值 |
GETZCNT | 获得信号量集semid中等待信号量称为0的进程数目,即semid_ds结构中sem.semzcnt的值 |
IPC_RMID | 删除信号量集。此操作只能由具有超级用户的进程或信号量拥有者的进程执行,这个操作会影响到正在使用信号量集的进程 |
IPC_SET | 按照参数arp.buf指向的结构体中的值设置此信号量集的sem_perm.uid、sem_perm.gid及sem_perm.mode的值。此操作只能由具有超级用户的进程或信号量拥有者的进程执行 |
IPC_STAT | 获得该信号量的semid_ds结构,保存在arg.buf指向的缓冲区 |
SETALL | 以arg.array的值设置信号量集semid中信号量的个数 |
SETVAL | 以arg.val的值设置信号量集semid中semnum所指定信号量的值semval。其作用是在信号量第一次使用之前对它进行设置 |
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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 semid;
int main(int argc, char **argv)
{
int i;
int ptime;
char opch = 'O';
srand((unsigned int)getpid());
semid = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if (argc > 1)
{
if (!set_semvalue())
{
fprintf(stderr, "failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
opch = 'X';
sleep(2);
}
for (i = 0; i < 10; i++)
{
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", opch);
fflush(stdout);
ptime = rand() % 3;
sleep(ptime);
printf("%c", opch);
fflush(stdout);
if (!semaphore_v())
exit(EXIT_FAILURE);
ptime = rand() % 2;
sleep(ptime);
}
printf("\n%d - finished\n", getpid());
if (argc > 1)
{
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if (semctl(semid, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue(void)
{
union semun sem_union;
if (semctl(semid, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "failed to delete semaphore\n");
}
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(semid, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(semid, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
[1] 11740
$ ./sem_com
OOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXOOXXXX
11740 - finished
整理自 《Linux程序设计第4版》、《Linux C编程从初学到精通》。