linux之信号量,PV操作

一、信号量是啥?PV操作是啥?

信号量用一句话来总结就是带有等待队列的计数器。信号量也就是对我们的临界资源进行计数。
就像我们去车站买票一样:
当取票机有票的时候,也就是资源计数 >0,那我们就可以直接取票,并且取票机的票数-1;
当取票机没有票的时候,也就是资源计数 <= 0,那我们就得等待取票机补上票,我们才能取票
当票机补一张票,资源计数也就+1,就是多了一份资源。

当信号量S > 0, S表示可用的临界资源数
当信号量S == 0,S表示目前无临界资源数,等待队列中无等待进程
当信号量S < 0, |S| 表示等待队列中的等待资源的进程个数

那么我们的PV操作又是啥?
P操作表示申请一份资源,申请了一份资源代表临界资源数就减少一份,信号量-1;也就是我们取了一张票
V操作表示释放一份资源,释放了一份资源代表临界资源数就增加一份,信号量+1;也就是补票机补了一张票
要注意:P 操作和 V 操作是搭配使用的,有 P 必有 V,有 V 必有 P

二、PV操作为啥是安全的?

为啥我们的PV操作没有加锁也不用考虑线程安全问题呢?
因为PV操作是原子性的。
P原语:

P(s)
{
	s.value = s.value--;
	if (s.value < 0)
	{
		该进程状态置为等待状状态
		将该进程的PCB插⼊相应的等待队列s.queue末尾
	}
}

V原语:

V(s)
{
	s.value = s.value++;
	if (s.value < =0)
	{
		唤醒相应等待队列s.queue中等待的⼀个进程
		改变其状态为就绪态
		并将其插⼊就绪队列
	}
}

三、啥是临界资源?啥是临界区?

临界资源(互斥资源):一次只允许一个进程使用的资源就是临界资源
临界区:在进程中涉及到临界资源的程序代码部分就是临界区
像我们的PV操作都是临界区,因为是对临界资源进行控制。

使用信号量就可以协调多个进程访问一份临界资源。保护临界资源的使用。任意时刻只能有一个线程进入临界区。


信号量其实就是由一个计数器和一个等待队列组成
信号量结构体:

struct semaphore
{
	int num;			//计数器
	pointer_PCB queue;	//等待队列
};

信号量集函数:
1、创建/访问一个信号量集

int semget(key_t key, int nsems, int semflg);
参数:
key:集合的名字
nsems:信号集中信号量的个数
semflg:由九个权限标志构成
返回值:成功返回该信号集的标识码;失败返回-1

2、控制信号量集

int semctl(int semid, int semnum, int cmd, …);
参数:
semid:由semget返回的信号集标识码
semnum:信号集中信号量的序号
cmd:将要采取的动作(有三个可取值)
最后⼀个参数根据命令不同⽽不同
返回值:成功返回0;失败返回-1

3、创建和访问一个信号量集

int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:
semid:是该信号量的标识码,也就是semget函数的返回值
sops:是个指向⼀个结构数值的指针
nsops:信号量的个数
返回值:成功返回0;失败返回-1

sops指向的结构体

struct sembuf {
	short sem_num;	//信号量的标识码,也就是semget返回值
	short sem_op;	//PV操作
	short sem_flg;	//两个取值IPC_NOWAIT或SEM_UNDO
};

sem_op:
① 如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;
② 如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;
③ 如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
sem_flg:
IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
SEM_UNDO:程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。


信号量的生命周期随内核。
我们来看看信号量的实际应用。子进程打印AA,父进程打印BB,正常情况打印出什么?如果没有PV操作打印出什么?

comm.h

#ifndef __COMM_H_
#define __COMM_H_

#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "."
#define PROJ_ID 0x6666

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

int createSemSet(int nums);
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.c

#include "comm.h"
static int commSemSet(int nums, int flags)
{
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if(_key < 0){
		perror("ftok error");
		return -1;
    }
    int semid = semget(_key, nums, flags);
    if(semid < 0){
		perror("semget error");
		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){
		perror("semctl error");
		return -1;
    }
    return 0;
}
static int commPV(int semid, int who, int op)
{
    struct sembuf _sf;
    _sf.sem_num = who;
    _sf.sem_op = op;
    _sf.sem_flg = 0;
    if(semop(semid, &_sf, 1) < 0){
		perror("semop error");
		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){
		perror("semctl error");
		return -1;
    }
}

test_sem.c

#include "comm.h"

int main()
{
    int semid = createSemSet(1);
    initSem(semid, 0, 1);
    pid_t id = fork();
    if(id == 0){
	//child
	int _semid = getSemSet(0);
	while(1){
	    P(_semid, 0);
	    printf("A");
	    fflush(stdout);
	    usleep(123456);
	    printf("A ");
	    fflush(stdout);
	    usleep(321456);
	    V(_semid, 0);
	}
    }
    else{
	//father
	while(1){
	    P(semid, 0);
	    printf("B");
	    fflush(stdout);
	    usleep(223456);
	    printf("B ");
	    fflush(stdout);
	    usleep(121456);
	    V(semid, 0);
	}
	wait(NULL);
    }
    destroySemSet(semid);
    return 0;
}

此时,显示器只有一个,两个进程同时打印,显示器成为临界资源,使用二元信号量进行保护。
结果如下:
在这里插入图片描述
如果没有PV操作打印就会乱序
linux之信号量,PV操作_第1张图片

在我们重新运行的时候会出现如下问题:
linux之信号量,PV操作_第2张图片
这是因为我们的信号量已经被创建。我们可以使用 ipcs -s 查看,然后使用ipcrm -s 删除即可
linux之信号量,PV操作_第3张图片

你可能感兴趣的:(linux)