操作系统—信号量和信号量临界区的保护

用临界区保护信号量,用信号量实现同步

empty:是当前缓冲区还剩几个位置,初始最大值是:BUFFER_SIZE。
生产者每生产一个进程,那么empty进行减减操作,等于0,那么就是缓冲区没有位置了,等于-1,进程就是在睡眠。
消费者每消费一个进程,那么empty进行加加操作,但是不能大于BUFFER_SIZE,因为在缓冲区是空的时候,就是BUFFER_SIZE

full:表示已经生产的内容个数,起始的值是0,缓冲区没有进程。也就是说开始缓冲区是空的。
生产者每生产一个进程,那么full进行加加操作
消费者每消费一个进程,那么full进行减减操作。但是不能小于0.

1.生产者往缓冲区增加进程:生产者相对进程是增加进程;相对缓冲区是减少缓冲区的位置,位置减少。
生产进程就是V操作,每生产一个进程那么v++,从而缓冲区里边多一个进程,缓冲区减少一个位置。
if(s.queue < 0 ) { sleep(sq.queue); } 那么由于生产了一个资源,缓冲区已经满了,所以就需要睡眠一个进程。

2.消费者往缓冲区取走进程:消费者相对进程是减少进程。相对缓冲区是增加缓冲区位置。缓冲区的位置就多了
消费进程就是P操作,每消费一个进程那么P–,那么缓冲区就减少一个进程,缓冲区增加一个位置。
如果有睡眠的进程,就可以唤醒一个进程,从而消费一个进程
if(s.queue <= 0){ wakeup( s.queue ); } 那么就可以唤醒一个等待的进程。

一、背景

008调度主要问题:能够克服竞态条件得到一些不确定结果情况。那么引入了lock锁机制,通过锁机制,形成互斥机制。有了互斥并不完全可以,还需要同步机制。是因为在临界区允许多个进程执行,所以需要同步机制。
在这里插入图片描述

1、 多进程共同完成一个任务。

每个进程都有自己执行的顺序,但并不是每条程序随便执行,而有时需要等待其他进程发的信号。
从信号到进程同步是自然的。但是从信号到信行量是比较大的进步。

合理有序的推进是:需要让“进程走走停停”来保证多进程合作的合理有序。这就是进程同步。
一个进程不是随意的往前执行,执行到一定程度,可能出现阻塞…此时需要等待另外一个进程的信号。才可以继续往下执行。这样才能同步的完成好。

生产者是:不断的往里边放,每放一个counter++while(counter == BUFFER_SIZE); 
//缓冲区满了,那么就进入死循环。含义就是不能再继续往里边存放了。阻塞了,需要等待。
//直到消费者取走一个内容,那么消费者的进程告诉生产者可以存放了。

消费者是:不断的往里边取,每取一个counter--

在这里插入图片描述

2、引出信号量

1、信号只有两种情况(有和无),信号量可以表示多少。

当缓冲器已满时 counter= BUFFER_SIZE   // 此时满了以后,counter不再进行累加
1.生产者P1,缓冲区已满,那么会进入睡眠。       counter= BUFFER_SIZE
2.生产者P2,放入缓冲区,但已满,又进入睡眠  	 counter= BUFFER_SIZE  
--此时来了一个消费者
消费者,消费一个,那么counter减一
if(counter == BUFFER_SIZE - 1)    {
    
    wakeup(生产者);} 
//此时就要唤醒生产者,就把P1唤醒了。
// 此时消费者以为,只有一个在等待,那么就造成了P2不能被唤醒。
单纯从用counter来表示信号,是不够的。因为不能表示信号量,量的多少。

在这里插入图片描述

2、信号量:
所以接下来PC操作就可以根据信号量来实现同步。同步=是否进行睡眠和唤醒。
如果看到信号量是负的,那么生产者需要等待。
如果看到信号量是负的,那么需要消费者来唤醒。

1.当缓冲区满了以后,P1执行,P1进入sleep, sem=-1
// 一个进程的等待,是缓冲区缺少一个位置。所以用-1来表示。
2. 当p2执行,p2进入sleep,				sem=-2
// 表示两个进程再等待。

3.C执行1次, wakeup P1 ,   此时sem= -1
//根据信号量决定,要不要根别人发信号,也就是根据信号量决定唤醒。
4.C再执行1次, wakeup P2 , 此时sem= 0
// 此时缓冲区满了,但是没有溢出来,所以没有需要唤醒的
5.C再执行1次,             此时sem= 1
//此时不需要唤醒和睡眠,因为缓冲区有一个位置了。

6.当P3执行时,             此时sem=0
// 不需要睡眠,缓冲区满了。

