上一篇博客中说到了进程间通信的第一种方式—管道(pipe),管道的通信方式是半双工的,在同一时间只能读或者写。
这一篇博客来简略的说一下进程间通信的第二种方式—信号量。那么,什么是信号量?什么是PV操作?什么又是临界区和临界资源?
目录
信号量
1、信号量(semaphore)是用于管理对资源的访问。它与管道、消息队列等的实现原理不同。信号量是一个计数器,用于多进程对共享数据对象的访问。
2、当我们编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户多进程系统上,我们通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权。
所以:
- 临界资源:指同一时刻,只允许一个进程(或线程)访问的资源
- 临界区:指访问临界资源的代码段
3、原子操作:指该操作绝不会在执行完毕前被任何其他任务或事件打断,也就是说,它是最小的执行单位,不能有比它更小的执行单元
4、信号量的一个更正式的定义是:它是一个特殊变量,值是可以改变的,但只允许对它进行等待(wait)和发送信号(signal)这两种操作。因为在Linux编程中,“等待”和“发送信号”都已具有特殊的含义。
- P(信号量变量):用于等待,对信号量的值进行原子减1。如果信号量值为0,则P操作阻塞,挂起该进程的执行。
- V(信号量变量):用于发送信号,对信号量的值进行原子加1,不会阻塞,V操作代表释放资源。
5、信号量的分类:
- 计数信号量(通用信号量):可以取多个正整数值的信号量,计数信号量的初始值其实就是共享资源的数量。有几个进程访问,信号量就减去几,直到再次没有资源可以使用,大于3的值都为计数信号量。
- 二值(二进制)信号量:只能取0和1的变量,取0代表资源不可访问,取1代表可以访问。
假设有一个信号量sv,PV操作定义如下:
- P(sv):如果sv得值大于0,执行原子减1;如果它的值等于0,就挂起该进程的执行
- V(sv):如果有其它进程因为等待sv被挂起,就让该进程恢复运行;如果没有,执行原子加1
还可以这样看信号量:当临界区域可用时,信号量变量sv的值是true,然后P操作执行原子减1,使sv变为false,表示临界区域正在被使用。当进程离开临界区域时,使用V(sv)操作将它加1,使临界区域再次变为可用。
如下图:二进制信号量,sv初值设为1。进程AB均有机会进入临界区。如果此时进程A先执行了P(sv)操作将sv减1。此时如果进程B再执行P操作试图进入临界区,就会被挂起。知道进程A退出临界区并执行V(sv)操作将sv加1,临界区才再次可用。如果进程B因为等待sv而被挂起,这时它将被唤醒。
、
6、 信号量编程基本步骤:
- 创建信号量并初始化
- 执行P操作
- 执行V操作
- 销毁信号量
Linux的信号量机制
Linux信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop和semctl。它们都被设计为操作一组信号量,即信号量集,这些函数都是用来对成组的信号值进行操作的,而不是单个信号量。
1、semget函数
semget函数的作用是创建一个新信号量或取得一个已有信号量的键。
int semget(key_t key,int num_sems,int sem_flags);
①参数key是键值,它是一个整数值,用来标识一个全局唯一的信号量集。要通过信号量通信的进程需要使用相同的键值来创建或获取该信号量。也就是不相关的进程可以通过它访问同一个信号量。程序对所有信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标识符。只有semget函数才直接使用信号量键,所有其他的信号量函数都是使用由semget函数返回的信号量标识符。特殊的信号量键值IPC_PRIVATE,它的作用是创建一个只有创建者进程才可以访问的信号量,但这个键值很少有实际的用途。在创建新的信号量时,需要给键提供一个唯一的非0整数。
②num_sems参数指定需要的信号量数目,正常情况下取1即可。如果创建信号量,必须指定该值;如果获取已经存在的信号量,可以置为0。
③sem_flags参数是一组标志,与底层文件访问函数(open)的标准非常相似,也类似于文件的访问权限。除此之外,还可以与IPC_CREAT按位或,来创建一个新信号量。即使在设置了IPC_CREAT标志后给出的键是一个已有信号量的键,也不会产生错误。如果用不到该标志,就会被忽略掉。可以通过联合使用IPC_CREAT和IPC_EXCL来确保创建出的是一个新的、唯一的信号量。如果该信号量已存在,将返回一个错误,并设置errno为EEXIST。
semget函数成功时返回一个正数值,该值是其它信号量函数将用到的信号量标识符。如果失败,返回-1并设置errno。
如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化。
#include<sys/sem.h>
/*该结构体用于描述IPC对象(信号量、共享内存和消息队列)的权限*/
struct ipc_perm
{
key_t key; //键值
uid_t uid; //所有者的有效用户ID
gid_t gid; //所有者的有效组ID
uid_t cuid; //创建者的有效用户ID
gid_t cgid; //创建者的有效组ID
mode_t mode; //访问权限
};
struct semid_ds
{
strtuct ipc_perm sem_perm; //信号量的操作权限
unsigned long int sem_nsems; //该信号量集中的信号量数目
time_t sem_otime; //最后一次调用semop的时间,初始化设为0
time_t sem_ctime; //最后一次调用semctl的时间,初始化设置为当前系统时间
};
2、semop函数
semop函数用于改变信号量的值,即执行PV操作。
/* 与每个信号量关联的一些重要的内核变量 */
unsigned short semval; //信号量的值
unsigned short semzcnt; //等待信号量值变为0的进程数量
unsigned short semncnt; //等待信号量值增加的进程数量
pid_t sempid; //最后一次执行semop的操作进程ID
semop对信号量的操作实际上就是对这些内核变量的操作。semop定义如下:
int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops);
①sem_id是由semget函数返回的信号量标识符。
②sem_ops是指向一个结构体数组的指针
struct sembuf
{
short sem_num;//信号量编号,除使用一组信号量之外,一般取0
short sem_op;//信号量在一次操作中需要改变的数值(p->-1,v->+1)
short sem_flg;//通常被设置为SEM_UNDO,含义是当进程退出时取消正在进行的semop操作
};
③num_sem_ops指定要执行的操作个数,即sem_ops数组中的元素的个数。semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,该过程为原子操作。
semop函数成功时返回0,失败则返回-1并设置errno。失败时,sem_ops数组中指定的所有操作都不被执行。
3、semctl函数
semctl函数用来直接控制信号量信息。定义如下:
int semctl(int sem_id,int sem_num,int command,···);
①sem_id是由semget函数返回的信号量标识符。
②sem_num指定被操作的信号量在信号集中的编号。
③command指定要执行的命令有两个常用值SETVAL,IPC_RMID。、
- SETVAL:用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置。其作用是在信号量第一次使用之前对它进行设置。
- IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
④第四个参数由用户自己定义,但sys/sem.h给出了它的推荐格式。具体如下:
union semun
{
int val; //用于SETVAL命令
struct semid_ds *buf; //用于IPC_STAT和IPC_SET命令
unsigned short *array; //用于GETALL和SETALL命令
};
该联合体中一般用到的是val,表示要传给信号量的初始值。
semctl函数成功时的返回值取决于command参数。失败时返回-1并设置errno。
4、无信号量控制,两个程序并发运行访问临界资源
/* a.c */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include"sem.h"
int main(int argc,char *argv[])
{
int i = 0;
for(;i<5;i++)
{
printf("A");//A开始访问临界资源
fflush(stdout);
int n = rand()%3;
sleep(n);
printf("A");//A退出临界区,释放资源
fflush(stdout);
n = rand()%3;
sleep(n);
}
exit(0);
}
/* b.c */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include"sem.h"
int main(int argc,char *argv[])
{
int i = 0;
for(;i<5;i++)
{
printf("B");//B开始访问临界资源
fflush(stdout);
int n = rand()%3;
sleep(n);
printf("B");//B退出临界区,释放资源
fflush(stdout);
n = rand()%3;
sleep(n);
}
exit(0);
}
从运行结果可以看出AB并不成对出现,对临界资源的访问并不互斥。
5、有信号量控制,两个程序并发运行访问临界资源
/* sem.h */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/sem.h>
#include<string.h>
#include<assert.h>
void sem_init();
void sem_p();
void sem_v();
void sem_destroy();
union semun
{
int val;
};
#include"sem.h"
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/sem.h>
static int semid;
void sem_init()
{
semid = semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//创建一个新的信号量
if(semid == -1)//创建失败
{
semid = semget((key_t)1234,1,IPC_CREAT|0600);//获取这个键值对应的已有的信号量
if(semid == -1)//获取失败
{
perror("semget error");
return;
}
}
else
{
union semun a;//semun --> semctl
a.val = 1;
if(semctl(semid,0,SETVAL,a) == -1)//给信号量赋初始值
{
perror("semctl error");
}
}
}
void sem_p()
{
struct sembuf buf;//sembuf ---> semget
buf.sem_num = 0;//第一个信号
buf.sem_op = -1;//p操作,信号量-1
buf.sem_flg = SEM_UNDO;
if(semop(semid,&buf,1) == -1)
{
perror("semop p error");
}
}
void sem_v()
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;//v操作,信号量+1
buf.sem_flg = SEM_UNDO;
if(semop(semid,&buf,1) == -1)
{
perror("semop v error");
}
}
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID) == -1)//IPC_RMID删除一个已经无需继续使用的信号量标识符
{
perror("semctl error");
}
}
/* as.c */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include"sem.h"
int main(int argc,char *argv[])
{
sem_init();
int i = 0;
for(;i<5;i++)
{
sem_p();
printf("A");
fflush(stdout);
int n = rand()%3;
sleep(n);
printf("A");
fflush(stdout);
// sem_v();
n = rand()%3;
sleep(n);
sem_v();
}
exit(0);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include"sem.h"
int main()
{
sem_init();
int i = 0;
for(;i<5;i++)
{
sem_p();
printf("B");
fflush(stdout);
int n = rand()%3;
sleep(n);
printf("B");
fflush(stdout);
sem_v();
n = rand()%3;
sleep(n);
}
sleep(10);
sem_destroy();
exit(0);
}
使用信号量来对临界资源进行控制之后,根据执行结果可以看出AB总是成对出现,因为A 和 B 对共享资源的访问是互斥的。AA表示:第一个A访问临界区、占用临界资源,第二个A释放临界资源。