重点来了!!!
信号量(semaphore)有时被称为信号灯,是操作系统用来解决并发中的互斥和同步问题的一种方法。进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
我们先看看PV同步和互斥等基本概念。。。
进程在运行过程中,一般会与其他的进程共享资源,而有些资源具有排他性。一次只能为一个进程所使用,通常把这种一次仅允许一个进程使用的资源称为临界资源(如:打印机、绘图机、一些进程间共享的变量、缓存区)。进程访问临界资源的那段代码称为临界区,也叫临界段。
访问临界资源应遵循如下原则:
1、空闲让进(或有空即进):当进程处于临界区时,可以允许一个请求进出临界区的进程立即进出自己的临界区。
2、忙则等待(或无空则等):当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
3、有限等待:对要求访问临界资源的进程,应保证能在有限的时间内进入自己的临界区。
4、让权等待:当进程不能进入自己的临界区时,应释放处理机。
1、同步
同步是合作进程间的直接制约问题。
进程间的同步是指进程间完成一项任务时直接发生相互作用的关系。
2、互斥
互斥是申请临界资源进程间的间接制约问题。
进程互斥是指系统中各进程互斥使用临界资源。
!!!信号量里面的pv我们也可以理解为P是申请资源,而V是释放资源。
1、P操作定义:
S:=S-1
若S>=0,则执行P操作的进程继续执行;
若S<0,则置该进程为阻塞状态(因为无可以用资源),并将其插入阻塞队列。
2、操作过程:
Procedure P(Var S:Semaphore){
S--;
if(S<0){
阻塞该进程;
将该进程插入信号量S的等待队列;
}
}
** Semphore表示所定义的变量是信号量。
1、 V操作定义:
S:=S+1
若S>0,则执行V操作的进程继续执行;
若S<=0,则从阻塞状态唤醒一个进程,并将其插入就绪队列,然后执行V操作的进程继续执行。
2、操作过程:
Procedure V(Var S:Semaphore){
S++;
if(S <= 0){
从信号量的等待队列中取出队首进程;
将其插入就绪队列;
}
}
比较经典的问题比生产者消费者问题、哲学家进餐问题等都是通过缓冲区的互斥访问,同时还需要注意死锁的问题。。。
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。
一个信号量 S 是个整型变量,它除了初始化外只能通过两个标准原子操作:wait () 和 signal() 来访问:
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
#include
#include
#include
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
在semop函数中,sembuf结构的定义如下:
struct sembuf sops[2];
int semid;
/* Code to set semid omitted */
sops[0].sem_num = 0; /* Operate on semaphore 0 */
sops[0].sem_op = 0; /* Wait for value to equal 0 */
sops[0].sem_flg = 0;
sops[1].sem_num = 0; /* Operate on semaphore 0 */
sops[1].sem_op = 1; /* Increment value by one */
sops[1].sem_flg = 0;
if (semop(semid, sops, 2) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
其中 sem_op 是一次操作中的信号量的改变量:
若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。
若sem_op < 0,请求 sem_op 的绝对值的资源。
在semctl函数中的命令有多种,这里就说两个常用的:
SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
改自:https://www.cnblogs.com/zgq0/p/8780893.html
#include
#include
#include
#include
#include
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
// int semop(int semid, struct sembuf *sops, unsigned nsops);
void getKey(int id){
struct sembuf sops;
sops.sem_num = 0; /* Operate on semaphore 0 */
sops.sem_op = -1; /* Wait for value to equal 0 */
sops.sem_flg = SEM_UNDO;
semop(id,&sops,1);
printf("get key suceed\n");
if(semop(id, &sops,1) == -1) {
perror("semop");
exit(-1);
}
}
void bacKey(int id){
struct sembuf sops;
sops.sem_num = 0; /* Operate on semaphore 0 */
sops.sem_op = 1; /* Wait for value to equal 0 */
sops.sem_flg = SEM_UNDO;
semop(id,&sops,1);
printf("back key suceed\n");
if(semop(id, &sops, 1) == -1) {
perror("semop");
exit(-1);
}
}
int main(){
key_t key;
key = ftok(".",2);
int semid;
// int semget(key_t key, int nsems, int semflg);
semid = semget(key,1,IPC_CREAT|0666);//Create semaphore
//这里信号量集我们就设置了1个
// int semctl(int semid, int semnum, int cmd, ...);
union semun initset;
initset.val = 0;
semctl(semid,0,SETVAL,initset);//Initialize the semaphore
int pid = fork();
while(1){
if(pid > 0){
//get key
getKey(semid);
printf("this is father\n");
//back key
bacKey(semid);
}else if(pid == 0){
printf("this is child\n");
bacKey(semid);
}else{
printf("fork error\n");
}
sleep(3);
printf("\n");
}
return 0;
}
int semctl(int semid, int semnum, int cmd, …);手册里面说了里面可以包含3个或者4个参数,第四个参数是一个联合体,里面有初始值等配置
在实例的运行结果中,我们可以看到,先执行child再执行father,因为一开始没有锁,也就是你P操作根本拿不到锁,导致阻塞,进而先去执行child,child执行完之后放锁,父进程才可以执行。
相比较之前没有PV操作的结果,我们可以控制进程的先后顺序,而不是父进程和子进程随意执行。