1.概述
SystemV信号量并不如Posix信号量那样“好用”,但相比之下它的年代更加久远,但是SystemV使用的却更加广泛(尤其是在老系统中)。在学习Posix信号量的时候,已经大概清楚了二值信号量和计数信号量是什么东西。在接触SystemV信号量之后,这里有一个新的概念叫做:计数信号量集。其实就是把信号量放入数组中,不过都用一些特别的结构封装。
1.信号量结构体
内核为每个信号量集维护一个信号量结构体,可在<sys/sem.h>找到该定义:
struct semid_ds {
struct ipc_perm sem_perm; /* 信号量集的操作许可权限 */
struct sem *sem_base; /* 某个信号量sem结构数组的指针,当前信号量集
中的每个信号量对应其中一个数组元素 */
ushort sem_nsems; /* sem_base 数组的个数 */
time_t sem_otime; /* 最后一次成功修改信号量数组的时间 */
time_t sem_ctime; /* 成功创建时间 */
};
struct sem {
ushort semval; /* 信号量的当前值 */
short sempid; /* 最后一次返回该信号量的进程ID 号 */
ushort semncnt; /* 等待semval大于当前值的进程个数 */
ushort semzcnt; /* 等待semval变成0的进程个数 */
};
打开/创建信号量
int semget(key_t key, int nsems, int oflag);
函数功能: 创建一个信号量集或访问一个已经存在的信号量集
返回值: 成功返回非负的标识符,出错返回-1
参数:
key是信号量的键值,多个进程可以通过这个键值访问同一个信号量;
nsems参数指定信号量集合中的信号量数,一般设为1,如果不创建新的信号量集,只是访问一个已经存在的集合,可以把该参数设为0,一旦创建完一个信号量集,就不能改变其中的信号量数;
oflag同open()权限位,IPC_CREAT标示创建新的信号量,如果或上IPC_EXCL,若信号量已存在则出错,如果没有或上IPC_EXCL,若信号量存在也不会出错。
操作/控制信号量
int semctl(int semid, int semnum, int cmd, … /*union semun arg */);
函数功能: 该函数用来直接控制信号量信息.也就是直接删除信号量或者初始化信号量.
返回值: 若成功,根据cmd不同返回不同的值,IPC_STAT,IPC_SETVAL,IPC_RMID返回0,IPC_GETVAL返回信号量当前值;出错返回-1.
参数:
semid标示操作的信号量集;
semnum标示该信号量集内的某个成员数组下标索引,通常取值0,也就是第一个信号量;
cmd:指定对单个信号量的各种操IPC_STAT,IPC_GETVAL,IPC_SETVAL,IPC_RMID;
struct seminfo {
int semmap; // 信号量映射里的条数,内核未使用
int semmni; // 信号量集合的最大个数
int semmns; // 在所有信号量集合里信号量个数上限
int semmnu; // 系统范围内的 undo 结构最大个数,内核未使用
int semmsl; // 一个信号量集合里信号量个数上限
int semopm; // 执行的最大操作个数
int semume; // 每个进程内 undo 结构最大个数,内核未使用
int semusz; // 结构 sem_undo 的尺寸
int semvmx; // 信号量值的上限
int semaem; // Max. value that can be recorded for
semaphore adjustment (SEM_UNDO)
};
arg: 可选参数,取决了第三个参数cmd,由此可以看见,有些成员仅仅针对某些命令,这也正是为什么这里用Union而不用Struct,可以节省空间,因为假设当前命令跟某个成员没关的时候,struct依然为这个成员分配空间。
union semun {
int val; /* SETVAL使用的值 */
struct semid_ds *buf; /* IPC_STAT、IPC_SET 使用缓存区 */
unsigned short *array; /* GETALL,、SETALL 使用的数组 */
struct seminfo *__buf; /* IPC_INFO(Linux特有) 使用缓存区 */
};
注意:该联合体没有定义在任何系统头文件中,因此得用户自己声明,centos6.5中/linux/sem.h可以找到
semid_ds 数据结构在头文件 <sys/sem.h> 有如下定义:
struct semid_ds {
struct ipc_perm sem_perm; // 所有者和权限
time_t sem_otime; // 上次执行 semop 的时间
time_t sem_ctime; // 上次更新时间
unsigned short sem_nsems; // 在信号量集合里的索引
};
struct ipc_perm {
key_t __key; // 提供给 semget()的键
uid_t uid; // 所有者有效 UID
gid_t gid; // 所有者有效 GID
uid_t cuid; // 创建者有效 UID
gid_t cgid; // 创建者有效 GID
unsigned short mode; // 权限
unsigned short __seq; // 序列号
};
*int semop(int semid, struct sembuf sops, unsigned nsops);
注释: 用来改变信号量的值,该函数是具有原子性的
功能: 打开一个信号量集合后,对其中一个或多个信号量操作(P/V加减操作)
对于struct sembuf这个结构体来说,其结构定义如下:
struct sembuf{
unsigned short sem_num; /* semaphore number */除非使用一组信号量,否则它为0
short sem_op; /* semaphore operation */p -1, v 1
short sem_flg; /* operation flags */填 0就好 SEM_NOWAIT SEM_UNDO
};
sem_num指定特定信号量的操作。
sem_op的值分为3类:
a.sem_op > 0:将值添加到semval上,对应与释放某个资源(V操作,生产操作)。
b.sem_op = 0:希望等待到semval值变为0,如果已经是0,则立即返回,否则semzcnt+1,并线程阻塞。
c.sem_op < 0:希望等待到semval值变为大于或等于|sem_op|(希望得到sem_op个数的资源,也就是sem_op的绝对值大于等于剩余的资源数量semval,如果不满足就阻塞,等到条件满足),这对应分配资源(P操作,消费操作)。如果已经满足条件,则semval减去sem_op的绝对值,否则semncnt+1并且线程投入睡眠。
nops是opstr数组中元素数目,通常取值为1。
SystemV信号量的一般编程步骤:
- 创建信号量或获得在系统中已存在的信号量
1). 调用semget().
2). 不同进程使用同一个信号量键值来获得同个信号量 - 初始化信号量
1).使用semctl()函数的SETVAL操作
2).当使用二维信号量时,通常将信号量初始化为1
3.进行信号量PV操作
1). 调用semop()函数
2). 实现进程之间的同步和互斥
4.如果不需要该信号量,从系统中删除
1).使用semctl()函数的IPC_RMID操作
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#define DEF_SYSTEMV_SEM 1
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int init_sem(int sem_id,int init_value) {
union semun sem_union;
sem_union.val=init_value;
if (semctl(sem_id,0,SETVAL,sem_union)==-1) {
perror("Error Sem init");
exit(1);
}
return 0;
}
int destory_sem(int sem_id){
union semun sem_union;
if (semctl(sem_id,0,IPC_RMID,sem_union)==-1){
perror("Error Sem delete");
exit(1);
}
return 0;
}
int sem_wait(int sem_id) {
struct sembuf sem_buf;
sem_buf.sem_num=0;//信号量编号
sem_buf.sem_op=-1;//P操作
sem_buf.sem_flg=SEM_UNDO;//系统退出前未释放信号量,系统自动释放
if (semop(sem_id,&sem_buf,1)==-1) {
perror("Error sem_wait");
exit(1);
}
return 0;
}
int sem_signal(int sem_id) {
struct sembuf sem_buf;
sem_buf.sem_num=0;
sem_buf.sem_op=1;//V操作
sem_buf.sem_flg=SEM_UNDO;
if (semop(sem_id,&sem_buf,1)==-1) {
perror("Error sem_signal");
exit(1);
}
return 0;
}
int main() {
pid_t pid;
#if DEF_SYSTEMV_SEM
key_t sem_key=ftok(".",'AAAA');
int sem_id=semget(sem_key,1,0666|IPC_CREAT);
init_sem(sem_id,1);
#endif
if ((pid=fork())<0) {
perror("fork error!\n");
exit(1);
} else if (pid==0) {
#if DEF_SYSTEMV_SEM
sem_wait(sem_id);
#endif
printf("Child befor sleep...\n");
sleep(20);
printf("Child after sleep...\n");
#if DEF_SYSTEMV_SEM
sem_signal(sem_id);
#endif
exit(0);
} else {
#if DEF_SYSTEMV_SEM
sem_wait(sem_id);
#endif
printf("Parent befor sleep...\n");
sleep(20);
printf("Parent after sleep...\n");
#if DEF_SYSTEMV_SEM
sem_signal(sem_id);
waitpid(pid,0,0);
destory_sem(sem_id);
#endif
exit(0);
}
}
运行结果:
$ ./a.out
Parent befor sleep…
Parent after sleep…
Child befor sleep…
Child after sleep…
可以看到是父进程执行完后才执行子进程。
如果把条件编译宏设为0 “#define DEF_SYSTEMV_SEM 0”
取消System V 信号量,运行结果为
$ ./a.out
Parent befor sleep…
Child befor sleep…
Parent after sleep…
Child after sleep…
可以看到父子进程之间临界区代码执行已经乱序
通过多个信号量解决经典的生产者—消费者的问题:
单个信号量单单依靠一个信号量是无法实现的,生产者-消费者模型中,两个进程共用临界资源,所以它们首先互斥;此外,如果缓冲区满,生产者是不能生产的,所以生产者进程受到消费者进程的制约;如果缓冲区空,消费者是不能消费的,所以消费者进程受到生产者进程的制约。但你用一个信号量去控制同步的话,因为V(n)没有条件,所以生产者进程实际上不受制约,永不阻塞,只有消费者进程受到生产者的制约,所以,必须要用到多个信号量,才能解决问题。
下一篇博客 systemV信号量实现进程间共享内存同步。