信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。
信号量的工作原理:
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv)。
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执⾏行V(sv)释放信号量,这时第二个进程就可以恢复执行。
关于信号量的几个函数:
1、信号量创建
其中的key值可以通过ftok函数来获得,与消息队列中的key一样。
第二个参数表示信号量的个数,信号量的创建是以信号量集为单位的。也就是说可以一次性创建多个信号量。所以这个nsems表示的就是创建的信号量集中信号量的个数。最后一个参数又是和消息队列的flg参数一模一样:IPC_CREAT:存在则打开,否则创建;IPC_CREAT | IPC_EXCL存在则出错返回,否则创建,这样保证了打开的是一个全新的信号量集。这个IPC_EXCL单独使用没有任何意义。
2、信号量的初始化
第一个参数为信号量集的id,semnum表示给信号量集中的哪一个信号量进行初始化(从0开始,分别表示第一个信号量,… …)。cmd就是初始化命令:SETVAL。最后用可变参数的方式传递信号量的初始值,这个参数的类型是union:
union semun {
int val; // 使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; //IPC_INFO(Linux特有) 使用缓存区
};
注意:该联合体没有定义在任何系统头⽂文件中,因此得用户自己声明。(Centos 下确实是这样,但是UNIX下不同,不需要自己定义声明)
3、信号量的销毁
是不是很眼熟?当然,因为前面刚刚出现过,就是信号量初始化函数,只是参数不同罢了。删除即一次性删除整个信号量集,所以第二个参数缺省0即可。第三个参数就是删除命令:IPC_RMID。
4、PV操作
sem_num表示要对信号量集中的哪一个信号量操作,sem_op为1,表示V操作,表示加1,sem_op为-1,表示P操作,表示减1。
sem_flg:信号操作标志,可能的选择有两种,
(1) IPC_NOWAIT :对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
(2) SEM_UNDO :程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
下面看一个简单程序:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork(); //创建进程
if(id<0)
{
printf("fork error\n");
exit(1);
}
else if(id==0) //子进程
{
while(1)
{
printf("c");
fflush(stdout);
sleep(1);
printf("c");
fflush(stdout);
usleep(rand()%12345);
}
}
else //父进程
{
while(1)
{
printf("f");
fflush(stdout);
sleep(1);
printf("f");
fflush(stdout);
usleep(rand()%12345);
}
}
return 0;
}
在这个程序中,父进程的执行的过程中子进程也许也在执行,所以执行结果不可预测:
如图,字符’c’和’f’的顺序没有规律。若使用信号量控制,使得一个进程执行的过程中另一个进程不得执行,就能控制进程顺序从而得到有序的打印。
下面在程序中引入信号量:
comm.h
#pragma once
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#define _PATH_NAME_ "/tmp"
#define _PROJ_ID_ 0x666
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int create_sem_set(int nums);
int get_sem_set(int nums);
int init_sem_set(int sem_id, int which, int val);
int P(int sem_id);
int V(int sem_id);
int destroy(int sem_id);
comm.c:
#include "comm.h"
static int comm_sem_set(int nums, int flag)
{
key_t key = ftok(_PATH_NAME_, _PROJ_ID_);
if (key < 0)
{
perror("ftok");
return -2;
}
return semget(key, nums, flag);
}
int create_sem_set(int nums)
{
int flag = IPC_CREAT | IPC_EXCL | 0644;
return comm_sem_set(nums, flag);
}
int get_sem_set(int nums)
{
int flag = IPC_CREAT;
return comm_sem_set(nums, flag);
}
int init_sem_set(int sem_id, int which, int val)
{
union semun un;
un.val = val;
return semctl(sem_id, which, SETVAL, un);
}
static int pv(int sem_id, int op)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = op;
buf.sem_flg = 0;
return semop(sem_id, &buf, 1);
}
int P(int sem_id)
{
return pv(sem_id, -1);
}
int V(int sem_id)
{
return pv(sem_id, 1);
}
int destroy(int sem_id)
{
return semctl(sem_id, 0, IPC_RMID);
}
test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "comm.h"
int main()
{
int sem_id = create_sem_set(1);
init_sem_set(sem_id, 0, 1);
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
else if (id == 0) // child
{
while (1)
{
P(sem_id);
printf("c");
fflush(stdout);
usleep(rand()%12345);
usleep(200000);
printf("c");
fflush(stdout);
usleep(rand()%12345);
V(sem_id);
}
}
else // father
{
while (1)
{
P(sem_id);
printf("f");
fflush(stdout);
usleep(rand()%12345);
usleep(200000);
printf("f");
fflush(stdout);
usleep(rand()%12345);
V(sem_id);
}
}
destroy(sem_id);
return 0;
}
引入信号量之后可以得到规律的输出: