在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。消息队列点击打开链接、共享内存点击打开链接,今天我们主要来看信号量。。。。
在看信号量之前,我们先来看几个概念
进程互斥:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程竞争使用这些资源,进程的这种关系为进程的互斥。也就是说一个资源每次只能被一个进程访问。
进程同步:在访问资源的时候,以某种特定顺序的方式区访问资源,也就是多个进程需要相互配合共同完成一项资源。
临界资源(互斥资源):系统中某些资源一次只允许一个进程使用。
临界区:在进程中涉及到互斥资源的程序段。
原子性:不可被中断的操作,通俗点就是一件事情只会有两种情况,要么是做了,要么就没做。
信号量是什么
(1)信号量本质上是一个具有原子性的计数器,用来描述临界资源的,不能用全局变量count加加减减替换(因为他没有原子性)。
(2)信号量以保护临界资源为目的,但他本身也是个临界资源;他控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数量,常常被当做锁来使用,防止一个进程访问另外一个进程正在使用的资源
(3)其中最简单的信号量=1,也叫做二元信号量(互斥锁),可控制单个资源,只能取0和1;信号量>=2为多远信号量
为什么要使用信号量
其实还是要拿临界资源来说,进程间通信时,可能会出现多个进程同时区访问同一临界资源,那这个时候就会出现让谁来访问的问题,为了防止出现这样的矛盾,我们需要一种方法,控制在任意时刻只能有一个执行线程来访问临界区,而信号量就是解决这种矛盾的一种机制,信号量就是用来协调进程对共享资源的访问的。
信号量的特点
(1)同消息队列一样,生命周期随内核,当进程结束的时候,信号来那个还没有被销毁;
(2)信号量并非是单个非负值,而必须定义为含有一个或多个信号量值的集合,当创建信号量的时候,需要制定集合当中信号量的数量
(3)信号量的创建和初始化不能同时进行,因为操作不具有原子性,由于不互斥,可能会导致创建后未初始化就被另一个进程获取。
信号量的P、V操作---->必须保证操作的原子性
(1)信号量:互斥---->P、V在同一进程中
同步---->P、V不在同一进程中
(2)信号量值含义:S>0:S表示可用资源的个数
S=0:S表示无可用资源,无等待进程
S<0:|S|表示等待队列中进程个数
为了更好理解信号量以及P、V操作,看下面的伪代码
信号量本质上是一个计数器
struct semaphore
{
int count;//计数
pointer_PCB queue;//等待队列
}
P操作
P(s)//申请资源
{
s.count=s.count--;//申请成功,资源减1
if(s.count<0)
{
//该进程状态置为等待状态
//将该进程的PCB插入相应的等待队列s.queue末尾
}
}
V操作
V(s)
{
s.count=s.cout++;//释放资源
if(s.count<=0)
{
//唤醒相应等待队列s.queue中等待的一个进程
//改变其状态为就绪态
//并将其插入就绪队列
}
}
信号集结构
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 short sem_nsems; /* No. of semaphores in set */
};
相关函数
//头文件
#include
//1.ftok--->获取key值
#include
#include
key_t ftok(const char* pathname,int id)//pathname:路径名 id:项目id,非0整数(只有低8位有效)
//2.semget函数
int semget(key_t key,int nsems,int semflg);
//参数:key-->信号集的名字
//nsems:信号集中信号量的个数
//semflg:由九个权限构成,用法和创建文件时使用的mode模式一样
//返回值:成功返回一个非负整数,即该信号集的标识码;失败返回-1
//semctl函数
int semctl(int semid,int semnum,int cmd,...);
//参数:
//semid:mesget返回的信号集标识码
//semnum:信号集中信号量的序号
//cmd:将要采取的动作(有三个取值)
//最后一个参数根据命令不同而不同
//返回值:成功返回0;失败返回-1
第四个参数是可选的,如果使用该参数,则他的类型是semun,它是一个联合
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
}
//4semop函数
int semop(int semid,struct sembuf* sops,unsigned nsops);
//参数:
//semid:semget函数返回值(信号量的标识码)
//sops:是指向一个结构数值的指针
//nsops:信号量的个数
//返回值:成功返回0,失败返回-1
【sembuf结构体】
sembuf结构体:
struct sembuf {
short sem_num;//信号量的编号0,1,.....nsems-1
short sem_op;//信号量一次PV操作时加减的数值(一个是“-1”,也就是P操作,等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用)
short sem_flg;//两个取值是IPC_NOWAIT或SEM_UND
};
【信号量实例】
下面所示例子是利用信号量,实现了父子进程的互斥
如果使用了P/V操作会出现如下情况
如果未使用P/V操作呢?
【实现代码】
Makefile
comm.h
#ifndef __COMM_H_
#define __COMM_H_
#include
#include
#include
#include
#define PATHNAME "."
#define PROJ_ID 0x4444
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int createsemset(int nums);//创建sem
int initsem(int semid,int nums,int initval);
int getsemset(int nums);
int P(int semid,int who);//申请资源
int V(int semid,int who);//释放资源
int destroysemset(int semid);//销毁资源
#endif //__COMM_H_
comm.c
#include"comm.h"
static int commsemset(int nums,int flags)
{
key_t key=ftok(PATHNAME,PROJ_ID);
if(key<0){
printf("ftok error\n");
return -1;
}
int semid=semget(key,nums,flags);
if(semid<0){
printf("semget error\n");
return -2;
}
return semid;
}
int createsemset(int nums)
{
return commsemset(nums,IPC_CREAT|IPC_EXCL|0666);
}
int getsemset(int nums)
{
return commsemset(nums,IPC_CREAT);
}
int initsem(int semid,int nums,int initval)
{
union semun _un;
_un.val=initval;
if(semctl(semid,nums,SETVAL,_un)<0){
printf("semctl error\n");
return -1;
}
return 0;
}
static int commPV(int semid,int who,int op)
{
struct sembuf _buf;
_buf.sem_num=who;
_buf.sem_op=op;
_buf.sem_flg=0;
if(semop(semid,&_buf,1)<0){
printf("semop error\n");
return -1;
}
return 0;
}
int P(int semid ,int who)
{
return commPV(semid,who,-1);
}
int V(int semid,int who)
{
return commPV(semid,who,1);
}
int destroysemset(int semid)
{
if(semctl(semid,0,IPC_RMID)<0){
printf("semctl error");
return -1;
}
}
【测试代码】test.c
#include "comm.h"
int main()
{
int semid=createsemset(1);
initsem(semid,0,1);
pid_t id=fork();
if(id==0){
int ssemid=getsemset(0);
while(1){
//P(ssemid,0);
printf("A");
fflush(stdout);
usleep(101000);
printf("A ");
fflush(stdout);
usleep(100000);
//V(ssemid,0);
}
}else{
while(1)
{
//P(semid,0);
printf("B");
fflush(stdout);
usleep(100100);
printf("B ");
fflush(stdout);
usleep(120000);
//V(semid,0);
}
wait(NULL);
}
destroysemset(semid);
return 0;
}
【封装】
1、使用gcc将上述文件生成静态库
【注】在第四步时,链接时-l参数后不加空格制定所需链接的库(库名虽然是libmysem.a,但是这里只需要给出-lmysem);
-L指定库路径,这里的-L.(指的是当前路径);
其中目标文件生成后,静态库删除,程序照样可以运行
2、使用gcc生成动态库
【注】
使用ldd test命令查看test使用动态库,如果libmysemshared无法找到,直接执行test就会出现错误。
解决方法:首先使用export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH将当前目录加入LD_LIBRARY_PATH变量中。再次运行ldd test
另一种编译test.c并链接libmymysemshared.so的方式如下(该方式通过./libmymysemshared.so直接指定使用当前目录下的libmymysemshared.so文件)
----->命令:gcc -o test test.c ./libmymysemshared.so
至此,我们已经了解了在Linux中支持System V 进程通信的三种手段。