System V信号量
1. System V IPC
概述
以下三种类型的IPC合称为System V IPC:
- System V信号量
- System V消息队列
- System V共享内存
System V IPC在访问它们的函数和内核为它们维护的信息上有一些类似点,主要包括:
- IPC键和ftok函数
- ipc_perm结构
- 创建或打开时指定的用户访问权限
- ipcs和ipcrm命令
下表汇总了所有System V IPC函数。
| 信号量 | 消息队列 | 共享内存
- | - | - | -
头文件 | sys/sem.h | sys/msg.h | sys/shm.h
创建或打开IPC的函数 | semget | msgget | shmget
控制IPC操作的函数 | semctl | msgctl | shmctl
IPC操作函数 | semop | msgsnd
msgrcv
| shmat
shmdt
IPC键和ftok函数
三种类型的System V IPC都使用IPC键作为它们的标识,IPC键是一个key_t类型的整数,该类型在sys/types.h
中定义。
IPC键通常是由ftok
函数赋予的,该函数把一个已存在的路径名pathname和一个非0整数id组合转换成一个key_t值,即IPC键。
#include <sys/ipc.h>
//成功返回IPC键,失败返回-1
key_t ftok(const char *pathname, int id);
参数说明:
- pathname在是程序运行期间必须稳定存在,不能反复创建与删除
- id不能为0,可以是正数或者负数
ipc_perm结构
内核给每个IPC对象维护一个信息结构,即struct ipc_perm
结构,该结构及System V IPC函数经常使用的常值定义在sys/ipc.h
头文件中。
struct ipc_perm
{
uid_t uid; //owner's user id
gid_t gid; //owner's group id
uid_t cuid; //creator's group id
gid_t cgid; //creator's group id
mode_t mode; //read-write permissions
ulong_t seq; //slot usage sequence number
key_t key; //IPC key
};
创建与打开IPC对象
创建或打开一个IPC对象使用相应的xxxget函数,它们都有两个共同的参数:
- 参数key,key_t类型的IPC键
- 参数oflag,用于指定IPC对象的读写权限(ipc_perm.mode),并选择是创建一个新的IPC对象还是打开一个已存在的IPC对象
对于参数key,应用程序有两种选择:
- 调用ftok,给它传pathname和id
- 指定key为
IPC_PRIVATE
,这将保证会创建一个新的、唯一的IPC对象,但该标志不能用于打开已存在的IPC对象,只能是新建
对于参数oflag,如上所述,它包含读写权限、创建或打开这两方面信息:
- 可以指定
IPC_CREAT
标志,其含义和Posix IPC的O_CREAT一样 - 还可以设置为下表所示的常值来指定读写权限
ipcs和ipcrm命令
- 由于System V IPC的三种类型不是以文件系统路径名标识的,因此无法使用ls和rm命令查看与删除它们
- ipcs和ipcrm分别用于查看与删除系统中的System V IPC
usage : ipcs -asmq -tclup
ipcs [-s -m -q] -i id
ipcs -h for help.
usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
[-Q msgkey] [-M shmkey] [-S semkey] ... ]
2. System V信号量
计数信号量集
我们已经知道了Posix信号量采用计数信号量,System V信号量在此基础上增加了一级复杂度,它采用计数信号量集,计数信号量集是由一个或多个计数信号量构成的集合。
对于系统中的每个信号量集,内核都维护一个struct semid_ds
信息结构,它定义在sys/sem.h
头文件中。
struct semid_ds
{
struct ipc_perm sem_perm;
struct sem *sem_base; //指向信号量集的指针
ushort sem_nsems; //信号量集中的信号量个数
time_t sem_otime; //上一次调用semop的时间
time_t sem_ctime; //创建时间或上一次以IPC_SET调用semctl的时间
};
其中,sem_base是指向信号量集的指针,信号量集中的每个成员都对应一个struct sem结构:
struct sem
{
ushort_t semval; //信号量的值
short sempid; //上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
ushort_t semncnt; //等待semval变为大于当前值的线程数
ushort_t semzcnt; //等待semval变为0的线程数
};
semget
semget用于创建一个新的信号量集或打开一个已存在的信号量集。
- nsems参数指定集合中的信号量个数,如果是打开一个已存在的信号量集,就把该参数设为0
- oflag参数可设置为IPC_CREAT,以及SEM_R和SEM_A指定的访问权限,如果是打开一个已存在的信号量集,就把该参数设为0
- 成功时返回一个称为信号量标识符的非负整数,semop和semctrl函数将使用它
//成功返回信号量标识符,失败返回-1
int semget(key_t key, int nsems, int oflag);
当实际操作为创建一个新的信号量集时,相应semid_ds结构中与集合中每个信号量关联的struct sem结构并不初始化,而是在以SETVAL或SETALL命令调用semctrl时初始化的。
也就是说,创建一个新的System V信号量集(semget)并将它初始化(semctl)需要两次函数调用,这是System V信号量的一个致命缺陷。
semop
使用semget打开一个信号量集后,对其中一个或多个信号量值的操作就使用semop函数。
//成功返回0,失败返回-1
int semop(int semid, struct sembuf *ops, size_t nops);
- semid指定待操作的信号量集
- nops为集合中的信号量个数
- ops指向一个struct sembuf类型的结构数组,该数组中的每个元素给目标信号量集中某个特定的信号量指定sem_op操作,这个特定的信号量由sem_num指定
我们只保证sembuf含有以下三个成员,不保证这三个成员的顺序,也不保证还有其他成员,因此sembuf数组元素必须采用如ops[0].sem_num = 0所示的方法进行初始化。
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
sem_op指定的操作有三类:
- sem_op > 0:操作内容为semval += sem_op,这对应于释放某个信号量控制的资源
- sem_op = 0:调用者阻塞等待直到semval变为0
- sem_op < 0:调用者阻塞等待直到semval >= abs(sem_op),调用者阻塞等待直到semval>这对应于分配资源
sem_flg可设置的值有:0、IPC_NOWAIT、SEM_UNDO,一般使用0,对于其余两个值:
- IPC_NOWAIT用于给信号量集中某个特定信号量设置非阻塞标志
- SEM_UNDO为System V信号量特有的复旧机制
semctl
semctl函数对一个信号量集执行各种控制操作。
//成功根据cmd返回相应的非负值,失败返回-1
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
- semid指定待控制的信号量集
- semnum指定信号量集内的某个成员,仅在cmd为GETVAL、SETVAL、GETNCNT、GETZCNT和GETPID时使用
- cmd指定控制命令
- arg是可选的,取决于cmd的具体值
第四个参数arg是可选的,取决于cmd的值,当需要用到该参数时,应用程序必须按如下结构定义union semun:
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 (Linux-specific) */
};
定义如下术语用于后续cmd说明:
- semval:信号量的当前值
- sempid:上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
- semncnt:等待semval变为大于当前值的线程数
- semzcnt:等待semval变为0的线程数
System V支持下列cmd值,除非特殊说明,否则成功时返回值均为0。
cmd | 说 明 |
---|---|
GETVAL | 把semval的当前值作为函数返回值返回,它是一个unsigned short类型的整数 |
SETVAL | 把semval的值设为arg.val |
GETPID | 把sempid的当前值作为函数返回值返回 |
GETNCNT | 把semncnt的当前值作为函数返回值返回 |
GETZCNT | 把semzcnt的当前值作为函数返回值返回 |
GETALL | 返回信号量集内每个成员的semval值,这些值通过arg.array返回,arg.array需由调用者分配内存 |
SETALL | 设置信号量集内每个成员的semval值,这些值通过arg.array指定,此时第二个参数semnum设为0即可 |
IPC_STAT | 返回信号量集当前的semid_ds结构,该结构通过arg.buf返回,arg.buf需由调用者分配内存,可以用该命令获得信号量集中的信号量个数 |
IPC_SET | 设置信号量集对应semid_ds结构中的sem_perm.uid、sem_perm.gid和sem_perm.mode,设置的值来自arg.buf |
IPC_RMID | 删除由semid指定的信号量集,此时第二个参数semnum设为0即可 |
其中,前五个命令针对的都是信号量集semid中由semnum指定的信号量。
3. 测试程序
代码实现
semcreate.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_FILE "/home/delphi/ftok.file"
#define FTOK_ID 1
#define SEM_MODE 0666
/*
#define SEM_MODE_OWNER SEM_R | SEM_A
#define SEM_MODE_GROUP (SEM_R >> 3) | (SEM_A >> 3)
#define SEM_MODE_OTHER (SEM_R >> 6) | (SEM_A >> 6)
#define SEM_MODE (SEM_MODE_OWNER | SEM_MODE_GROUP | SEM_MODE_OTHER)
*/
int main()
{
int nsems = 3;
int oflag = IPC_CREAT | SEM_MODE;
key_t key = ftok(FTOK_FILE, FTOK_ID);
int semid = semget(key, nsems, oflag);
if (semid >= 0)
{
printf("semcreate success, semid = %d\n", semid);
}
return 0;
}
这里遇到个问题,SEM_MODE一开始是按注释部分定义的,但man semget给出的三个头文件都已经包含了,编译时却报错,提示SEM_R和SEM_A未定义,不知道为什么,只能用0666定义了。
semrmid.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_FILE "/home/delphi/ftok.file"
#define FTOK_ID 1
int main()
{
key_t key = ftok(FTOK_FILE, FTOK_ID);
int semid = semget(key, 0, 0);
semctl(semid, 0, IPC_RMID);
return 0;
}
semsetvalues.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_FILE "/home/delphi/ftok.file"
#define FTOK_ID 1
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 (Linux-specific) */
};
int main(int argc, char **argv)
{
key_t key;
int semid;
int nsems;
unsigned short *semvals;
union semun arg;
struct semid_ds seminfo;
int i;
/* 打开semcreate创建的信号量集 */
key = ftok(FTOK_FILE, FTOK_ID);
semid = semget(key, 0, 0);
/* 获得信号量集中的信号量个数 */
arg.buf = &seminfo;
semctl(semid, 0, IPC_STAT, arg);
nsems = arg.buf->sem_nsems;
/* 设置信号量集中每个信号量的值 */
semvals = (unsigned short *)calloc(nsems, sizeof(unsigned short));
for (i = 0; i < nsems; i++)
{
semvals[i] = atoi(argv[i + 1]); //通过命令行参数分别指定集合中每个信号量的值
}
arg.array = semvals;
semctl(semid, 0, SETALL, arg);
return 0;
}
semgetvalues.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_FILE "/home/delphi/ftok.file"
#define FTOK_ID 1
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 (Linux-specific) */
};
int main(int argc, char **argv)
{
key_t key;
int semid;
int nsems;
unsigned short *semvals;
union semun arg;
struct semid_ds seminfo;
int i;
/* 打开semcreate创建的信号量集 */
key = ftok(FTOK_FILE, FTOK_ID);
semid = semget(key, 0, 0);
/* 获得信号量集中的信号量个数 */
arg.buf = &seminfo;
semctl(semid, 0, IPC_STAT, arg);
nsems = arg.buf->sem_nsems;
/* 获得信号量集中每个信号量的值 */
semvals = (unsigned short *)calloc(nsems, sizeof(unsigned short));
arg.array = semvals;
semctl(semid, 0, GETALL, arg);
for (i = 0; i < nsems; i++)
{
printf("semvals[%d] = %d\n", i, semvals[i]);
}
return 0;
}
semops.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_FILE "/home/delphi/ftok.file"
#define FTOK_ID 1
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 (Linux-specific) */
};
int main(int argc, char **argv)
{
key_t key;
int semid;
int nsems;
union semun arg;
struct semid_ds seminfo;
struct sembuf *semops;
int i;
/* 打开semcreate创建的信号量集 */
key = ftok(FTOK_FILE, FTOK_ID);
semid = semget(key, 0, 0);
/* 获得信号量集中的信号量个数 */
arg.buf = &seminfo;
semctl(semid, 0, IPC_STAT, arg);
nsems = arg.buf->sem_nsems;
/* 对信号量集中的所有信号量进行相同的semop操作 */
semops = (struct sembuf *)calloc(nsems, sizeof(struct sembuf));
for (i = 0; i < nsems; i++)
{
semops[i].sem_num = i;
semops[i].sem_op = atoi(argv[1]); //操作类型由命令行参数指定,>0, 0, <0
semops[i].sem_flg = 0;
}
semop(semid, semops, nsems);
return 0;
}
运行测试
- 运行semcreate,通过运行前后的ipcs -s,可以确认信号量集创建成功
- 运行semsetvalues,将三个信号量的值分别设为1、2、3
- 然后运行semgetvalues,打印信息和我们设置的值一致,符合预期
- 运行semops,并指定sem_op > 0
- 然后运行semgetvalues,打印信息显示每个信号量的值都增加了1,符合预期
- 运行semrmid,通过运行前后的ipcs -s,可以确认信号量集已经从系统删除