一、信号量IPC原理
信号量通讯机制主要用来实现进程间同步,避免并发访问共享资源。信号量值可以表示系统可用资源的个数。例如,可以使用信号量来标识一个缓冲区可用空间大小(假定缓冲区大小为256字节),在缓冲区使用前,该缓冲区没有任何内容,可以资源256,即可以初始化信号量为256,每向缓冲区写入一个字节,信号量的值自动减1,当信号量的值减为0时即标识缓冲区满资源暂不开用。每从缓冲区中读出一个字节,信号量的值自动加1,如果信号量的值为256,则表示缓冲区中没有内容,不可读。
通常所说的创建一个信号量实际上是创建了一个信号量集合,在这个信号量集合中,可以能有多个信号量。整个信号量集合由以下部分组成。
1.信号量集合数据结构
在此数据结构中定义了整个信号量集合的基本属性。如此信号量的访问权限、指针、最近修改时间和队列中信号量队列信息,其定义如下。
// come from /usr/include/linux/sem.h
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */ // 访问权限
__kernel_time_t sem_otime; /* last semop time */ // 最近semop时间
__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 */ // undo队列
unsigned short sem_nsems; /* no. of semaphores in array */ //
};
2.每一个信号量结构
信号量集合使用指针指向一个由数组组成的信号量单元,在此信号量单元中存储了各信号量的值,其数据结构定义如下:
struct sem {
int semval; // 信号量的值
int sempid; // 最近一个操作的进程号PID
};
二、Linux信号量管理操作
1.创建信号量
创建一个信号量集合的函数为semget(),其函数声明如下:
// come from /usr/include/sys/sem.h
/* Get semaphore. */
extern int semget(key_t key, int nsems, int semflg);
第1个参数为key值,有ftok()函数产生,关于ftok函数在我的博客《System V IPC基础》一节有说明;
第2个参数nsems为创建的信号量个数,以数组的方式存储;
第3个参数semflg用来标识信号量集合的权限,其最终权限为当前进程的umask值与设置值perm相与所得,即最终值为perm&~umask。其高位包含以下项:
#define IPC_CREAT 01000 /* Create key if key does not exist. */ // 如果key不存在,则创建,存在,返回ID
#define IPC_EXCL 02000 /* Fail if key exists. */ // 如果key存在返回失败
#define IPC_NOWAIT 04000 /* Return error on wait. */ // 如果需要等待,直接返回错误
下面给出创建信号量的示例代码:
#include <sys/ipc.h>
#include <sys/sem.h>
key_t key = ftok("/", 3);
int semid = semget(key, 3, IPC_CREAT | 0666); // 创建信号量集合,包含3个信号量
2.控制信号量
semctl()函数可以对一个信号量集合以及集合中的某个或某几个信号量进行操作,该函数声明如下:
// come from /usr/include/sys/sem.h
/* Semaphore control operation. */
extern int semctl (int semid, int semnum, int cmd, ...);
该函数最多可以有4个参数,也有可能只有3个参数。
第1个参数semid为要操作的信号量集合标识符,该值由semget()函数返回;
第2个参数为集合中信号量的编号。如果标识某个信号量,此值为该信号量的下标(从0到n-1);如果操作整个信号量集合,此参数无意义;
第3个参数为要执行的操作。
a.操作整个信号量集合
这些操作定义在/usr/include/linux/ipc.h文件中,如下所示:
// come from /usr/include/linux/ipc.h
/*
* Control commands used with semctl, msgctl and shmctl
* see also specific commands in sem.h, msg.h and shm.h
*/
#define IPC_RMID 0 /* remove resource */ // 删除信号量
#define IPC_SET 1 /* set ipc_perm options */ // 设置ipc_perm参数
#define IPC_STAT 2 /* get ipc_perm options */ // 获取ipc_perm参数
#define IPC_INFO 3 /* see ipcs */ // 获取限制信息
b.操作集合中的某个或某几个信号量
如果是对信号量集合中的某个或某几个信号量操作,则包括以下命令:
#define GETVAL 12 /* get semval */ // 获取信号量的值
获取当前信号量的值。使用此命令时,第2个参数为信号量编号,不需要第4个参数。如果执行成semctl()将返回当前信号量的值,否则返回-1。
#define GETALL 13 /* get all semval's */ // 获取所有信号量的值
获取所有信号量的值,使用此命令时,第2个参数为0,第4个参数为存储所有信号量值内存空间首地址。如果执行成semctl()将返回0,否则返回-1。
#define SETVAL 16 /* set semval */ // 设置当前信号量的值
设置当前信号量的值。使用此命令时,第2个参数为信号量的编号,第4个参数为欲设置的值。如果执行成semctl()将返回0,否则返回-1。
#define SETALL 17 /* set all semval's */ // 设置所有信号量的值
设置所有信号量的值。使用此命令时,第2个参数为0,第4个参数为欲设置的信号量值所在数组首地址。如果执行成semctl()将返回0,否则返回-1。
#define GETPID 11 /* get sempid */ // 获取信号量拥有者进程pid值
获取信号量拥有者进程pid值。使用此命令时,第2个参数为0,第4个参数无效。如果执行成功semctl()将返回拥有信号量进程pid值,否则返回-1。
#define GETNCNT 14 /* get semncnt */ // 获取等待信号量的值递增的进程数
获取等待信号量的值递增的进程数。使用此命令时,第2个参数为0,不需要第4个参数。如果执行成semctl()将返回等待信号量的值递增的进程数,否则返回-1。
#define GETZCNT 15 /* get semzcnt */ // 获取等待信号量的值递减的进程数
获取等待信号量的值递减的进程数。使用此命令时,第2个参数为0,不需要第4个参数。如果执行成semctl()将返回等待信号量的值递减的进程数,否则返回-1。
第4个参数根据第3个参数的具体操作设置。其类型为senum的联合体,具体定义如下:
// come from /usr/include/linux/sem.h
/* arg for semctl system calls. */
union semun {
int val; /* value for SETVAL */ // SETVAL的值
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */ // 用于IPC_STAT和IPC_SET命令
unsigned short *array; /* array for GETALL & SETALL */ // 用于GETAL和SETALL命令
struct seminfo *__buf; /* buffer for IPC_INFO */ // 用于IPC_INFO命令
void *__pad;
};
下面给出初始化整个信号量集合的示例代码:
#include <sys/ipc.h>
#include <sys/sem.h>
key_t key = ftok("/", 3);
int semid = semget(key, 3, IPC_CREAT | 0666); // 创建信号量集合,包含3个信号量
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
short semarray[3] = {0, 1, 1}; // 信号量的值分别设置为0, 1, 1
arg.array = semarray;
semctl(semid, 0, SETALL, arg);
下面给出初始化一个信号量的示例代码:
#include <sys/ipc.h>
#include <sys/sem.h>
key_t key = ftok("/", 3);
int semid = semget(key, 1, IPC_CREAT | 0666); // 创建信号量集合,包含1个信号量
semctl(semid, 0, SETVAL, 1);
3.信号量操作
semop()函数用来操作信号量集合,此函数声明如下:
/* Operate on semaphore. */
extern int semop (int semid, struct sembuf *sops, size_t nsops);
第1个参数为要操作的信号量集合ID;
第2个参数为struct sembuf结构的变量,其定义如下:
/* semop system calls takes an array of these. */
struct sembuf {
unsigned short sem_num; /* semaphore index in array */ // 信号量下标
short sem_op; /* semaphore operation */ // 信号量操作
short sem_flg; /* operation flags */ // 操作标识
};
此结构体有3个成员变量,具体说明如下:
(1)sem_num为操作信号量的编号;
(2)sem_op位作用于信号量的操作:该值为正整数表示增加信号量的值(如果为1,表示在原来的基础上加1,如果为3,表示在原来的基础上加3);如果为负整数表示减小信号量的值;如果为0,表示对信号量的当前值是否为0的测试。
(3)sem_flg为操作标识,可选以下各值:
IPC_NOWAIT:在对信号量集合的操作不能执行的情况下,调用立即返回,对某信号量操作,即使其中一个操作失败,也不会导致修改集合中的其他信号量。
SEM_UNDO:当进程退出后,该进程对sem进行的操作将被撤销。
第3个参数nsops为欲操作信号量的数目。
下面给出对信号量操作的示例代码:
#include <sys/ipc.h>
#include <sys/sem.h>
key_t key = ftok("/", 3);
int semid = semget(key, 2, IPC_CREAT | 0666); // 创建信号量集合,包含2个信号量
union semun arg;
short semarray[2] = {1, 0}; // 信号量的值分别设置为1, 0
arg.array = semarray;
semctl(semid, 0, SETALL, arg);
struct sembuf sops[2];
sops[0].sem_num = 0;
sops[0].sem_op = -1; /*P操作*/
sops[0].sem_flg = 0;
sops[0].sem_num = 1;
sops[0].sem_op = 1; /*V操作*/
sops[0].sem_flg = 0;
semop(semid, sops, 2);
注意:对信号量进行P操作时,如果P操作之后信号量的值小于0则会阻塞当前进程,直到有其他进程对当前信号量进行V操作,且信号量的值要大于等于P操作将要减去的值。
三、示例
下面通过信号量通讯机制来实现生产者消费者问题:
customer.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
int sem_id;
void init()
{
key_t key;
key = ftok(".", 's');
sem_id = semget(key, 2, IPC_CREAT | 0644);
}
int main(int argc, char *argv[])
{
init();
struct sembuf sops[2];
// 对0号信号量P操作(消费产品)
sops[0].sem_num = 0;
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
// 对1号信号量V操作(产品消费后仓库空间增加)
sops[1].sem_num = 1;
sops[1].sem_op = 1;
sops[1].sem_flg = 0;
init();
printf("this is customer\n");
while(1)
{
printf("\nbefore consume:\n");
printf("productor is %d\n", semctl(sem_id, 0, GETVAL));
printf("space is %d\n", semctl(sem_id, 1, GETVAL));
semop(sem_id, (struct sembuf *)&sops[0], 1); //get the productor to cusume
printf("now consuming......\n");
semop(sem_id, (struct sembuf *)&sops[1], 1); //now tell the productor can bu produce
printf("\nafter consume\n");
printf("products number is %d\n", semctl(sem_id, 0, GETVAL));
printf("space number is %d\n", semctl(sem_id, 1, GETVAL));
sleep(3);
}
}
productor.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
int sem_id;
void init()
{
key_t key;
int ret;
unsigned short sem_array[2];
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
key = ftok(".", 's');
// 申请两个信号量 0:代表产品个数 1:代表仓库空间
sem_id = semget(key, 2, IPC_CREAT | 0644);
sem_array[0] = 0; // 产品数量初始值为0
sem_array[1] = 100; // 仓库空间初始值为100
arg.array = sem_array;
ret = semctl(sem_id, 0, SETALL, arg);
if (ret == -1) {
printf("SETALL failed (%d)\n", errno);
}
printf("productor init is %d\n",semctl(sem_id, 0, GETVAL));
printf("space init is %d\n\n",semctl(sem_id, 1, GETVAL));
}
void del()
{
semctl(sem_id, IPC_RMID, 0);
}
int main(int argc,char *argv[])
{
struct sembuf sops[2];
// 对0号信号量V操作(生产产品)
sops[0].sem_num = 0;
sops[0].sem_op = 1;
sops[0].sem_flg = 0;
// 对1号信号量P操作(生产产品后仓库空间减少)
sops[1].sem_num = 1;
sops[1].sem_op = -1;
sops[1].sem_flg = 0;
init();
printf("this is productor\n");
while(1)
{
printf("\nbefore produce:\n");
printf("productor number is %d\n", semctl(sem_id, 0, GETVAL));
printf("space number is %d\n",semctl(sem_id, 1, GETVAL));
semop(sem_id, (struct sembuf *)&sops[1], 1); //get the space to instore the productor
printf("now producing......\n");
semop(sem_id, (struct sembuf *)&sops[0], 1); //now tell the customer can bu cusume
printf("\nafter produce\n");
printf("spaces number is %d\n", semctl(sem_id, 1, GETVAL));
printf("productor number is %d\n",semctl(sem_id, 0, GETVAL));
sleep(4);
}
del();
}
生产者端先运行,运行结果如下:
消费者端运行结果如下: