目录

​1、基本含义​

​使用PV操作实现进程互斥时应该注意的是:​

​2、例子:生产者/消费者模型​

​3.代码实现信号量​

​信号量分类:进程间信号量、线程间信号量​



在计算机操作系统中,PV操作是进程管理中的难点。

1、基本含义

     什么是信号量?信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。

     PV操作的含义:PV操作由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:

    P(S):①将信号量S的值减1,即S=S-1;                                                             

           ②如果S>=0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

    V(S):①将信号量S的值加1,即S=S+1;                                                           

           ②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

 利用信号量和PV操作实现进程互斥的一般模型是:

    进程P1              进程P2           ……          进程Pn
    ……                  ……                           ……
    P(S);              P(S);                         P(S);
    临界区;             临界区;                        临界区;
    V(S);              V(S);                        V(S);
    ……                  ……            ……           ……

使用PV操作实现进程互斥时应该注意的是:

    (1)每个程序中用户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要认真检查其成对性。

    (2)P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。

    (3)互斥信号量的初值一般为1。

利用信号量和PV操作实现进程同步:

    PV操作是典型的同步机制之一。用一个信号量与一个消息联系起来,当信号量的值为0时,表示期望的消息尚未产生;当信号量的值非0时,表示期望的消息已经存在。用PV操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息。

    使用PV操作实现进程同步时应该注意的是:

    (1)分析进程间的制约关系,确定信号量种类。在保持进程间有正确的同步关系情况下,哪个进程先执行,哪些进程后执行,彼此间通过什么资源(信号量)进行协调,从而明确要设置哪些信号量。

    (2)信号量的初值与相应资源的数量有关,也与P、V操作在程序代码中出现的位置有关。

    (3)同一信号量的P、V操作要成对出现,但它们分别在不同的进程代码中。

2、例子:生产者/消费者模型

(1)一个生产者,一个消费者,公用一个缓冲区。

 定义两个同步信号量:

         S_empty——表示缓冲区空位数量,初值为1

         S_ full    ——表示缓冲区占位数量,初值为0

伪代码:

生产者进程
while(TRUE){
生产一个产品;
P(S_empty); //buff有空的吗?
产品送往Buffer;
V(S_full); //buff已满
}

消费者进程
while(True){
P(S_full); //buff有数据?
从Buffer取出一个产品;
V(S_empty); //buff有空
消费该产品;
}

(2)一生产者,一消费者,公用n个(单元的)环形缓冲区。 

定义两个同步信号量:

     S_empty——表示缓冲区空位数量,初值为n

    S_full      ——表示缓冲区占位数量,初值为0

    设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指,指向下一个可用的缓冲区。

伪代码:

生产者进程
while(TRUE){
生产一个产品;
P(S_empty);
产品送往buffer(in);
in=(in+1)mod n;
V(S_full);
}

消费者进程
while(TRUE){
P(S_full);
从buffer(out)中取出产品;
out=(out+1)mod n;
V(S_empty);
消费该产品;
}

(3)一生产者,一消费者,公用n个(单元的)环形缓冲区

    在这个问题中,不仅生产者与消费者之间要同步,而且各个生产者之间、各个消费者之间还必须互斥地访问缓冲区

定义四个信号量:

        empty——表示缓冲区空位数量,初值为n。

        full——表示缓冲区占位数量,初值为0。

        mutex1——生产者之间的互斥信号量,初值为1

        mutex2——消费者之间的互斥信号量,初值为1

    设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

伪代码:

生产者进程
while(TRUE){
生产一个产品;
P(empty);
P(mutex1)
产品送往buffer(in);
in=(in+1)mod n;
V(mutex1);
V(full);
}
消费者进程
while(TRUE){
P(full)
P(mutex2)
从buffer(out)中取出产品;
out=(out+1)mod n;
V(mutex2);
V(empty);
消费该产品;
}

  需要注意的是无论在生产者进程中还是在消费者进程中,两个P操作的次序不能颠倒。应先执行同步信号量的P操作,然后再执行互斥信号量的P操作,否则可能造成进程死锁。


 

代码示例:

【linux】信号量与PV操作 (进程和线程的同步)_临界区

当有进程要求使用共享资源时,需要执行以下操作:

1.系统首先要检测该资源的信号量;

2.若该资源的信号量值大于0,则进程可以使用该资源,此时,进程将该资源的信号量值减1;

3.若该资源的信号量值为0,则进程进入休眠状态,直到信号量值大于0时进程被唤醒,访问该资源;

       当进程不再使用由一个信号量控制的共享资源时,该信号量值增加1,如果此时有进程处于休眠状态等待此信号量,则该进程会被唤醒。

2.信号量的具体结构

每个信号量集都有一个与其相对应的结构,该结构定义如下:

 

/* Data structure describing a set of semaphores.  */  
struct semid_ds
{
struct ipc_perm sem_perm; /* operation permission struct */
struct sem *sem_base; /* ptr to array of semaphores in set */
unsigned short sem_nsems; /* # of semaphores in set */
time_t sem_otime; /* last-semop() time */
time_t sem_ctime; /* last-change time */
};

/* Data structure describing each of semaphores. */
struct sem
{
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last successful semop(), SETVAL, SETALL */
unsigned short semncnt; /* # processes awaiting semval > curval */
unsigned short semzcnt; /* # processes awaiting semval == 0 */
};

信号量集的结构图如下所示:

【linux】信号量与PV操作 (进程和线程的同步)_互斥_02

3.代码实现信号量

使用信号量实现生产者消费者模式

