0. 前言
通过前面几篇博文,大概清楚这些IPC 的设计都是为了进程间数据的共享而设计的,例如消息队列、共享内存、命名管道(FIFO),本文将要介绍的信号量(semaphore) 跟这些有些区别,更确切说它是为了共享数据的访问服务,它是一个计数器,是由狄克斯特拉提出,并通过PV(通过&释放,是荷兰文缩写)操作对信号量进行控制。
信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。注意,信号量的值仅能由PV操作来改变。
一般来说,信号量S>=0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S<0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。
1. 创建信号量
通过semget 来创建信号量或获取一个已有的信号量,函数原型:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数:
- key 信号量的关键字,可以通过ftok() 创建,详细看 进程间通信——消息队列
- nsems 信号量的数目。如果是创建新的信号量,必须要指定nsems。如果是引用现有的信号量,nsems指定为0。
- shmflg 有两个选项,IPC_CREAT 表示内核中没有此信号量则创建它。IPC_EXCL 当和IPC_CREAT一起使用时,如果信号量已经存在,则返回错误。
返回值:成功返回标识符,否则返回-1。通过errno和perror函数可以查看错误信息。
通过nsems 可以看出,创建一个信号量,里面可能包含多个信号量值。该信号量就相当于一个集合。该值表明有多少个共享资源单位可供共享应用。下面semctl 也是通过nsems 中的某一个进行操作。
如果nsems 设为1,该信号量又被称为二元信号量(binary semaphore)。
2. 控制信号量
同进程间通信——消息队列 和 进程间通信——共享内存,信号量标识符信息被记录在结构体semid_ds中:
struct semid_ds {
struct ipc_perm sem_perm; /* operation permission struct */
time_t sem_otime; /* last semop() time */
time_t sem_ctime; /* last time changed by semctl() */
unsigned short sem_nsems; /* number of semaphores in set */
...
}
通过semctl 函数对信号量进行控制或对一些属性进行修改,函数原型:
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, .../* union semun arg */);
参数:
- semid 信号的标识符
- semnum 指定nsems 个信号量中的一个。semnum值在0 和nsems-1 之间,包括0 和 nsems-1。
- cmd 有10个命令,其中有5中命令是针对一个特定的信号量值的(semnun 指定的)。
- arg 第4个参数,详细见下面分析。
返回值:若成功返回0(GETALL 等GET 命令详细看下面),若出错,则返回-1。通过errno 和perror 函数可以查看错误信息。
首先来看下第 4 个参数 arg,是否使用取决于所请求的命令,如果使用该参数,则其类型是semun,它是多个命令特定参数的联合,其中包括semid_ds。如下:
union semun {
int val; /* value for cmd SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short int *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
}
注意,这个选项参数是一个联合,而非指向联合的指针。
下面来看下cmd 的10 个命令:
- IPC_STAT 获取semid_ds结构,并存储在由arg.buf 指向的结构中。
- IPC_SET 按arg.buf 指向的结构中的值,设置与信号量相关的sem_perm.uid、sem_perm.gid 和sem_perm.mode 字段。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
- IPC_RMID 删除信号量。这种删除是立即发生的。删除时仍在使用此信号量的其他进程,在它们下次试图对此信号量进行操作时,将出错返回EIDRM。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
- GETVAL 返回成员semnum 的semval 值
- SETVAL 设置成员semnum 的semval值,该值由 arg.val 指定。
- GETPID 返回成员semnum 的sempid 值。
- GETNCNT 返回成员semnum 的semncnt 值
- GETZCNT 返回成员semnum 的semzcnt 值
- GETALL 取出信号量集合中所有信号量的值。这些值存储在 arg.array 指向的数组中。
- SETALL 将该集合中所有信号量的值设置成arg.array 指向的数组中的值。
对于出GETALL 以外的所有GET 命令,semctl 函数都返回相应值。其他命令,若成功返回0,若出错,返回-1。
3. 信号量PV操作
通过semop 进行 PV 的操作,函数原型:
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
参数:
- semid 信号量的标识符
- semoparray 一个指针,指向一个由sembuf 结构表示的信号量操作数组。
- nops 规定semoparray 中操作的数量(元素个数)
返回值:若成功,返回0;若失败,返回-1。通过errno和perror函数可以查看错误信息。
sembuf 的数据结构:
struct sembuf {
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
}
sem_op值可以是负值、0 或正值。
- 最易于处理的情况是sem_op 为正值。这对应于进程释放的占用的资源数。sem_op 值会加到信号量上。如果指定了undo 标志,则也从该进程的此信号量调整中减去sem_op。
- 若sem_op 为负值,则表示要获取由该信号量控制的资源。
如信号量的值大于等于sem_op的绝对值(具有所需的资源),则从信号量值减去sem_op的绝对值。这能保证信号量的结果值大于0。如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。
如果信号量值小于sem_op的绝对值(资源不能满足要求),则使用下列条件:
1)若指定了IPC_NOWAIT,则semop 函数返回EAGAIN。
2)若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至下列事件之一发生:
a、此信号量值变成大于等于sem_op的绝对值(即某个进程已经释放了某些资源)。此信号量的semncnt值减1(因为已结束 等待),并且从信号量值中减去sem_op 的绝对值。
如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。
b、从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。
c、进程捕捉到一个信号,从此案好处理程序返回,在这种情况下,此信号量的semncnt值减1(因为调用进程不再等待),并 且函数出错返回EINTR。
- 若sem_op 为0,这表示调用进程希望等待到该信号值变0.
如果信号量值当前是0,则此函数立即返回。
如果信号量值非0,则使用下列条件:
1)若指定了IPC_NOWAIT,则出错返回EAGAIN。
2)若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起,直至下面事件发生:
a、此信号量值变成0。此信号量的semzcnt值减1(因为调用进程已结束等待)。
b、从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。
c、进程捕捉到一个信号,并从信号处理程序返回。在这种情况下,此信号量的semzcnt值减1(因为调用进程不再等待),并 且函数出错返回EINTR。
4. 信号量、记录所和互斥的时间比较
如果在多个进程间共享一个西苑,可以使用3种技术中的一种来协调访问。可以使用映射到两个进程地址空间中的信号量、记录所或者互斥量。
若使用信号量,则先创建一个包含一个成员的信号量集合,然后将该信号量值初始化为1。为了分配资源,以sem_op 为-1 调用semop。为了释放资源,以sem_op 为+1 调用semop。对每个操作都指定SEM_UNDO,以处理在未释放资源条件下进程终止的情况。
若使用记录所,则先创建一个空文件,并且用该文件的第一个字节作为锁字节。为了分配资源,先对该字节获得一个写锁。释放资源时,则对该字节解锁。记录所的性质保证了当一个锁的持有者进程终止时,内核会自动释放该锁。
若使用互斥,需要所有的进程将相同的文件映射到它们的地址空间里,并且使用PTHREAD_PROCESS_SHARED互斥量属性在文件的相同偏移处初始化互斥量。为了分配资源,对互斥量加锁。为了释放锁,解锁互斥量。如果一个进程没有释放互斥量而终止,回复将是非常困难的,除非使用鲁棒互斥量。
下面是在每一种情况下资源都被分配、释放1 000 000次,由3个不同的进程执行的总计(单位秒):
操作 | 用户 | 系统 | 时钟 |
---|---|---|---|
带undo的信号量 | 0.50 | 6.08 | 7.55 |
建议性记录锁 | 0.51 | 9.06 | 4.38 |
共享存储中的互斥量 | 0.51 | 0.40 | 0.25 |
在Linux 上,记录锁比信号量快,但共享存储中的互斥量的性能比信号量和记录锁都要优越。
如果单一资源加锁,并且不需要信号量的所有花哨功能,那么记录锁将比信号量要好。原因是它使用起来更简单、速度更快,当进程终止时系统会管理遗留下来的锁。
5. 实例
在 共享内存 中的例子,test_write 每2秒写一次共享内存,而test_read 是每1 秒读一次共享内存,所以,相同的数据在test_read 会读两次。稍微变化一次,test_write 占用一个信号量,在写共享内存的时候P 信号量(占用、通过),在写完的时候V 信号量(释放)。在test_read 同样使用该信号量,那么虽然1 秒读一次共享内存,但是test_write 不释放,test_read只能睡眠等待。
test_shm.h
#ifndef __TEST_SEMPHORE_INCLUDE__
#define __TEST_SEMPHORE_INCLUDE__
//include this header file for shared memory
#include <sys/sem.h>
#define sem_key "sem_key"
typedef struct sembuf sembuf_t;
int create_sem();
int get_sem();
int remove_sem(int semid);
int p(int semid);
int v(int semid);
#endif //#ifndef __TEST_SEMPHORE_INCLUDE__
test_shm.c
#include <stdio.h>
#include "test_shm.h"
static int create_shm_common(int flags)
{
printf("create shared memory\n");
key_t key = ftok(shm_key, 's');
if (key == -1) {
perror("ftok error.\n");
return -1;
}
printf("key is 0x%x\n", key);
int shmid = shmget(key, SHARE_BUF, flags);
if (shmid == -1) {
perror("shmget error.\n");
return -1;
}
printf("shmid is %d\n", shmid);
return shmid;
}
int create_shm()
{
return create_shm_common(IPC_CREAT /*| IPC_EXCL */| 0644);
}
int get_shm()
{
return create_shm_common(IPC_CREAT | 0644);
}
int remove_shm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl error.\n");
return -1;
}
return 0;
}
test_sem.h
#ifndef __TEST_SEMPHORE_INCLUDE__
#define __TEST_SEMPHORE_INCLUDE__
//include this header file for shared memory
#include <sys/sem.h>
#define sem_key "sem_key"
typedef struct sembuf sembuf_t;
int create_sem();
int get_sem();
int remove_sem(int semid);
int p(int semid);
int v(int semid);
#endif //#ifndef __TEST_SEMPHORE_INCLUDE__
test_sem.c
#include <stdio.h>
#include <errno.h>
#include "test_sem.h"
int create_sem()
{
printf("create semphore\n");
key_t key = ftok(sem_key, 's');
if (key == -1) {
perror("ftok error.\n");
return -1;
}
printf("key is 0x%x\n", key);
int semid = semget(key, 1, IPC_CREAT /*| IPC_EXCL */| 0644);
if (semid == -1) {
if (errno == EEXIST) {
semid = semget(key, 1, 0);
if (semid != -1)
return semid;
}
perror("semget error.\n");
return -1;
}
printf("semid is %d\n", semid);
return semid;
}
int get_sem()
{
printf("get semphore\n");
key_t key = ftok(sem_key, 's');
if (key == -1) {
perror("ftok error.\n");
return -1;
}
printf("key is 0x%x\n", key);
int semid = semget(key, 0, 0);
if (semid == -1) {
perror("semget error.\n");
return -1;
}
printf("semid is %d\n", semid);
return semid;
}
int remove_sem(int semid)
{
if (semctl(semid, 1, IPC_RMID) == -1) {
perror("semctl error.\n");
return -1;
}
return 0;
}
int p(int semid)
{
sembuf_t sem_p;
sem_p.sem_num = 0;
sem_p.sem_op = -1;
sem_p.sem_flg = SEM_UNDO;
if (semop(semid, &sem_p, 1) == -1) {
perror("semop(p) error.\n");
return -1;
}
return 0;
}
int v(int semid)
{
sembuf_t sem_v;
sem_v.sem_num = 0;
sem_v.sem_op = 1;
sem_v.sem_flg = SEM_UNDO;
if (semop(semid, &sem_v, 1) == -1) {
perror("semop(v) error.\n");
return -1;
}
return 0;
}
test_read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "test_shm.h"
#include "test_sem.h"
// server for testing
int main()
{
printf("main of server for reading shm.\n");
int shmid = create_shm();
int semid = create_sem();
shm_buf_t *shbuf = shmat(shmid, 0, SHM_RDONLY);
if (shbuf == (void*) -1L) {
perror("shmat error.\n");
return -1;
}
printf("test_read: Memory attched at %X\n", (int)shbuf);
int n = 100;
// add a semphore resource, now there have resource because semphore is +1
v(semid);
while (1) {
p(semid);
printf("test_read: buf->flag = %d\n", shbuf->flag);
if (shbuf->flag == END || n < 0)
break;
sleep(1);
v(semid);
}
remove_sem(semid);
remove_shm(shmid);
return 0;
}
test_write.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include "test_shm.h"
#include "test_sem.h"
// client for testing
int main()
{
printf("main of server for writing shm.\n");
int shmid = get_shm();
int semid = get_sem();
shm_buf_t *shbuf = shmat(shmid, NULL, 0);
if (shbuf == (void*) -1L) {
perror("shmat error.\n");
return -1;
}
printf("test_write: Memory attched at %X\n", (int)shbuf);
memset(shbuf, 0, SHARE_BUF);
shm_buf_t *buf = malloc(sizeof(shm_buf_t));
int n = 20;
while (n > 0) {
p(semid);
if (n == 10) {
buf->flag = END;
} else {
buf->flag = n;
sleep(2);
}
printf("test_write: buf->flag = %d\n", buf->flag);
memcpy(shbuf, buf, sizeof(shm_buf_t));
n--;
v(semid);
if (n == 9)
break;
}
free(buf);
return 0;
}
主要注意的是test_read.c 中最开始给一个信号量资源。不然信号量为0,下面的p 函数会一直等待其他进程释放信号量。
代码正常,运行结果就省略了。。。
附Makefile:
ifeq ($(wildcard shm_key),)
$(shell touch shm_key)
endif
ifeq ($(wildcard sem_key),)
$(shell touch sem_key)
endif
target := test_read test_write
all: $(target)
test_read: test_read.o test_shm.o test_sem.o
gcc -o $@ $^
test_write: test_write.o test_shm.o test_sem.o
gcc -o $@ $^
%.o: %.c
gcc -c -o $@ $<
clean:
@rm -f $(target)
@rm -f *.o
相关博文: