System V 信号量
1.1 基本的API介绍
semget
-
函数功能:创建或者打开一个信号量集合
-
返回值:成功返回信号量集合的标识符,错误返回-1,并且设置errno值
-
函数原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
-
参数:
- key:一个键值,和消息队列生成键值的方式一样
- nsems:
- 当创建一个信号量集合的时候:该参数表示信号量集合中信号量的个数
- 当获取一个既有标识符的信号量集合的时候,该值必须小于或者等于集合的大小,否则返回EINVAL错误
- semflg:一个位掩码,相关参数如下,可以用OR或起来:
- IPC_CREATE
- IPC_EXCL
semctl
-
函数功能:在信号量集合或者当中的单个信号量执行相关控制操作
-
返回值: On failure semctl() returns -1 with errno indicating the error.
Otherwise the system call returns a nonnegative value depending on cmd -
函数原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
-
参数:
-
semid:信号量集合的标识符
-
semnum:对某一个信号量集合中单个信号量操作的时候,指明了相关信号量的序号,置0表示忽视该参数
-
cmd:指定了相关控制操作,常见参数如下:
- IPC_RMID:立即删除信号量集以及相关关联数据结构,所有因为调用semop而等待这个信号量集的进程都会被立即唤醒,semop会报EIDRM错误,该参数忽略arg参数和semnum参数
-
…:是一个可选参数,是一个联合体,定义如下,不同的unix实现,导致有些版本这个参数不是可选的(原型:
int semctl(int semid, int semnum, int cmd, union semun arg);
),所以为了保证移植性能,不使用这个参数的时候,需要填写一个dummy的哑参数/*the fourth has the type union semun. The calling pro‐ gram must define this union as follows:*/ 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) */ };
-
1.2 关联数据结构介绍
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set 信号集中实际的信号量数量*/
};
1.3 初始化信号集
-
SUSv3要求
semget()
不需要对信号量进行初始化,初始化操作应该由程序员手动通过semctl()
系统调用来完成,但是这个时候可能会出现竞争问题,比如进程A和进程B同时执行以下代码:#include <sys/types.h> #include <sys/sem.h> #include <sys/stat.h> #include "semun.h" /* Definition of semun union */ #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int semid, key, perms; struct sembuf sops[2]; key = 12345; perms = S_IRUSR | S_IWUSR; semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms); if (semid != -1) { /* Successfully created the semaphore */ union semun arg; /* XXXX */ /*进程B打断了进程A的执行*/ arg.val = 0; /* So initialize it */ if (semctl(semid, 0, SETVAL, arg) == -1) errExit("semctl"); } else { /* We didn't create semaphore set */ if (errno != EEXIST) { /* Unexpected error from semget() */ errExit("semget 1"); } else { /* Someone else already created it */ semid = semget(key, 1, perms); /* So just get ID */ if (semid == -1) errExit("semget 2"); } } /* Now perform some operation on the semaphore */ sops[0].sem_op = 1; /* Add 1 */ sops[0].sem_num = 0; /* ... to semaphore 0 */ sops[0].sem_flg = 0; if (semop(semid, sops, 1) == -1) errExit("semop"); exit(EXIT_SUCCESS); }
- 出现问题如下图:
-
解决办法:
semid_ds
数据结构中有一个sem_otime
字段,信号量集被创建的时候,会被初始化为0,后续的semop()
会修改这个值的大小,所以进程B中加入一段代码,检测sem_otime
字段,只有这个字段值改变了之后,进程B才进行其它操作,但是如果可以保证信号量集创建和初始化不会被打断,可以不用这么做,相关操作代码如下:#include <sys/types.h> #include <sys/sem.h> #include <sys/stat.h> #include "semun.h" /* Definition of semun union */ #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int semid, key, perms; struct sembuf sops[2]; if (argc != 2 || strcmp(argv[1], "--help") == 0) usageErr("%s sem-op\n", argv[0]); key = 12345; perms = S_IRUSR | S_IWUSR; semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms); if (semid != -1) { /* Successfully created the semaphore */ union semun arg; struct sembuf sop; sleep(5); printf("%ld: created semaphore\n", (long) getpid()); arg.val = 0; /* So initialize it to 0 */ if (semctl(semid, 0, SETVAL, arg) == -1) errExit("semctl 1"); printf("%ld: initialized semaphore\n", (long) getpid()); /* Perform a "no-op" semaphore operation - changes sem_otime so other processes can see we've initialized the set. */ sop.sem_num = 0; /* Operate on semaphore 0 */ sop.sem_op = 0; /* Wait for value to equal 0 */ sop.sem_flg = 0; if (semop(semid, &sop, 1) == -1) errExit("semop"); printf("%ld: completed dummy semop()\n", (long) getpid()); } else { /* We didn't create the semaphore set */ if (errno != EEXIST) { /* Unexpected error from semget() */ errExit("semget 1"); } else { /* Someone else already created it */ const int MAX_TRIES = 10; int j; union semun arg; struct semid_ds ds; semid = semget(key, 1, perms); /* So just get ID */ if (semid == -1) errExit("semget 2"); printf("%ld: got semaphore key\n", (long) getpid()); /* Wait until another process has called semop() */ arg.buf = &ds; for (j = 0; j < MAX_TRIES; j++) { printf("Try %d\n", j); if (semctl(semid, 0, IPC_STAT, arg) == -1) errExit("semctl 2"); if (ds.sem_otime != 0) /* Semop() performed? */ break; /* Yes, quit loop */ sleep(1); /* If not, wait and retry */ } if (ds.sem_otime == 0) /* Loop ran to completion! */ fatal("Existing semaphore not initialized"); } } /* Now perform some operation on the semaphore */ sops[0].sem_num = 0; /* Operate on semaphore 0... */ sops[0].sem_op = getInt(argv[1], 0, "sem-op"); sops[0].sem_flg = 0; if (semop(semid, sops, 1) == -1) errExit("semop"); exit(EXIT_SUCCESS); }
1.4 信号量操作
semop
-
函数功能:系统调用在
semid
标识符的信号量集上进行一个或者多个原子操作 -
返回值:返回0表示成功,错误返回1
-
函数原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops);
-
参数:
-
semid
:信号量集的标识符 -
sops
:指向一个数组 -
nsops
:指出了sops
指向的数组包含的元素个数
-
-
struct sembuf
数据结构介绍:
struct sembuf
{
unsigned short sem_num; /* semaphore number(信号集中单个信号量的序号) */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
-
sem_op
参数选项介绍:-
sem_op
大于0:调用进程具备在信号量上面的写权限。将sem_op
值加到信号量上面,等待减少该信号量值得进程会被唤醒 -
sem_op
等于0:调用进程具备在信号量上面的读权限。调用进程检查信号量值是否为0,如果等于0,操作立即结束,否则阻塞到等于0为止 -
sem_op
小于0:调用进程具备在信号量上面的写权限。当前信号量的值减去sem_op
大于等于0,操作结束,否则阻塞进程知道操作之后不会把信号量变为负值为止
-
-
sem_flg
:指定IPC_NOWAIT
,本来semop()
会阻塞,但是指定了这个标志之后,会返回EAGAIN
错误 -
sem_op
调用阻塞被唤醒的一般情况:- 等待的信号量发生了变化
- 一个信号中断了
sem_op()
,返回EINTR
错误 - 信号量被删除,返回
EIDRM
错误
-
注意:
-
semop
可以同时操作信号集中多个信号量,但是存在一个问题,semop
要么立即执行,要么阻塞到能够对信号集中的所有信号量操作为止 - 多个进程同时对一个信号量进行操作的时候,一定要避免某一个进程出现饥饿现象
- 信号量1的
value
为0,进程A打算在信号量减1,进程B也打算在信号量1上减1,当信号量变为1的时候,进程A和进程B谁先执行了,这个主要根据内核的进程调度算法解决了 - 当进程A和B对信号量减去的值不相等的时候,假如进程A减2,进程B减1,当信号量值变为1的时候,不管如何进程B都会先执行,主要是为了满足,先满足条件先执行的顺序执行进程
- 信号量1的
-
semtimedop
- 函数功能:相比于
semop
多了一个阻塞时间设置参数timeout
,如果timeout
设置为NULL
那么就和semop
没有差别了 - 函数原型:
int semtimedop(int semid, struct sembuf *sops, unsigned nsops,
struct timespec *timeout);
- 返回值:成功返回1,错误返回-1
- 参数:
- timeout:表示可以指定一个时间,在信号量操作完成之前,如果超过这个时间,函数返回EAGAIN错误,如果置为0,函数就和
semop
功能一样了
- timeout:表示可以指定一个时间,在信号量操作完成之前,如果超过这个时间,函数返回EAGAIN错误,如果置为0,函数就和
1.5 信号量撤销值
-
semop()
在修改一个信号量值得时候可以使用SEM_UNDO
标记,当指定了这个标记,内核会记录信号量操作的效果,然后再进程终止的时候,会撤销semop()
操作
1.6 System V信号量的限制
- 信号量通过标识符来标识,而其它大多数I/O操作和IPC采用文件描述符来表示
- 使用键值而不是文件名来生成标识符,增加了编程复杂度
- 创建和初始化信号量是分开的操作,可能会出现竞争问题,增加了编程的复杂度
- 内核不会维护一个引用该信号量集的进程数量,所以会造成删除了一个信号量集的不确定因素
- 编程接口过于复杂
- 信号量操作存在许多限制