进程同步与信号量


前言

一般情况下,系统中运行着大量的进程,而每个进程之间并不是相互独立的,有些进程之间经常需要互相传递消息,所以就引出了信号,但是信号不能完全满足需求,又引入了信号量


提示:以下是本篇文章正文内容

一、信号

信号的大致内容可以参考这篇文章
Linux信号基本知识

为了完成进程间的同步或者通信从而引出了信号,但是单靠信号是不能解决问题的,如生产者与消费者典例

注:这里counter 就是一个信号
//生产者
while(true){
    //当 counter == BUFFER_SIZE,生产者 sleep(),不再生产
    if(counter == BUFFER_SIZE)
        sleep();
    ...
    counter++;
    //当生产者发现 counter == 1,又有产品资源了,唤醒消费者
    if(counter == 1)
        wakeup(消费者);
}

//消费者
while(true){
    //当消费者发现 counter == 0,进入sleep,不再消费
    if(counter == 0)
        sleep();
    ...
    counter--;
    //当消费者发现 counter == BUFFER_SIZE - 1,就可以生产了,唤醒生产者
    if(counter == BUFFER_SIZE - 1)
        wakeup(生产者);
}

假设程序执行过程如下:

(1) 缓冲区满以后生产者P1生产一个item放入, 会sleep信号

(2) 又一个生产者P2生产一个item放入, 会sleep

(3) 消费者C执行1次循环, counter==BUFFER_SIZE-1,发信号给P1,P1 wakeup

(4) 消费者C再执行1次循环, counter==BUFFER_SIZE-2, P2不能被唤醒

这就导致进程2不能被执行,所以,单纯依靠counter判断缓冲区的个数是不够的,还要知道有多少进程睡眠了

二、信号量

背景介绍:

原子操作(atomic operation)
原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的

同步与互斥
同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问

临界资源
不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源。

临界区
临界区是一段代码,在这段代码中进程将访问临界资源,当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。

1.引入信号量

单独依靠信号,会导致无法唤醒P2进程。生产者消费者之间的多种映射对应,多个生产者,多个 消费者。由这个原因引出信号量。

信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做锁(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。

信号量的工作原理

若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示一个资源被使用。

若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒

若次信号量的值为负,则进程无法使用资源,进程被阻塞

为了正确地实现信号量,信号量的操作应是原子操作,所以信号量通常是在内核中实现的。

在以上面的问题为例,引入信号量后
1.缓冲区满,P1执行,P1 sleep,sem = -1,表示一个进程睡眠了

2.P2执行,发现缓冲区满,P2 sleep,sem = -2,表示2个进程睡眠了

3.消费者C执行,wakeup P1,sem = -1

4.消费者C再执行一次,wakeup P2,sem = 0

5.消费者C再执行一次,sem = 1,消费者进程睡眠

6.P3执行,sem = 0,表示没有资源了

这样就完美的解决这个问题

种机制的主要思想就是——通过将资源数量化,将申请资源和释放资源的动作具体化,从而达到对资源的操作及结果可视化的程度

2.临界区

通过对信号量的访问和修改,让进程有序推进,但是让程序正确有序的进行的前提是保证信号量的值必须是正确的

信号量操作可能出现的问题

注:这里empty是信号量
//生产者
Producer(item)
{
    P(empty);//生产者先判断 缓存区个数 empty是否满了,empty == 0,阻塞
    ...
}

//生产者P1
register = empty;
register = register - 1;
empty = register;

//生产者P2
register = empty;
register = register - 1;
empty = register;

//初始情况
empty = -1; //空闲缓冲区的个数,-1表示有一个进程在睡眠

//一个可能的执行(调度)
P1.register = empty; // P1.register = -1
P1.register = P1.register - 1; // P1.register = -2

P2.register = empty; // P2.register = -1;
P2.register = P2.register - 1; // P2.register = -2

empty = P1.register; // empty = -2
empty = P2.register; // empty = -2

如果进程真像上面的调度就会导致信号量的值出错。
可能某一次调度会正确执行(进程时间片不知道)如下图
在这里插入图片描述

所以,必须对信号量进行保护操作。临界区就是用来保护信号量的

临界区:
每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。

多个进程涉及到同一个临界资源的的临界区称为相关临界区。使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。

临界区代码的保护原则
1.互斥进入:如果一个进程在临界区中执行,其他进程不允许进入。进程间是互斥关系

2.有空让进:若干进程要求进入空闲临界区时,要尽快使一个进程进入临界区

3.有限等待:从进程 发出进入请求 到 允许进入,不能无限等待

所以,找出进程中的临界区代码(读写信号量的代码)不就可以对信号量保护了吗?

临界区算法

1)软件方式:

