什么是信号量
信号量的实质就是计数器,用来统计临界资源数。这个计数器的值为0或1,称为,二元信号量,二元信号量是用来保护临界资源的。
临界资源:不同进程能够看到的一份公共的资源,且一次仅允许一个进程使用的资源称为临界资源。
临界区:临界区是一段代码,在这段代码中进程将访问临界资源,当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。
信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。
信号量又是一种数据操作锁,在此过程中负责数据操作的互斥、同步等功能。
当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获得一个信号量ID。Linux下定义的信号量结构体:
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量的工作原理
- 信号量本来是为了保护临界资源而产生的,而这里的信号量本身又是一个临界资源。
所以操作系统就保证了信号量的PV操作是原子的。
原子操作:意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。
- 信号量只能进行两种操作,等待和发送信号,即P(sv)和V(sv):
P(sv) —–>如果sv的值大于0,信号量执行减1操作;如果等于0,则挂起该进程的执行。
V(sv) ——>如果有其他进程因等待sv而被挂起,就让它恢复运行, 否则,信号量执行加1操作。
例如,有两个进程共享信号量sv,一旦其中一个进程执行了P操作,它将得到信号量,并可以进入临界区(访问临界资源的代码),使信号量减一。
这时第二个进程想要执行P操作时,信号量为0,将被阻止进入临界区,
它会被挂起以等待第一个进程离开临界区并执行V操作释放信号量。
当第一个进程释放信号量时,信号量执行加1操作,这时第二个进程就可以恢复执行。
- Linux的信号量机制
<1> 在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。
<2> 创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是信号量的一个缺点,因为创建和初始化信号量并不是原子操作。
<3> 即使没有进程在使用IPC资源,它们仍然是存在的,这称之为信号量的生命周期是随内核,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。
同步与互斥
同步:在访问公共资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。
同步与互斥是保证在高效率运行的同时,可以正确运行。大部分情况下同步是在互斥的基础上进行的。
接口函数
- ftok函数
#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);
参数说明:
* ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字
* path 参数就是一个指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。
* id 是子序号, 只使用8bit (1-255)
* 返回值:若成功返回键值,若出错返回-1
2. semget函数: 用来创建一个信号集,或者获取已存在的信号集。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
参数说明:
* key: 所创建或打开信号量集的键值(ftok函数执行的返回值)。
* nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。
* semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。
- IPC_CREAT |
IPC _EXCL
| 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666
- IPC_CREAT : 用于获取一个已经存在的ID
- 返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:
- EACESS : 没有访问该信号量集的权限。
- EEXIST:信号量集已经存在,无法创建。
- EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
- ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
- ENOMEM:没有足够的内存创建新的信号量集。
- semctl函数:用来初始化信号集(带有四个参数),或者删除信号集(关注两个参数)。
#include <sys/types.h>
#include <sys/ipc.h>>
#include <sys/sem.h>
int semctl(int semid, int semun, int cmd, ...);
参数说明:
semid:信号量集I P C 标识符。
* semun:信号集实质上是一个数组,那么该参数则是操作信号在信号量数组中的下标。
* cmd:在semid指定的信号量集合上执行此命令。
第三个参数cmd常用命令:
* IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
* IPC_RMID:从系统中删除该信号量集合。
* SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数,,当初始化信号量时就需要设置该值。
返回值:成功返回一个正数,失败返回-1。
第四个参数是可选的,当要初始化信号量数组中的某个信号量时,就需要初始化该结构体中的val成员。
如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):
4. semop函数::操作一个或一组信号。也可以叫PV操作
#include <sys/types.h>
#include <sys/ipc.h>lude <sys/sem.h>
int semop(int semid, struct sembuf * sops, unsigned nsops);
参数说明:
* semid:信号集的ID,可以通过semget获取。
* sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:
struct sembuf
{
unsigned short sem_num; // 在信号集中的编码 0 , 1, ...
nsems-1 short sem_op; //操作 负值或正值
short sem_flg; // IPC_NOWAIT, SEM_UNDO
};
返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。
代码验证:
场景:父子进程都向显示器(文件设备)上面打印信息,因为父子进程的执行顺序并不一定,所以父子进程进程向显示器上面打印信息这个操作并不是原子的操作。
#include "comm.h"
int main()
{
pid_t id = fork();
if(id==0)
{
int semid = GetSemSet();
while(1)
{
printf("A");
fflush(stdout);
usleep(23445);
printf("A ");
fflush(stdout);
usleep(30000);
}
}
else
{
while(1)
{
printf("B");
fflush(stdout);
usleep(30000);
printf("B ");
fflush(stdout);
usleep(30000);
}
pid_t ret = waitpid(id,NULL,0);
if(ret>0)
{
printf("proc is done\n");
}
}
return 0;
}
运行结果如下,可以看出结果就是随机的
下面就来实现以下,原子的
//comm.h
#ifedif __COMM__
#define __COMM__
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/sem.h>
#include<sys/ipc.h>
#include<string.h>
#include<errno.h>
#define PATHNAME "."
#define ID 6666
union Sem
{
int val;
struct Semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
};
int CreatSemSet(int nums);//创建消息队列
int GetSemSet();
int InitSemSet(int semid,int which,int value);
int P(int semid,int which);
int V(int semid,int which);
int DestorySemSet(int semid);
#endif
//comm.c
#include"comm.h"
static int CommSemSet(int nums,int flags)
{
key_t _k = ftok(PATHNAME,ID);
if(_k<0)
{
perror("ftok");
sleep(2);
return -1;
}
int semid = semget(_k,nums,flags);
if(semid<0)
{
perror("semget");
sleep(2);
return -2;
}
return semid;
}
int CreatSemSet(int nums)//创建消息队列
{
return CommSemSet(nums,IPC_CREAT | IPC_EXCL | 0666);
}
int GetSemSet()
{
return CommSemSet(0,0);
}
int InitSemSet(int semid,int which,int value)
{
union Sem _sem;
_sem.val = value;
if(semctl(semid,which,SETVAL,_sem)<0)
{
perror("semctl");
sleep(2);
return -1;
}
return 0;
}
static int CommPV(int semid,int which,int op)
{
struct sembuf _sf;
_sf.sem_op = op;
_sf.sem_num = which;
_sf.sem_flg = 0;
return semop(semid,&_sf,1);
}
int P(int semid,int which)
{
return CommPV(semid,which,-1);
}
int V(int semid,int which)
{
return CommPV(semid,which,1);
}
int DestorySemSet(int semid)
{
if(semctl(semid,0,IPC_RMID,NULL)<0)
{
perror("semctl");
sleep(2);
return -1;
}
return 0;
}
//sem.c
#include"comm.h"
int main()
{
int semid = CreatSemSet(1);
InitSemSet(semid,0,1);
pid_t id = fork();
if(id==0)
{
int semid = GetSemSet();
while(1)
{
P(semid,0);
printf("A");
fflush(stdout);
usleep(23445);
printf("A ");
fflush(stdout);
usleep(30000);
V(semid,0);
}
}
else
{
while(1)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(30000);
printf("B ");
fflush(stdout);
usleep(30000);
V(semid,0);
}
pid_t ret = waitpid(id,NULL,0);
if(ret>0)
{
printf("proc is done\n");
}
}
return 0;
}
运行结果如下
Linux下,查看信号量的指令,和删除信号量的指令
ipcs -s 查看
ipcrm -s (id) 删除