信号量是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号量是进程/线程同步的一种方式。
有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关。有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量。信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样。
1、System V 信号量,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。
2、Posix 有名信号量,一种来源于 POSIX 技术规范的实时扩展方案(POSIX Realtime
Extension),可用于进程或线程间的同步,常用于线程。
3、Posix 基于内存的信号量,存放在共享内存区中,可用于进程或线程间的同步。
为了获得共享资源进程需要执行下列操作:
1、测试控制该资源的信号量。
2、若信号量的值为正,则进程可以使用该资源。进程信号量值减 1,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号量会加 1。以便其他进程使用。
3、若信号量的值为 0,则进程进入休息状态,直至信号量值大于 0。进程被唤醒,返回第1步。
为了正确地实现信号量, 信号量值的测试值的测试及减 1 操作应当是原子操作(原子操作是不可分割的,在执行完毕前不会被任何其它任务或事件中断)。为此信号量通常是在内核中实现的。
进程三个状态之间的转换就是靠PV操作来控制的。
PV操作(PV原语)实质就是一个都对临界资源进行加锁的机制,使得许多进程可以对临界资源进行互斥的访问。主要涉及P操作、V操作和信号量。其中信号量起到了至关重要的作用。
是由若干条指令组成的,用于完成一定功能的一个过程。是由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。
信号量(Saphore)由一个值和一个指针组成,指针指向等待该信号量的进程。信号量的值表示相应资源的使用情况。信号量S>=0时,S表示可用资源的数量。
执行一次P操作意味着请求分配一个资源,因此S的值减1。当S<0时,表示已经没有可用资源,S的绝对值表示当前等待该资源的进程数。请求者必须等待其他进程释放该类资源,才能继续运行。
执行一个V操作意味着释放一个资源,因此S的值加1。若S<0,表示正在等待该资源的进程的资源个数。因此要唤醒一个等待状态的进程,使之运行下去。
P(s):将信号量value值减1,若结果小于0,则执行P操作的进程被阻塞,若结果大于等于0,则执行P操作的进程将继续执行。
V(s):将信号量的值加1,若结果不大于0,则执行V操作的进程从信号量s有关的list所知队列中释放一个进程,使其转化为就绪态,自己则继续执行。若结果大于0,则执行V操作的进程继续执行。
(1)S减1;
(2)若S减1后仍大于或等于零,则进程继续执行;
(3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
(1)S加1;
(2)若相加结果大于零,则进程继续执行;
(3)若相加结果小于或等于零(表明有进程阻塞在该类资源上)
则从该信号的等待队列中唤醒一等待进程(从阻塞队列里唤醒一个进程来“转手”该类资源,唤醒了就意味着可以干活了),然后再返回原进程继续执行或转进程调度。
执行P操作,信号量减一
然后进行对共享资源的访问
V操作,信号量加一
首先设初始的信号量值为1,进程A要进入临界区(每个进程中访问临界资源——(一次仅允许一个进程使用的资源)的程序段)。
a. 进程A先执行P操作,信号量从1变成0,进入临界区。
b.此时进程B要进入临界区,执行P操作,信号量变成-1,进程B阻塞。
c. 进程A访问临界区之后,执行V操作,信号量加一,变成0。唤醒阻塞进程B,于是B进入临界区,执行完毕后执行V操作,信号量+1,变成1。
有关PV操作以及原语的解释来自:
https://blog.csdn.net/diyinqian/article/details/75332070
函数原型:
#include
#include
#include
int semget(key_t key,int nsems,int flag);
int semop(int semid,struct sembuf *sops,size_t num_sops);
int semctl(int semid, int semnum, int cmd, …);
int semget(key_t key,int nsems,int flag);
创建一个信号量集或访问一个已存在的信号量集。
返回值:成功时,返回一个称为信号量标识符的整数,semop 和 semctl 会使用它,出错时,返回-1。
参数 key是唯一标识一个信号量的关键字,如果为 IPC_PRIVATE(值为 0,创建一个只有创建者进程才可以访问的信号量, 通常用于父子进程之间。非 0 值的 key(可以通过 ftok 函数获得)表示创建一个可以被多个进程共享的信号量。
参数 nsems指定需要使用的信号量数目。如果是创建新集合,则必须制定 nsems。如果引用一个现存的集合,则将 nsems 指定为 0。
参数 flag 是一组标志,其作用与 open 函数的各种标志很相似。它低端的九个位是该信号量的权限, 其作用相当于文件的访问权限。 此外, 它们还可以与键值 IPC_CREAT按位或操作,以创建一个新的信号量。即使在设置了IPC_CREAT 标志后给出的是一个现有的信号量的键字,也并不是一个错误。
我们也可以通过 IPC_CREAT 和 IPC_EXCL标志的联合使用确保自己将创建出一个新的独一无二的信号量来, 如果该信号量已经存在,就会返回一个错误。
int semop(int semid,struct sembuf *sops,size_t num_sops);
函数 semop 用于改变信号量对象中各个信号量的状态。
返回值:成功时,返回 0,失败时,返回-1。
参数 semid是由 semget 返回的信号量标识符。
参数 sops 是指向一个结构体数组的指针。每个数组元素至少包含以下几个成员:
struct sembuf{
short sem_num; //操作信号量在信号量集合中的编号,第一个信号量的编号是 0。
short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。
short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为semop调用前的值。
};
sem_op:
sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是 p 操作,它等待信号量变为可用。一个是+1,也就是 v 操作,它发送信号通知信号量现在可用。
sem_flag:
信号量操作的属性标志,如果为0,表示正常操作,如果为IPC_WAIT,使对信号量的操作是非阻塞的。即指定了该标志,调用线程在信号量的值不满足条件的情况下不会被阻塞,而是直接返回-1,并将errno设置为EAGAIN。
如果为SEM_UNDO,那么将维护进程对信号量的调整值,以便进程结束时恢复信号量的状态。
参数 num_ops 为 sops 指向的 sembuf 结构体数组的大小。
int semctl(int semid, int semnum, int cmd, …);
函数 semctl 用来直接控制信号量信息。函数返回值:成功时,返回 0;失败时,返回-1。
参数 semid 是由 semget 返回的信号量标识符。
参数 semnum 为集合中信号量的编号,当要用到成组的信号量时,从 0 开始。一般取值为 0,表示这是第一个也是唯一的一个信号量。
参数 cmd 为执行的操作,通常为:
IPC_RMID //立即删除信号集,唤醒所有被阻塞的进程
GETVAL //根据 semun 返回信号量的值,从 0 开始,第一个信号量编号为 0
SETVAL //根据 semun 设定信号的值,从 0 开始,第一个信号量编号为 0
GETALL //获取所有信号量的值,第二个参数为 0,将所有信号的值存入 semun.array中
SETALL //将所有 semun.array 的值设定到信号集中,第二个参数为 0
参数省略号是一个 union semun(需要自己定义),它至少包含以下几个成员:
union semun{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
};
通常情况仅使用 val,给 val 赋值为 1
struct semid_ds:
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 long sem_nsems; /* No. of semaphores in set */
};
struct ipc_perm:
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
程序实现思路:
父进程设为生产者 space 10 代表空的位置 0号信号量的值
子进程设为消费者 product 0 代表产品个数 1号信号量的值
消费者先对product-1,当消费完毕后对space+1
p 1号信号量 产品个数-1
v 0号信号量 空位+1
生产者先对space-1,当生产完毕后对product+1
p 0号信号量 空位-1
v 1号信号量 产品个数+1
#include
int main(int argc, char* argv[])
{
int semArrId=semget(1000,2,IPC_CREAT|0600); //创建一个信号量集
ERROR_CHECK(semArrId,-1,"semget");
unsigned short arr[2]={10,0}; //0号信号量的值代表仓库位置数目,1号信号量的值代表产品个数
semctl(semArrId,0,SETALL,arr); //用来设置和控制信号量信息的函数
struct sembuf sopp,sopv;
if(!fork()) //子进程为消费者
{
while(1)
{
sopp.sem_num=1; //操作信号量在信号量集合中的编号,第一个信号量的编号是 0
sopp.sem_op=-1; //sem_op 成员的值是信号量在一次操作中需要改变的数值
sopp.sem_flg=SEM_UNDO; //维护进程对信号量的调整值,以便进程结束时恢复信号量的状态
sopv.sem_num=0;
sopv.sem_op=1;
sopv.sem_flg=SEM_UNDO;
printf("I am child space number=%d,productor num=%d\n",semctl(semArrId,0,GETVAL),semctl(semArrId,1,GETVAL));
semop(semArrId,&sopp,1); //用于改变信号量对象中各个信号量的状态,产品个数减1
printf("consume success\n");
semop(semArrId,&sopv,1); //空位+1
printf("I am child space number=%d,productor num=%d\n",semctl(semArrId,0,GETVAL),semctl(semArrId,1,GETVAL));
sleep(2);
}
}
else
{
while(1)
{
sopp.sem_num=0;
sopp.sem_op=-1;
sopp.sem_flg=SEM_UNDO;
sopv.sem_num=1;
sopv.sem_op=1;
sopv.sem_flg=SEM_UNDO;
printf("I am parent space number=%d,productor num=%d\n",semctl(semArrId,0,GETVAL),semctl(semArrId,1,GETVAL));
semop(semArrId,&sopp,1); //仓库个数减1
printf("produce success\n");
semop(semArrId,&sopv,1);
printf("I am parent space number=%d,productor num=%d\n",semctl(semArrId,0,GETVAL),semctl(semArrId,1,GETVAL));
sleep(1);
}
}
return 0;
}