1.轮换法
2.标记法
3.非对称标记法
4.Peterson算法(结合标记和轮转思想)
5.面包店算法

2)硬件方式

1.中断控制
一个进程在操作临界区,另一个进程请求进入临界区,一定发生了调度,只要阻止这种调度不进行就可以,只要关闭中断,系统就不会响应

//进程Pi
cli(); //关中断
临界区
sti();//开中断
剩余区

这种方式只适合单核CPU,像UCOS,FreeRTOS这种小型系统都采用的这种。这种方式不适合多核系统。

单核CPU:
中断是在CPU上有一个中断寄存器 INTR,发生中断,寄存器某的某一位置1,CPU每执
行完一个指令,看INTR是否是1,如果是1,就进入中断处理程序,一旦设置了 
cli(),指令执行完,就不判断 INTR 了

多核CPU(不适用):
多CPU时,执行中断,每个CPU对应的 INTR 都置1

假设临界区在 CPU1 上,P1在执行,设置了 cli(), CPU1 上再有中断,就不调度了,CPU1 上的临界区可以一直执行

设 CPU2 在执行P2,设置了 cli(),也不判断中断,P2 也执行
此时 P1 P2 就都在执行临界区了

2.硬件原子指令法
在执行临界区之前上锁,然后执行临界区,执行完开锁,其实这个锁就是一个变量,上锁或者开锁就是给变量赋值
在这里插入图片描述

// TestAndSet是操作锁的,不能被打断
boolean TestAndSet(boolean &x)
{
    //该函数代码 一次执行完毕
    boolean rv = x;
    x = true;
    return rv;
}

//进程Pi
// lock = true 表示上锁,TestAndSet返回true,如果锁上了,Pi就空转
while(TestAndSet(&lock));

临界区; // 没上锁,进入临界区执行

lock = false; // 执行完临界区,解锁
剩余区

3.信号量的操作

有关信号量的操作及相关API可以参考一下链接
systerm-V

4.信号量的类型

信号量有三种类型:

Posⅸ有名信号量:可用于进程或线程间的同步。

Posix基于内存的信号量(无名信号量) :存放在内存区中,可用于进程或线程间的同步。常用于多线程间同步。

System V信号量(IPC机制):在内核中维护,可用于进程或线程间的同步。 常用于进程间同步。

有名信号量通过文件系统中的路径名对应的文件名进行维护(信号量只是通过文件名进
行标识,信号量的值并不存放到这个文件中,除非信号量存放在空间映射到这个文件
上)。

无名信号量通过用户空间内存进行维护,无名信号量要想在进程间通信,该内存必须为
共享内存区。

System V信号量由内核进行维护。

信号量的分类:

二值信号量( binary semaphore):其值或为0或为1的信号量。这与互斥锁类似,若资源被锁住则信号量值为0,若资源可用则信号量值为1。

计数信号量( counting semaphore):其值在0和某个限制值(对于Posiⅸ信号量,该值必须至少为32767)之间的信号量。信号量的值就是可用资源数。

Posix信号量为单个计数信号量,System V信号量为计数信号量集,偏集合的概念。

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

除可以像互斥锁那样使用外,信号量还有一个互斥锁没有提供的特性:互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。

总结

提示:这里对文章进行总结:

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

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值