信号量实现的是课堂上“操作系统原理”里提到的经典PV操作
即:
点击(此处)折叠或打开
semaphore s = 1;
...
P(s);
critical code (临界区)
V(s);
...
信号量可以近似理解为博物馆里的互动体验区:
体验区有一个初始的机器数;
每来一个人,机器数就减少1;
每离开一个人,机器数就加1;
当机器数降到0时,新来的人就必须等待有人离开才能得到机器;
与互动体验区 不同的是,信号量通常用来实现串行机制,因此初始数量通常是1。这样被信号量保护的一小段代码在同一时间就只能被一个线程执行。
linux里使用信号量需要三个函数
点击(此处)折叠或打开
#include
#include
#include
// 创建信号量
int semget(key_t key, int nsems, int semflg);
// 控制信号量
int semctl(int semid, int semnum, int cmd, ...);
// 实现PV操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
创建信号量
int semget(key_t key, int nsems, int semflg);
key:自定义一个整数。这个参数类似open函数的第一个参数,由调用者指定一个“文件名”。
nsems:要初始化多少个信号量,通常设为1。
semflg:这个参数也和open函数类似。支持权限和IPC_CREAT以及IPC_EXCL
IPC_CREAT表示要创建一个信号量,但是如果信号量已经存在了也不会报错。
IPC_EXCL和IPC_CREAT一起使用时,如果信号量已经存在就会报EEXIST。
通常可以将semflg设为IPC_CREAT|IPC_EXCL|0666等
例子
#include
#include
#include
#include
#include
#include
#include
#include
int init_sem(int key, int val)
{
int id = semget((key_t)key, 1, IPC_CREAT|0666|IPC_EXCL);
if(id < 0)
{
if(errno == EEXIST)
{
printf("sem %d exists ... ", key);
if((id = semget((key_t)key, 1, 0)) > 0)
printf("get existed sem %d.\n", key);
}
}
else
{
printf("new sem %d created.\n", key);
}
return id;
}
int main()
{
int pid = getpid();
printf("pid %d started\n", pid);
int semid = init_sem(9999, 1);
printf("sem id %d\n", semid);
}
程序指定key为9999,这个是随意的。
第一次执行时,用“IPC_CREAT|0666|IPC_EXCL ”可以成功,会提示创建成功。
[root@server2 sem]# ./a
pid 5273 started
new sem 9999 created.
sem id 360451
此时用ipcs -s可以看到创建的信号量。即使程序推出了,这个信号量也会留在系统里。
------ Semaphore Arrays --------
key semid owner perms nsems
0x00000000 0 root 600 1
0x00000000 32769 root 600 1
0x1d00b184 163842 oracle 640 154
0x0000270f 360451 root 666 1
再次执行程序时,由于指定的是相同的key,系统会发现已经存在这个信号量了。
[root@server2 sem]# ./a
pid 5319 started
sem 9999 exists ... get existed sem 9999.
sem id 360451
控制信号量
int semctl(int semid, int semnum, int cmd, ...);
semid:有semget返回的id
semnum:由semid指向的第几个信号量。由于semid是唯一标识,因此semnum通常是0,也就是第1个。
cmd:进行何种操作。支持的操作比较多,常用的有:
GETVAL:设置信号量的值,semctl(sem_id, 0, GETVAL);
IPC_RMID:删除信号量,semctl(sem_id, 0, IPC_RMID);
SETVAL:设置信号量的值,这个需要用到semctl的第四个参数
第四个参数是一个union,根据cmd不同赋不同的值。
要使用第四个参数,需要先自定义这个union
#ifndef _SEMUN_H
#define _SEMUN_H
union semun
{
int val; // value for 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
};
#endif
然后就可以SETVAL了:
union semun sem_union;
sem_union.val = 1;
semctl(sem_id, 0, SETVAL, sem_union);
实现PV操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid:semget返回的id。
sops:要进行的操作的地址。是个数组,但通常只有一个元素。
nsops:要进行几项操作,通常都是1。
描述操作的结构体:
struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
其中:
sem_num是这个元素在数组里的下表,只有一个就写0。
sem_op是要给信号量加或减多少。
sem_flg通常设置为SEM_UNDO,这样当系统异常退出,系统可以回退进程对信号量的操作。
简单的PV操作
int sem_p(int id)
{
struct sembuf buf = {0, -1, SEM_UNDO};
return semop(id, &buf, 1);
}
int sem_v(int id)
{
struct sembuf buf = {0, 1, SEM_UNDO};
return semop(id, &buf, 1);
}
完整例子
#include
#include
#include
#include
#include
#include
#include
#include
#ifndef _SEMUN_H
#define _SEMUN_H
union semun
{
int val; // value for 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
};
#endif
int init_sem(int key, int val)
{
int id = semget((key_t)key, 1, IPC_CREAT|0666|IPC_EXCL);
if(id < 0)
{
if(errno == EEXIST)
{
printf("sem exists ... ");
if((id = semget((key_t)key, 1, 0)) > 0)
{
printf("get existed sem %d ", id);
int ret = semctl(id, 0, GETVAL);
printf("with value %d\n", ret);
}
}
}
else
{
printf("new sem %d created ... ", id);
union semun sun;
sun.val = val;
int ret = semctl(id, 0, SETVAL, sun);
if(ret == 0)
{
printf("initial value %d.\n", val);
}
else return ret;
}
return id;
}
int sem_p(int id)
{
struct sembuf buf = {0, -1, SEM_UNDO};
return semop(id, &buf, 1);
}
int sem_v(int id)
{
struct sembuf buf = {0, 1, SEM_UNDO};
return semop(id, &buf, 1);
}
int getsem(int id)
{
return semctl(id, 0, GETVAL);
}
int main()
{
int pid = getpid();
printf("pid %d started\n", pid);
int semid = init_sem(9999, 1);
if(semid < 0)
{
printf("%s\n", strerror(errno));
}
char c = 0;
while(1)
{
if((c = getchar()) == 'q') return 1;
printf("sem value = %d. P ... ", getsem(semid));
sem_p(semid);
printf("ok, sem value = %d.\n", getsem(semid));
if((c = getchar()) == 'q') return 1;
printf("sem value = %d. V ... ", getsem(semid));
sem_v(semid);
printf("ok, sem value = %d.\n", getsem(semid));
}
}
使用方法
开两个shell
shell 1
[root@server2 sem]# ./a
pid 5800 started
new sem 458755 created ... initial value 1.
shell 2
[root@server2 sem]# ./a
pid 5803 started
sem exists ... get existed sem 458755 with value 1
shell 1
按回车
sem value = 1. P ... ok, sem value = 0.
shell 2
按回车,陷入等待
shell 1
再按回车
sem value = 0. V ... ok, sem value = 0.
shell 2
此时P操作完成,显示
sem value = 0. P ... ok, sem value = 0.
最后,不管两个进程是什么状态,推出后系统都会回退进程所做的操作。信号量变回1。