为什么会等待? 而关注点是【缓冲区】
缓冲区没资源了, 一个进程申请一个缓冲区资源,缓冲区没资源了,进程才会等待。
在这里插入图片描述
在这里插入图片描述

二、信号量

  • 信号量的引入: 在临界区中希望多个进程运行,那么不光有互斥,也要有同步功能。比如说读写操作,那么可以用量进程进行执行。

抽象数据类型:一个整形(sem)来表示信号量,两原子操作

  • p():sem减1,如果sem<0,等待,否则继续。(信号量减一,小于零,那么当前执行p操作的进程,需要去睡眠。否则p进程继续执行。类似于Lock操作,阻挡操作,lock只允许有一个)
  • v():sem加1,如果sem<=0,唤醒一个等待的P。(信号量加一,如果小于等于0,意味着当前有进程等待着信号量操作p操作,那么会唤醒挂在信号量等待的进程。)

三、信号量使用

信号量的属性:

  1. 信号量是整数
  2. 信号量是被保护的变量
    初始化完成后,唯一改变一个信号量的值的办法是通过p()和v()
    操作必须是原子
  3. p()能够阻塞,v()不会阻塞
  4. 我们假定信号量是“公平的”
    没有线程被阻塞在p()仍然堵塞,如果v()被无限频繁调用(在同一个信号量)
    在实践中,FIFO经常被使用——先来的先唤醒。用队列来存放。尾部放,头部取。
  5. 两种类型信号量:
    二进制信号量:可以是0或1
    一般/计数信号量:可取任何非负值 (多个执行p操作的进程,进入后续操作)
    两者相互表现(给定一个可以实现另一个)
    p操作:可以进程阻塞,挂起进程。
    v操作:可以唤醒进程
  6. 信号量可以用在两个方面
    互斥
    条件同步(调度约束——一个线程等待另一个线程的事情发生)

例如将condition的初值设为1,Ta要等待Tb执行到某一个地方才开始执行,可以加一条con->p()操作,那么Ta就会被挂起。
必须等到Tb执行到con->Tb,确保前面程序完毕了,才可能将Ta唤醒。从而Ta继续往下执行。
在这里插入图片描述
但是有一些更复杂的情况,那么二进制信号量,可能无法解决
那么需要“条件机制”进行完成。
Producer:生产者线程
buffer:缓冲区
Consumer:消费者。
生产者给缓冲区写数据,消费者线程会在缓冲区进行取。
所以会用到互斥和同步。

  • 正确性要求问题:
    1.在任何时间只能有一个线程操作缓冲区(互斥)。——当生产者写数据时,不能消费者不能做操作,多个生产者可以写数据。
    2.当缓冲区为空,消费者必须等待生产者(调度、同步约束)。——缓冲区为空,消费者取不到数据,此时消费者应该睡眠,直到生产者去填数据,才会唤醒消费者。
    3.当缓冲区满,生产者必须等待消费者(调度、同步约束)。——

在这里插入图片描述

  • 每个约束用一个单独的信号量
    1.二进制信号量互斥
    2.一般信号量fullBuffer
    3.一般信号量emptyBuffers
  1. 互斥信号量:一般是1.一个执行,另外一个不可以执行。直到v操作进行释放才可以另外一个去操作。
    在这里插入图片描述

四、信号量实现

1、信号量的概论:

信号量实现的细节。
在操作系统中pv操作如何执行。
信号量本身是整型,用一个整型变量来记录,
P和V加减的具体的值。在p操作的时候,可能出现有进程等着信号量,等如何实现?
等待队列,等待信号量大于等于0的信号量,等待信号量被执行v操作后,唤醒过程。

定义信号量的结构体;
P 消费资源,sem-1,让生产过多的进程睡眠

P(sem s):消费资源,要看信号量
s.value--;
//先对信号量的个数减减,也就是要使用一个信号量。
if(s.value<0){
    
     sleep(s.queue); }
//如果小于0,那么在消费者执行前,要么缓冲区满了(sem=0),要么就是生产者有进程在睡眠(sem<0)。
//如果得不到资源,那么就进入睡眠。那么就进入阻塞队列。先进先出。

消费者调用V的代码:
V生产资源,让消费者唤醒,也就是发信号让生产者生产起来。

V(sem s):生产资源
s.value++;
// 生产了一个资源,那么缓冲区多了一个可利用的地方。
if(s.value <= 0) {
    
     wakeup(s.queue); }
// 如果缓冲区满了,已经溢出来了。如果现在V生产了一个资源,就么就需要从睡眠的进程中,唤醒一个进程。

在这里插入图片描述

2、信号量解决生产者和消费者的问题