#include "stdafx.h"
#include <stdio.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <conio.h>
#include <ctype.h>
#include <signal.h>
#include <iostream>

#include<Windows.h>
using namespace std;
#pragma comment(lib,"pthreadVC2.lib")

#define N 5 //消费者或者生产者的数目
#define M 10 //缓冲数目

int productin = 0; //生产者放置产品的位置
int prochaseout = 0; //消费者取产品的位置

int buff[M] = {0}; //缓冲区初始化为0,开始时没有产品。

sem_t empty_sem; // 同步信号量,当满的时候阻止生产者放产品。
sem_t full_sem; //同步信号量,当没有产品的时候阻止消费者消费。

pthread_mutex_t mutex; //互斥信号量,一次只有一个线程访问缓冲区。

int product_id = 0; //生产者id
int prochase_id = 0; //消费者id

void SignalExit(int signo)
{
printf("程序退出%d\n",signo);
return;
}

void PrintProduction()
{
printf("此时的产品队列为::");
for(int i = 0; i < M; i++ )
{
printf("%d ",buff[i]);
}
printf("\n\n");
}

//生产者方法
void* Product(void* pramter)
{
int id = ++product_id;
while(1)
{
Sleep(5000); //毫秒
sem_wait(&empty_sem); //给信号量减1操作
pthread_mutex_lock(&mutex);
productin = productin % M;
printf("生产者%d在产品队列中放入第%d个产品\n\n",id,productin+1);
buff[productin] = 1;
PrintProduction();
++productin;

pthread_mutex_unlock(&mutex); //释放互斥量对象
sem_post(&full_sem); //给信号量的值加1操作
}
}

//消费者方法///
void* Prochase( void* pramter )
{
int id = ++prochase_id;
while(1)
{
Sleep(7000);
sem_wait(&full_sem);
pthread_mutex_lock(&mutex);
prochaseout = prochaseout % M;
printf("消费者%d从产品队列中取出第%d个产品\n\n",id,prochaseout+1);

buff[prochaseout] = 0;
PrintProduction();
++prochaseout;

pthread_mutex_unlock(&mutex);
sem_post(&empty_sem);
}
}

int main()
{
cout << "生产者和消费者数目都为5,产品缓冲区为10,生产者每2秒生产一个产品,消费者每5秒消费一个产品" << endl << endl;
pthread_t productid[N];
pthread_t prochaseid[N];

int ret[N];

//初始化信号量
int seminit1 = sem_init(&empty_sem,0,M);
int seminit2 = sem_init(&full_sem,0,0);
if( seminit1 != 0 && seminit2 != 0 )
{
printf("sem_init failed !!!\n");
return 0;
}

//初始化互斥信号量
int mutexinit = pthread_mutex_init(&mutex,NULL);
if( mutexinit != 0 )
{
printf("pthread_mutex_init failed !!\n");
return 0;
}

//创建n个生产者线程
for(int i = 0; i < N; i++ )
{
ret[i] = pthread_create( &productid[i], NULL,Product,(void*)(&i) );
if( ret[i] != 0 )
{
printf("生产者%d线程创建失败!\n",i);
return 0;
}
}

//创建n个消费者线程
for(int j = 0; j < N; j++ )
{
ret[j] = pthread_create(&prochaseid[j],NULL,Prochase,NULL);
if( ret[j] != 0 )
{
printf("消费者%d线程创建失败\n",j);
return 0;
}
}

///等待线程被销毁///
for( int k = 0; k < N; k++ )
{
printf("销毁线程\n");
pthread_join(productid[k],NULL);
pthread_join(prochaseid[k],NULL);
}
return 0;
}

信号量分类:进程间信号量、线程间信号量

 

信号量的种类

1、按用途:进程信号量、线程信号量

2、 三种

        1、posix 有名信号灯 

        2、posix 基于内存的信号灯(无名信号灯 )  //这个是线程用的信号量  因为无名管道就是在父子线程之间的 它也一样

        3、System V信号灯(IPC对象)

 

 

进程间使用的信号灯:

 1、posix 有名信号灯  使用频率不高,不做介绍

 2、System V信号灯    它是一个集合一个或者多个信号灯的集合    它用于进程之间的同步控制 

 

利用PV原语进行操作 它的函数有 : 

 

创建信号灯:

int semget(key_t key, int num_sems, int sem_flags);

操作信号灯:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
对信号灯的控制

int semctl(int semid,int semnum,int cmd,/*union semun arg*/);

进程间信号量用法:

 线程信号量(posix)

      

初始化sem                           

int sem_init(sem_t *sem,int pshared,unsigned int value);

P操作

int sem_wait(sem_t * sem); //阻塞

int sem_trywait(sem_t * sem); //非阻塞

V操作

int sem_post(sem_t * sem);

销毁sem

int sem_destroy(sem_t *sem);

sem_init函数

sem_init函数是Posix信号量操作中的函数。sem_init() 初始化一个定位在 sem 的匿名信号量。value 参数指定信号量的初始值。 pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。

如果 pshared 是非零值,那么信号量将在进程之间共享,并且应该定位共享内存区域(见 shm_open(3)、mmap(2) 和 shmget(2))。因为通过 fork(2) 创建的孩子继承其父亲的内存映射,因此它也可以见到这个信号量。所有可以访问共享内存区域的进程都可以用 sem_post(3)、sem_wait(3) 等等操作信号量。初始化一个已经初始的信号量其结果未定义。

返回值 

sem_init() 成功时返回 0;错误时,返回 -1,并把 errno 设置为合适的值。

用下面一组函数(系统调用)来实现。