在Linux编程中,通过fork调用创建子进程从而实现多进程非常容易,应用也非常广泛。多进程之间要保护临界区资源,方法之一就是使用信号量。Linux中的信号量API有两套,一套是POSIX标准的API,另一套则是较老的System V API。POSIX API遵循POSIX标准,因此移植性更好,即便是非UNIX/Linux系统,只要支持POSIX标准,代码也可编译运行;而System V API存在时间更长,因此可能存在的更广泛。下面分别介绍这两种API。
POSIX信号量
POSIX信号量有两种形式:
- 命名信号量。命名信号量又一个名字标识,名字的形式为"/somename",即一个斜杠再加一些字母(只能又一个斜杠)。命名信号量可以用sem_open来创建或者打开,可以被多个进程同时操作。
- 匿名信号量。匿名信号量则没有名字标识,根据使用范围,分为线程间共享的匿名信号量和进程间共享的匿名信号量。进程间共享的匿名信号量通常放在全局变量中;而进程间共享的信号量则放在共享内存中。匿名信号量使用sem_init函数进行初始化。
POSIX信号量相关的API如下。
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
int sem_destroy(sem_t *sem);
使用POSIX信号量的基本步骤为:
- 对于命名信号量,用sem_open创建或者打开,并指定名字。对于匿名信号量,应先申请sem_t存储,如果是进程间共享,可放到共享内存中;如果是线程间共享,则可放到全局变量中,然后调用sem_init函数初始化信号量。
- 调用sem_wait等待信号量。
- 访问临界区资源。
- 调用sem_post释放信号量。
- 对于命名信号量,调用sem_close释放信号量,并可调用sem_unlink删除。对于匿名信号量,调用sem_destroy删除信号量。
下面这个例子演示了命名信号量的使用。这个例子非常简单,父进程创建了两个子进程,然后三个进程都尝试获取信号量。从运行结果不难看出,由于初始值为1,因此刚开始两个进程尝试获取信号量只有1个成功,另一个等待;第一个获取信号量的进程释放后,后续进程才能再获取信号量。
/*
* posix_sem.c: demostrates POSIX named semaphore
*/
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#define SEM_NAME "/test_posix_shm"
static sem_t *sem = SEM_FAILED;
static void create_sem(void)
{
sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
return;
}
}
static void destroy_sem(void)
{
if (sem != SEM_FAILED) {
sem_close(sem);
sem = SEM_FAILED;
}
sem_unlink(SEM_NAME);
}
static pid_t do_child(void)
{
pid_t pid;
sigset_t full, old;
sigfillset(&full);
sigprocmask(SIG_SETMASK, &full, &old);
pid = fork();
if (pid == 0) {
sigprocmask(SIG_SETMASK, &old, NULL);
printf("%d: acquiring semaphore\n", getpid());
sem_wait(sem);
printf("%d: semaphore acquired\n", getpid());
sleep(1);
sem_post(sem);
printf("%d: semaphore released\n", getpid());
exit(EXIT_SUCCESS);
}
sigprocmask(SIG_SETMASK, &old, NULL);
return pid;
}
int main(int argc, char *argv[])
{
create_sem();
do_child();
do_child();
printf("%d: acquiring semaphore\n", getpid());
sem_wait(sem);
printf("%d: semaphore acquired\n", getpid());
sleep(1);
sem_post(sem);
printf("%d: semaphore released\n", getpid());
while (wait(NULL) > 0);
destroy_sem();
return 0;
}
gcc -o posix_sem posix_sem.c -lpthread
./posix_sem
30690: acquiring semaphore
30691: acquiring semaphore
30690: semaphore acquired
30692: acquiring semaphore
30690: semaphore released
30691: semaphore acquired
30691: semaphore released
30692: semaphore acquired
30692: semaphore released
下面这个例子与前面的例子结构类似,演示放在共享内存里的匿名信号量的使用。
/*
* posix_anonsem.c: demostrates POSIX anonymous semaphore
*/
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static char self_exe[MAXPATHLEN + 1];
#define SHM_NAME "/test_posix_anon_sem"
struct shared_data {
sem_t sem;
};
#define SHM_SIZE sizeof(struct shared_data)
static int shm_id = -1;
static struct shared_data *sd = MAP_FAILED;
static sem_t *sem = SEM_FAILED;
static void create_sem(void)
{
shm_id = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_id < 0) {
perror("shm_open");
return;
}
ftruncate(shm_id, SHM_SIZE);
sd = (struct shared_data *)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_id, 0);
if (sd == MAP_FAILED) {
perror("mmap");
return;
}
sem = &sd->sem;
sem_init(sem, 1, 1);
}
static void destroy_sem(void)
{
if (sem != SEM_FAILED) {
sem_destroy(sem);
sem = SEM_FAILED;
}
if (sd != MAP_FAILED) {
munmap(sd, SHM_SIZE);
sd = MAP_FAILED;
}
if (shm_id > 0) {
close(shm_id);
shm_id = -1;
shm_unlink(SHM_NAME);
}
}
static void child_proc(void)
{
shm_id = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_id < 0) {
perror("shm_open");
return;
}
ftruncate(shm_id, SHM_SIZE);
sd = (struct shared_data *)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_id, 0);
if (sd == MAP_FAILED) {
perror("mmap");
return;
}
sem = &sd->sem;
printf("%d: acquiring semaphore\n", getpid());
sem_wait(sem);
printf("%d: semaphore acquired\n", getpid());
sleep(1);
sem_post(sem);
printf("%d: semaphore released\n", getpid());
munmap(sd, SHM_SIZE);
close(shm_id);
}
static pid_t do_child(void)
{
pid_t pid;
sigset_t full, old;
sigfillset(&full);
sigprocmask(SIG_SETMASK, &full, &old);
pid = fork();
if (pid == 0) {
sigprocmask(SIG_SETMASK, &old, NULL);
execl(self_exe, self_exe, "child", (char *)0);
exit(-1);
}
sigprocmask(SIG_SETMASK, &old, NULL);
return pid;
}
int main(int argc, char *argv[])
{
if (argc == 1) {
realpath(argv[0], self_exe);
create_sem();
do_child();
do_child();
printf("%d: acquiring semaphore\n", getpid());
sem_wait(sem);
printf("%d: semaphore acquired\n", getpid());
sleep(1);
sem_post(sem);
printf("%d: semaphore released\n", getpid());
while (wait(NULL) > 0);
destroy_sem();
} else {
child_proc();
}
return 0;
}
gcc -o posix_anonsem posix_anonsem.c -lpthread -lrt
./posix_anonsem
32471: acquiring semaphore
32471: semaphore acquired
32472: acquiring semaphore
32473: acquiring semaphore
32471: semaphore released
32473: semaphore acquired
32473: semaphore released
32472: semaphore acquired
32472: semaphore released
System V信号量
System V的信号量与POSIX信号量略有不同,它实际上是一个信号量集合。其相关API如下。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
int semctl(int semid, int semnum, int cmd, ...);
System V信号量的使用步骤如下:
- 调用semget函数来创建或者打开一个信号量集合。第一个参数为标识信号量集合的key,可通过ftok函数从一个文件路径来生成key。第二个参数指定信号量集合中的信号量个数。如果是创建信号量,那么第三个参数应带标记IPC_CREAT。
- 调用semctl来设置信号量初值。
- 调用semop来实现信号量的P操作。
- 访问临界区资源。
- 调用semop来实现信号量的V操作。
- 调用semctl可删除信号量。
信号量对象是在内核中维护的,因此如果一个进程创建了一个信号量,没有调用semctl删除它就退出了,那么它依然在内核中存在。如果内核配置了CONFIG_PROC_FS,则可以在/proc/sysvipc/sem中看到它。
cat /proc/sysvipc/sem
key semid perms nsems uid gid cuid cgid otime ctime
0 0 600 1 0 0 0 0 1602142859 1602142857
0 32769 600 1 0 0 0 0 1602114063 1602142861
0 98306 600 1 0 0 0 0 0 1608105510
下面这个例子演示了System V信号量的使用。与前面的例子稍有不同的时,在这个例子中,创建的第一个子进程在获取信号量后就调用abort()函数异常中止了。但是从运行结果可以看出,这并没有导致后面等待信号量的进程死锁,这就时System V信号量中的SEM_UNDO的作用。从最后该信号量的状态输出也可看出,最终信号量与初值一样,而且没有等待进程。System V信号量的SEM_UNDO这个功能比POSIX信号量更稳健。
/*
* sysv_sem.c: demostrates System V semaphore
*/
#include <sys/param.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static char self_exe[MAXPATHLEN + 1];
#define SEM_NAME "/tmp/test_sysv_sem"
static int sem_id = -1;
#define P(S) \
do { \
struct sembuf sop = {0, -1, SEM_UNDO}; \
semop(S, &sop, 1); \
} while(0)
#define V(S) \
do { \
struct sembuf sop = {0, 1, SEM_UNDO}; \
semop(S, &sop, 1); \
} while(0)
static void create_sem(void)
{
sem_id = semget(ftok(SEM_NAME, 1), 1, IPC_CREAT | 0666);
if (sem_id == -1) {
perror("semget");
return;
}
semctl(sem_id, 0, SETVAL, 1);
}
static void destroy_sem(void)
{
if (sem_id != -1) {
semctl(sem_id, 0, IPC_RMID);
sem_id = -1;
}
}
static void dump_sem(void)
{
printf("GETVAL: %d\n", semctl(sem_id, 0, GETVAL));
printf("GETNCNT: %d\n", semctl(sem_id, 0, GETNCNT));
printf("GETZCNT: %d\n", semctl(sem_id, 0, GETZCNT));
}
static void child_proc(int test)
{
sem_id = semget(ftok(SEM_NAME, 1), 1, 0666);
if (sem_id < 0) {
perror("semget");
return;
}
printf("%d: acquiring semaphore\n", getpid());
P(sem_id);
printf("%d: semaphore acquired\n", getpid());
if (test) {
printf("%d: child abort with semaphore acquired, it won't block since SEM_UNDO is set\n", getpid());
abort();
}
sleep(1);
V(sem_id);
printf("%d: semaphore released\n", getpid());
}
static pid_t do_child(int test)
{
pid_t pid;
sigset_t full, old;
sigfillset(&full);
sigprocmask(SIG_SETMASK, &full, &old);
pid = fork();
if (pid == 0) {
sigprocmask(SIG_SETMASK, &old, NULL);
if (test)
execl(self_exe, self_exe, "child", "test", (char *)0);
else
execl(self_exe, self_exe, "child", (char *)0);
exit(-1);
}
sigprocmask(SIG_SETMASK, &old, NULL);
return pid;
}
int main(int argc, char *argv[])
{
if (argc == 1) {
realpath(argv[0], self_exe);
create_sem();
do_child(1);
do_child(0);
printf("%d: acquiring semaphore\n", getpid());
P(sem_id);
printf("%d: semaphore acquired\n", getpid());
sleep(1);
V(sem_id);
printf("%d: semaphore released\n", getpid());
while (wait(NULL) > 0);
dump_sem();
destroy_sem();
} else {
child_proc(argc > 2 ? 1 : 0);
}
return 0;
}
gcc -o sysv_sem sysv_sem.c
./sysv_sem
3290: acquiring semaphore
3290: semaphore acquired
3291: acquiring semaphore
3292: acquiring semaphore
3290: semaphore released
3291: semaphore acquired
3291: child abort with semaphore acquired, it won't block since SEM_UNDO is set
3292: semaphore acquired
3292: semaphore released
GETVAL: 1
GETNCNT: 0
GETZCNT: 0

本文介绍了Linux编程中用于多进程同步的信号量机制,包括POSIX和System V两种API。POSIX信号量有命名和匿名两种,使用sem_open、sem_init等函数操作。System V信号量是一个集合,通过semget、semctl等函数管理。文中通过实例展示了两种信号量的使用步骤和区别。
293

被折叠的 条评论
为什么被折叠?



