信号量用于进程间传递信号的一个整数值。在信号量上只有三种操作可以进行,初始化,递减和增加,这三种操作都是原子操作。递减的操作可以用于阻塞一个进程,增加操作可以用于接触阻塞一个进程。因此信号量也可以称之为一个计数器。
信号量(信号量本身是一个计数器)–是用来描述临界资源的,当中资源的的数目的计数器
信号量就是为了实现同步与互斥机制(保护临界资源)
信号量计数非0即1的叫做二元信号量
但是这个计数器因为也是公共资源是临界资源也是需要被保护起来的
对这个计数器进行++或者–必须都是原子操作
但是++和–自身并非原子操作
信号量的常见操作 P操作和V操作
P操作-1
V操作+1
1、创建一个信号量:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key:所创建或打开信号量集的键值。
nsems:创建的信号量集中的信号量的个数,该参数只在创建信号量集时有效。
semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示
返回值说明:
如果成功,则返回信号量集的IPC标识符。
如果失败,则返回-1,errno被设定成以下的某个值
2、对信号量进行删除操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
功能:控制信号量的信息。
返回值:成功返回0,失败返回-1;
参数:
_semid 信号量的标志码(ID),也就是semget()函数的返回值;
_semnum, 操作信号在信号集中的编号。从0开始。
_cmd 命令,表示要进行的操作。删除操作时cmd设置成为IPC_RMID
semctl() 在 semid 标识的信号量集上,或者该集合的第 semnum 个信号量上执行 cmd 指定的 控制命令。(信号量集合索引起始于零。)根据 cmd 不同,这个函数有三个或四个参数。当有 四个参数时,第四个参数的类型是 union 。调⽤用程序必须按照下⾯面方式定义这个联合体:
union semun
{
int val; // 设置信号量的初始值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用缓存区
};
struct sembuf {
unsigned short sem_num; //信号在信号集的索引
short sem_op; //操作类型(P操作,V操作)
short sem_flg; //操作标志,在设置P,V操作时一般设置成为SEM_UNDO
};
3、进行P V操作时需要用到的函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数: semid表示对哪一个信号量集进行操作
struct sembuf *sops 表示一个指向sembuf结构体的指针,通过对它的成员变量进行设置,表示要操作的是哪一个信号量,进行什么操作,以及SEM_UNDO的设置
接下来编写信号量代码,实现二元信号量对显示器进行保护,实现父子进程输出成对AA或BB
sem.h
#ifndef _SEM_H_
#define _SEM_H_
#define PATHNAME "."
#define PROJ_ID 0X6666
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun
{
int val; // 使⽤用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使⽤用缓存区
unsigned short *array; // GETALL,、SETALL 使⽤用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使⽤用缓存区
};
int CreateSem(int nums);
int DestroySem(int semid);
int GetSem(int nums);
int InitSem(int semid,int which);
int P(int semid,int which);
int V(int semid,int which);
#endif
sem.c
#include"sem.h"
int ComSem(int nums,int flag)
{
key_t key=ftok(PATHNAME,PROJ_ID);
if(key<0)
{
perror("ftok");
return -1;
}
int semid=semget(key,nums,flag|0666);
if(semid<0)
{
perror("semget");
return -1;
}
return semid;
}
static int op_sem(int semid,int op,int which)
{
struct sembuf sem;
sem.sem_num=which;
sem.sem_op= op;
return semop(semid,&sem,1);
}
int P(int semid,int which)
{
int ret=op_sem(semid,-1,which);
if(ret==0)
{
return 0;
}
else
{
return -1;
}
}
int V(int semid,int which)
{
int ret=op_sem(semid,1,which);
if(ret==0)
{
return 0;
}
else
{
return -1;
}
}
int CreateSem(int nums)
{
return ComSem(nums,IPC_CREAT|IPC_EXCL);
}
int GetSem(int nums)
{
return ComSem(nums,IPC_CREAT);
}
int DestroySem(int semid)
{
if(semctl(semid,0,IPC_RMID)<0)
{
perror("semctl");
return -1;
}
return 0;
}
int InitSem(int semid,int which)
{
union semun sem;
sem.val=1;
int ret=semctl(semid,which,SETVAL,sem);
if(ret<0)
{
perror("semctl");
return -1;
}
return 0;
}
test.c
#include"sem.h"
int main()
{
// int semid=CreateSem(12);
// InitSem(semid,1);
// sleep(5);
//DestroySem(semid);
pid_t id=fork();
if(id==0)
{//child
while(1)
{
// P(semid,1);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(123456);
// V(semid,1);
}
}
else
{
while(1)
{
// P(semid,1);
printf("B");
fflush(stdout);
usleep(123456);
printf("B ");
fflush(stdout);
usleep(123456);
// V(semid,1);
}
}
wait(NULL);
return 0;
}
浅谈为什么要设置SEM_UNDO标志位
因为一个进程在运行的时候有可能因为异常而中止,而这里我们采用信号量实现两个不同进程间的同步与互斥机制,那么很可能一个进程刚执行过P操作还没来得及执行V操作,也就是一个进程将它之前所独占的资源上锁了,但由于还没来得及解锁就被异常终止了,那么另一进程就会因为资源没有被解锁而长期得不到资源,处于饥饿状态。为了解决这个问题,引入SEM_UNDO这个标志来解决这个问题。
当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。
它能做到一个回滚操作,如何实现?
如果一个信号量的初始值是1,对它进行P操作是信号量变成0(表示当前资源不可用),在进程异常退出之前它将之前信号量的值恢复成初始值,也就是将它变成之前的初始值1,这样另一个进程就不会因为资源没有被解锁而长期得不到资源,处于饥饿状态。