进程间的通信不仅仅包括数据交流 , 也包括过程控制 .
信号量是一个可以用来控制进程存储共享资源的计数器 , 它可以是跟踪共享资源的生产和消费的计数器 , 也可以是协调资源的生产者和消费者之间的同步器 , 还可以是控制生产进程和消费进程的互斥开关 .
信号量简介
操作系统通过信号量和 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阻塞状态。