生产者是生产资源,v++ 就是缓冲区的位置变多,如果说当缓冲区已经满了,并且有睡眠的进程了,那么就是唤醒进程了。生产者的作用是【生产缓冲区空闲位置】
在这里插入图片描述

  • empty:表示空闲缓冲区的个数,起始是最大值是。也就说全部空闲。

  • full:表示已经生产的内容个数,起始的值是0。也就是说开始缓冲区是空的。

  • mutex:表示互斥信号量,一次只有一个进程能进去。

  • 生产者什么时候会停:肯定会停在一个信号量上面。【当缓冲区为满的时候,也就是空闲缓冲区的个数等于0】

  • (当sem=0的时候,缓冲区是满的。)

1、生产者首先会用【V生产资源】操作检查当前空闲缓冲区的个数,是否有空闲的地方
2、用【P消费资源】操作检查互斥信号量,只能有1个进程进去。所以由1变成0
3、进入信号量,则进行写操作。V操作++,生产资源,那么empty缓存区的地方会减少,那么里边的full资源个数会增多。
4、关闭互斥信号量
5、那么就是empty减少,从而full生产内容的个数会增加。

Producer(item){
    
      //生产者
	而生产者每次首先测试一下p(empty)是否为零。为0就是满了,起始empty=BUFFER_SIZE (则缓冲区的最大存储个数。)
	//--而消费者的v(empty)会增加空间缓冲区的个数。
	p(mutex); // 测试是否等于1,如果等1,则变成0,就可以进去了。
	写入
	p(mutex); //离开,则释放,由0变1
}

在这里插入图片描述

  • 消费者什么时候会停:【生产的内容为0就会停下来,也就是full=0】(生产的个数为0。当缓冲区全部空闲,也就是等于MAX)

  • (当sem=MAX,缓冲区是空的)

1、P消费资源,检查full是当前资源个数。
2、用【P消费资源】操作检查互斥信号量,只能有1个进程进去。所以由1变成0
3、进入信号量,读操作。P操作--,消费资源,那么empty缓存区的地方会增多,那么里边的full资源个数会减少。
4、关闭互斥信号量
5、那么就是empty缓冲区地方会增多,从而full生产内容的个数会减少。

 Consumer(){
    
     //消费者
	首先消费者测试一下P(full)是否为0。是否为零。没有就阻塞
	// --生产者的V(full)来增加内容。
	p(mutex); // 测试是否等于1,如果等1,则变成0,就可以进去了。
	读出
	p(mutex); //离开,则释放,由0变1
}

在这里插入图片描述

在这里插入图片描述

五、信号量临界区的保护

1、为什么保护信号量,引出临界区

什么是信号量:一个整型变量,通过对它的访问和修改,让多个进程有序推进。

  • 那为什么保证empty的数值正确呢?
    生产者看到empty=-1,就要睡眠呢?因为已经在empty缓冲区里边没有容量了。只能睡大街了。
    消费者看到,就要唤醒。
    所以一定要保证empty表示的数值是正确的,才能做好多个程序有序推进。

在这里插入图片描述

临界区:一个只允许一个进程进入的该进程的那段代码。
所以说:读写信号量的代码一定是临界区
例如:修改信号量,要想保证大家共同修改共享的全局变量,那就必须用临界区来进行保护。

在这里插入图片描述

临界区:一个只允许一个进程进入的该进程的那段代码。
信号量需要正确的语义,才能保证程序的正确执行,所以应保护信号量,当一个进程修改信号量时,也就说进入临界区,其他进程不得入内。
一个进程进入临界区时,通过什么方法?【写一段代码】,让其他进程不得进入,并且在退出临界区时,并修改权限,其他进程可以进入了。

2、什么方法来保护信号量

1、基本原则——互斥进入
2、保护原则——有空让进
3、保护原则——优先等待
在这里插入图片描述
例子中【谁去买牛奶】,其实就是【谁进入邻接区】

1、软件方法

1.1轮换法

turn 是全局变量,要么1要么0. 所以是互斥进入。不支持【有空让进】。
例如进程1先执行一次,更改一次。那么下一个进程2,就可以使用,使用后更改。从而进程1在使用…
在这里插入图片描述

1.2、标记法

满足互斥、
会出现问题: 两个进程可能出现无限等待。
进程1中,0:设为true
进程2中,1:设为true
那么进程1会进入while(1:ture)死循环
那么进程2会进入while(0:true)死循环

在这里插入图片描述

1.3、peterson算法——非对称标记

结合了标记和轮转两种思想。

什么时候等待呢?
进程1中:进程2设了标记1:true 并且 自己也准备去买了。此时自己空转,不进行进临界区(不去买牛奶)
在这里插入图片描述

1.4、多进程——面包店算法

队列:先来先服务

在这里插入图片描述
在这里插入图片描述

