欢迎转载,请保留作者信息
bill@华中科技大学
http://billstone.cublog.cn
十三章 信号量
进程间的通信不仅仅包括数据交流, 也包括过程控制.
信号量是一个可以用来控制进程存储共享资源的计数器, 它可以是跟踪共享资源的生产和消费的计数器, 也可以是协调资源的生产者和消费者之间的同步器, 还可以是控制生产进程和消费进程的互斥开关.
信号量简介
操作系统通过信号量和PV操作, 可以完成同步和互斥操作.
信号量集合由一个或多个信号量集合组成, IPC对象中的'信号量'通常指的是信号量集合, UNIX的内核采用结构semid_ds来管理信号量, 结构如下:
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
指针sem_base指向一个信号量数组,信号量由结构sem记载,如下所示:
Struct sem{
unsigned short semval; // 信号量取值
pid_t sempid; // 最近访问进程ID
unsigned short semncnt; // P阻塞进程数
unsigned short semzcnt; // Z阻塞进程数
}
在Shell中可以通过'ipcs -a -s'命令查询系统中信号量的基本信息.
(1) 信号量的创建
在UNIX中, 函数semget用来创建信号量, 原型如下:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
函数semget创建一个新的信号量, 或者访问一个已经存在的信号量.
(2) 信号量的控制
系统调用semctl用来控制信号量, 原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
函数semctl对标识号为semid的信号量集合中序号为semnum的信号量进行赋值, 初始化, 信息获取和删除等多相操作, 参数cmd指定了操作的类型, 参数arg指定了函数输入输出的缓冲区, 定义如下:
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
void *__pad;
};
函数semctl的第四个参数arg在本质上是一个4字节的缓冲区. 调用失败时返回-1并置errno.
本处设计一个类似于命令'ipcs'和命令'ipcrm'的程序ipcsem, 它从命令行参数中获取要执行的操作, 包括创建信号量, 读取信号量信息, 读取信号量取值和删除信号量等, 程序如下:
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <stdio.h>
#define VerifyErr(a, b) \
if (a) fprintf(stderr, "%s failed.\n", (b)); \
else fprintf(stderr, "%s success.\n", (b));
int main(int argc, char *argv[1])
{
int semid, index, i;
unsigned short array[100];
struct semid_ds ds;
if(argc != 4)
return 0;
semid = atoi(argv[1]);
index = atoi(argv[2]);
if(argv[3][0] == 'c'){
VerifyErr(semget(semid, index, 0666|IPC_CREAT|IPC_EXCL) < 0, "Create sem");
}
else if(argv[3][0] == 'd'){
VerifyErr(semctl(semid, 0, IPC_RMID, NULL) < 0, "Delete sem");
}
else if(argv[3][0] == 'v'){
fprintf(stderr, "T ID INDEX SEMVAL SEMIPID SEMNCNT SEMZCNT\n");
fprintf(stderr, "s %6d %6d %10d %10d %10d %10d\n", semid, index,
semctl(semid, index, GETVAL), semctl(semid, index, GETPID),
semctl(semid, index, GETNCNT), semctl(semid, index, GETZCNT));
}
else if(argv[3][0] == 'a'){
ds.sem_nsems = 0;
VerifyErr(semctl(semid, 0, IPC_STAT, &ds) != 0, "Get Sem Stat");
VerifyErr(semctl(semid, 0, GETALL, array) != 0, "Get Sem All");
for(i=0;i<ds.sem_nsems;i++)
fprintf(stderr, "sem no [%d]: [%d]\n", i, array[i]);
}
else
VerifyErr(semctl(semid, index, SETVAL, atoi(argv[3])) != 0, "Set Sem Val");
return 0;
}
执行结果如下:
[bill@billstone Unix_study]$ make ipcsem
cc ipcsem.c -o ipcsem
[bill@billstone Unix_study]$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x000003e8 0 bill 666 10
[bill@billstone Unix_study]$ ./ipcsem 2000 2 c
Create sem success.
[bill@billstone Unix_study]$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x000003e8 0 bill 666 10
0x000007d0 65537 bill 666 2
[bill@billstone Unix_study]$ ./ipcsem 65537 0 100
Set Sem Val success.
[bill@billstone Unix_study]$ ./ipcsem 65537 0 v
T ID INDEX SEMVAL SEMIPID SEMNCNT SEMZCNT
s 65537 0 100 23829 0 0
[bill@billstone Unix_study]$ ./ipcsem 65537 1 200
Set Sem Val success.
[bill@billstone Unix_study]$ ./ipcsem 65537 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [100]
sem no [1]: [200]
[bill@billstone Unix_study]$ ./ipcsem 65537 0 d
Delete sem success.
[bill@billstone Unix_study]$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x000003e8 0 bill 666 10
[bill@billstone Unix_study]$
操作信号量
信号量具有P, V和Z三种操作, 在UNIX中, 这些操作可以通过函数semop调用完成, 函数semop可以一次性操作同一信号量集合中的多个信号量. 原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
函数semop对标识号为semid的信号量集合中的一个或多个信号量执行信号数值的增加, 减少或比较操作. 参数sops指向一个sembuf结构的缓冲区, nsops指定了缓冲区中存储的sembuf结构的个数.
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
其中, 第一个信号的序号是0. sem_op指定了操作的类型:
a) 正数. V操作.
b) 负数. P操作.
c) 0. Z操作. 判断信号量数值是否等于0.
而sem_flg取值有IPC_NOWAIT和SEM_UNDO等.
下面是一个用于临界资源的读写控制和并发进程的同步和互斥控制的实例: 假设进程A是生产者, 进程B是消费者, 系统最多只能同时容纳5个产品, 初始成品数为0. 当产品数不足5时允许进程A生产, 当产品数超过0时允许进程B消费.
这里需要两个信号量模拟生产-消费过程. 信号量A代表了当前生产的数目, 它控制了生产者进程A, 信号量n代表当前尚有n个成品可以生产.
信号B代表了当前的产品数, 他控制消费者进程B, 当信号量为n时剩余n个产品.
生产者进程sema.c如下:
[bill@billstone Unix_study]$ cat sema.c
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/stat.h>
#define VerifyErr(a,b) \
if (a) { fprintf(stderr, "%s failed.\n", (b)); exit(1); } \
else fprintf(stderr, "%s success.\n", (b));
int main(void)
{
int semid;
struct sembuf sb;
VerifyErr((semid = semget(2000, 2, 0666)) < 0, "Open Sem 2000");
sb.sem_num = 0;
sb.sem_op = -1;
sb.sem_flg &= ~IPC_NOWAIT;
VerifyErr(semop(semid, &sb, 1) != 0, "P sem 2000:0");
fprintf(stderr, "[%d] producing ... ... \n", getpid());
sleep(1);
fprintf(stderr, "[%d] produced\n", getpid());
sb.sem_num = 1;
sb.sem_op = 1;
sb.sem_flg &= ~IPC_NOWAIT;
VerifyErr(semop(semid, &sb, 1) != 0, "V sem 2000:1");
return 0;
}
[bill@billstone Unix_study]$
消费者进程semb.c如下:
[bill@billstone Unix_study]$ cat semb.c
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/stat.h>
#define VerifyErr(a,b) \
if (a) { fprintf(stderr, "%s failed.\n", (b)); exit(1); } \
else fprintf(stderr, "%s success.\n", (b));
int main(void)
{
int semid;
struct sembuf sb;
VerifyErr((semid = semget(2000, 2, 0666)) < 0, "Open Sem 2000");
sb.sem_num = 1;
sb.sem_op = -1;
sb.sem_flg &= ~IPC_NOWAIT;
VerifyErr(semop(semid, &sb, 1) != 0, "P sem 2000:1");
fprintf(stderr, "[%d] consuming ... ... \n", getpid());
sleep(1);
fprintf(stderr, "[%d] consumed\n", getpid());
sb.sem_num = 0;
sb.sem_op = 1;
sb.sem_flg &= ~IPC_NOWAIT;
VerifyErr(semop(semid, &sb, 1) != 0, "V sem 2000:1");
return 0;
}
[bill@billstone Unix_study]$
编译程序并使用之前的程序ipcsem创建信号量集合:
[bill@billstone Unix_study]$ ./ipcsem 2000 2 c
Create sem success.
[bill@billstone Unix_study]$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x000003e8 0 bill 666 10
0x000007d0 98305 bill 666 2
[bill@billstone Unix_study]$ ./ipcsem 98305 0 5
Set Sem Val success.
[bill@billstone Unix_study]$ ./ipcsem 98305 1 0
Set Sem Val success.
[bill@billstone Unix_study]$ ./ipcsem 98305 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [5]
sem no [1]: [0]
[bill@billstone Unix_study]$
在一个终端上运行sema:
[bill@billstone Unix_study]$ ./ipcsem 98305 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [3]
sem no [1]: [2]
[bill@billstone Unix_study]$ ./sema
Open Sem 2000 success.
P sem 2000:0 success.
[23940] producing ... ...
[23940] produced
V sem 2000:1 success.
[bill@billstone Unix_study]$ ./ipcsem 98305 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [2]
sem no [1]: [3]
[bill@billstone Unix_study]$
在另一个终端上执行semb:
[bill@billstone Unix_study]$ ./ipcsem 98305 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [2]
sem no [1]: [3]
[bill@billstone Unix_study]$ ./semb
Open Sem 2000 success.
P sem 2000:1 success.
[23942] consuming ... ...
[23942] consumed
V sem 2000:1 success.
[bill@billstone Unix_study]$ ./ipcsem 98305 0 a
Get Sem Stat success.
Get Sem All success.
sem no [0]: [3]
sem no [1]: [2]
[bill@billstone Unix_study]$
读者可以试着连续执行sema几次,观察进程的P阻塞状态。