算法的问题是:太过于复杂。
比如:不停地取号,比较号。 在队列里边取号,如果溢出怎么办?
那么有没有更好、更简单一点的算法呢?

软硬件协同设置。

2、硬件方法

再一次想临界区:只允许一个进程进入。那么为什么会突然去执行第二个进程了呢?是因为调度
被调度:拎一个进程只有被调度采取执行,才可能进入临界区,那么阻止了调度,不就保证了信号量的安全了嘛

  • 那么如何阻止调度呢?
    调度的前提是【中断】,那么阻止了【中断】(中断阻塞了)就不会进行调度了。(时间片到时了, 其实也是因为中断)
    而硬件就可以启动【关中断】,来保护临界区。

2.1 硬件关闭中断寄存器

  • 中断: 中断在每个CPU上面有个中断寄存器INTR。如何工作呢?如果来中断了,那么就在寄存器上面打一个1。而CPU每执行完一条指令,都会看中断寄存器有无1,如果有1,则中断,进入中断程序,修改时间片。就有可能引起调度。

  • 单处理器:有了关闭中断cli(),那么CPU就不会看中断寄存器。

  • 多处理器:每个CPU都有中断寄存器INTR。只能关闭当前CPU不看ITNR,其他CPU不能关闭。

  • 总结:比软件方法简单的多,但是适用的范围是有限的。因为多CPU不支持。
    在这里插入图片描述

2.2 硬件原子指令

  • 什么是上锁呢?
    锁是变量,是整数,是信号量。概念和信号量完全一样。但这个整数不应该主动去保护,而是本身就有保护机制,就是硬件来保护。

那么就需要一句代码原子指令,来保护信号量。
在这里插入图片描述

六、信号量的代码实现(不懂)

信号量可以实现多个执行序列的同步,互相等待交替执行。这种推进不仅在上层应用需要(生产者和消费者),在操作系统内部也有大量的同步。
比如说:一个应用程序进入内核了,要进行磁盘的读写,有时需要等待一些事件,比如磁盘忙。在内存执行的过程中,可能就要停下来。当发生中断时,继续让磁盘工作。

通过代码的学校,可以得到:
1、可以写用户态的代码。写信号量的系统调用,来实现上层应用之间的同步。
2、根据学习代码,操作系统内部增加大量的代码,多个执行序列之间的同步。同步做好了,操作系统合理有序的往前执行。

1、生产者和消费者的例子

伪代码:生产者和消费者的例子
实际的程序:
Producer.c是生产者进程


1----信号量的结构体。   定义全局数组 :semtable[20];
每个信号量的名字(例如empty)
信号量的值和queue队列

2--- 如何打开信号量
在结构体中找到name对应的。
如果没有则创建,并返回下标。
 
3-----用户态程序,打开信号量
main(){
    
    
	sd=sem_open("emtpy");  //申请信号量,打开“emtpy”的信号量
	// - 信号量在操作系统的内部、内核,信号量包括valuse值,和TCB队列和queue
	// - 既然在内核,就需要通过系统调用,来获取共享内存的结构。

	// - 这样就打开了
	大家共同看到了信号量的值,就可以进行是否走和唤醒了
	执行五次,在文件里面写出5个数,每个数4个字节。
	写之前需要判断,是否有空闲缓冲区:sem_wait(sd);
	
4--sem_wait 如何工作呢?
关中断
根据传进来的se找到,内核对应的vaule -1 如果小于零。将字节设为阻塞,放入队列中,调用schedule();切换到其他进程中
开中断。	

}
 

在这里插入图片描述

2、在Linux内部的阻塞

在Linux内部,一个进程在用户态中发出了read系统调用,就要进入内核。最后执行的是bread。
Bread是:到内核中读一个磁盘块。
申请一块内存缓存器,用总线调用技术DMA,逐步读到内存。
启动读命令;
读命令中,要进行进程为阻塞状态,也就是说不让CPU去执行进程,进程要去I/O操作了。
阻塞:停在缓冲区上,缓冲区带着一个信号量。

缓冲区定义一个信号量
bh->b_lock=1; 1表示上锁。 上锁表示没读完,正在读。 读完了以后会解锁。解锁需要中断。
– 接下来有人也行访问这一块时,那么就要等在这个锁上。锁就是信号量。
在这里插入图片描述

所谓睡眠、阻塞就是:把自己放在阻塞队列上。
然后将自己的状态阻塞上,然后schedule();–》switch_to…
在这里插入图片描述

首先传的是:**p,既然传递的是队列,那么就是传递的指向队首的指针。它就是指针的指针。
tmp是局部变量。 将局部变量*P赋值给tmp
*P=current; current就是当前的进程。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43989347/article/details/120296830