• 操作系统学习笔记:进程同步


    互相协作的进程之间有共享的数据,于是这里就有一个并发情况下,如何确保有序操作这些数据、维护一致性的问题,即进程同步。

    从底层到高级应用,同步机制依次有临界区、信号量、管程、原子事务。

    1、临界区

    每个进程有一个代码段称为临界区,共享数据在此进行操作。没有两个进程同时在临界区执行。

    临界区方案是一种协议,即每个进程进入临界区操作都需要请求。实现这一请求的代码称为进入区,从临界区退出的善后工作由退出区,之后是剩余区。

    临界区方案必须满足三项要求:

    1)互斥

    两个进程不能同时在临界区操作

    2)前进

    临界区空闲,如果有进程需要,且不在剩余区,则可参加选择

    3)有限等待

    进程只要有意愿,总有一天会进入临界区,因为进程进入临界区的次数有上限。

    操作系统内部的临界区问题中,非抢占式比较容易,因为进程没有竞争条件;而抢占式则困难得多,因为进程可能会运行在不同处理器上。但抢占式内核更适合实时编程。


    Peterson算法是一种临界区问题算法。

    对于临界区问题,除了软件上进行设计,也可以在硬件层面来解决。现代计算机系统提供了一些特殊硬件指令,可以原子地执行。


    2、信号量

    临界区方案比较复杂,可以使用信号量这个同步工具。

    信号量是一个整数变量,除了初始化,只能通过两个标准原子操作:wait()和signal()来访问。

    wait(s){
    	while(s <= 0)
    		;//当s<=0时,循环等待,直到S变为正数。如果将这个S看做可用资源,就很好理解了。S<=0,代表没有资源
    	s--;//可用资源减一
    }
    
    signal(s){
    	s++;//可用资源加一
    }


    //使用信号量实现临界区问题方案
    do{
    	wait(mutex);
    	//临界区
    	signal(mutex);
    	//剩余区
    }while(true);

    上述例子中,有循环等待,又叫忙等待。忙等待浪费了CPU时钟,这在多道程序系统中,显然是个问题,因为本可以让给其他进程执行。

    不过,这种依靠忙等待实现的信号量又称为自旋锁(spinlock)。自旋锁有一定的优越性,因为无须进行上下文切换,有时上下文切换相比之下更浪费时间)。通常,等待时间如果比较短,就适合用自旋锁。自旋锁常用在多处理器系统中,因为多线程可以用于多处理器,一个线程自旋,另一个线程可以在另一个处理器上运行。

    不过,为了克服忙等的缺点,可以修改wait()和signal()的定义,采用进程堵塞来替代忙等:

    typedef struct {
        int value;//记录了这个信号量的值 
        struct process *list;//储存正在等待这个信号量的进程 
    } semaphore;
    
    wait(semaphore *S) {
        S->value--;
        if(S->value < 0) {//没有资源了
            add this process to S->list;//进入等待队列
            block();//堵塞
        }
    }
    
    signal(semaphore *S) {
        S->value++;
        if(S->value <= 0) {//上面++后,S仍然还<=0,说明资源供不应求,等待者众,于是唤醒等待队列中的一个,意思是说,我做完了,你好自为之。至于是否可以获得资源,看造化。。。就此别过,青山绿水,后会有期,good bye!
            remove a process P from S->list;
            wakeup(P);//切换到就绪状态
        }
    }


    3、管程

    信号量比临界区方便,但如果使用不正确,比如顺序不当,仍然会导致一些错误。

    管程用高级语言封装了信号量,方便程序员调用。

    管程结构确保一次只有一个进程能在管程内活动。但是,进程在管程内 应该怎么理解?难道是进程在管程里面运行?但看上去,是进程调用了管程,依管程的返回信号而行事?

    管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作。在管程入口有一个等待队列,称为入口等待队列。当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另一个进程时,两者必须有一个退出或停止使用管程。在管程内部,由于执行唤醒操作,可能存在多个等待进程(等待使用管程),称为紧急等待队列,它的优先级高于入口等待队列。 


    因此,一个进程进入管程之前要先申请,一般由管程提供一个enter过程;离开时释放使用权,如果紧急等待队列不空,则唤醒第一个等待者,一般也由管程提供外部过程leave。 


    管程内部有自己的等待机制。管程可以说明一种特殊的条件型变量:var c:condition;实际上是一个指针,指向一个等待该条件的PCB(进程控制块)队列。对条件型变量可执行wait和signal操作


    wait(c):若紧急等待队列不空,唤醒第一个等待者,否则释放管程使用权。执行本操作的进程进入C队列尾部; 


    signal(c):若C队列为空,继续原进程,否则唤醒队列第一个等待者,自己进入紧急等待队列尾部。


    (额,从上述描述看,管程可以控制进程等待、唤醒等,从这点来说,进程在管程内是说得过去的)


    生产者-消费者问题(有buffer)
    
    问题描述:(一个仓库可以存放K件物品。生产者每生产一件产品,将产品放入仓库,仓库满了就停止生产。消费者每次从仓库中去一件物品,然后进行消费,仓库空时就停止消费。 
    解答: 
    管程:buffer=MODULE; 
    (假设已实现一基本管程monitor,提供enter,leave,signal,wait等操作)
    
    notfull,notempty:condition; // notfull控制缓冲区不满,notempty控制缓冲区不空; 
    count,in,out: integer;     // count记录共有几件物品,in记录第一个空缓冲区,out记录第一个不空的缓冲区 
    buf:array [0..k-1] of item_type; 
    define deposit,fetch; 
    use monitor.enter,monitor.leave,monitor.wait,monitor.signal;
     
    procedure deposit(item); 
    { 
      if(count=k) monitor.wait(notfull); 
      buf[in]=item; 
      in:=(in+1) mod k; 
      count++; 
      monitor.signal(notempty); 
    } 
    procedure fetch:Item_type; 
    { 
      if(count=0) monitor.wait(notempty); 
      item=buf[out]; 
      in:=(in+1) mod k; 
      count--; 
      monitor.signal(notfull); 
      return(item); 
    } 
    { 
    count=0; 
    in=0; 
    out=0; 
    } 
    
    进程:producer,consumer; 
    producer(生产者进程): 
    Item_Type item; 
    { 
      while (true) 
      { 
        produce(&item); 
        buffer.enter(); 
        buffer.deposit(item); 
        buffer.leave(); 
      } 
    } 
    
    consumer(消费者进程): 
    Item_Type item; 
    { 
      while (true) 
      { 
        buffer.enter(); 
        item=buffer.fetch(); 
        buffer.leave(); 
        consume(&item); 
      } 
    }

    4、原子事务

    有一些操作里面的步骤必须一口气全部执行完,不可分割,结果是要么全部成功,要么就失败。

    这点在数据库技术上体现得淋漓尽致:事务。近来(什么时候的事了?)有将数据库技术应用于操作系统的热潮。

    1)日志

    数据库的数据为什么能保存得那么好?很大程度上是归功于日志。

    最常用的方法是操作数据的时候,先记录日志,再操作数据。

    每条日志记录:

    (1)事务名称

    (2)数据项名称

    (3)旧值

    (4)新值

    事务开始前,记录<t_start>记入日志;

    当事务提交时,记录<t_commit>记入日志;

    如果事务失败,或者系统故障,系统就会检查日志(这一步也许在系统重启之时),凡有<t_start>记录而无<t_commit>的,系统做回滚操作;两条记录都有的,系统则将数据重新写一遍。(有些重写可能是不必要的,但也不会引起错误)

    但这种做法很浪费,因为绝大多数的事务都是成功的。于是引入检查点(checkpoint):

    当系统将数据从内存写入硬盘或稳定存储设备时,记录一个<checkpoint>。以后系统重启时只处理这个checkpoint之后的日志记录。


    2)锁及时间戳

    在并发的情况下,多个事务同时执行,由于事务是原子性的,所以事务并发,其实相当于让一个个事务串行化执行。这里就牵扯到串行调度和非串行调度。

    非串行调度不一定会引起错误,因为事务之间,里面的步骤不一定会相关。将这些步骤打散、组合,可能效率会更高。

    串行处理可以依靠:

    (1)锁

    (2)时间戳

    方案是数据读写时记录时间值:

    W-timestamp(Q)

    R-timestamp(Q)

    Q是数据项,只要操作Q,即记录时间。

    在一个事务中,如果发出read(Q)

    (1)事务开始时间 < W-timestamp(Q),表明值正在被改写,read被拒绝,事务回滚;

    (2)事务开始时间 >= W-timestamp(Q),read,R-timestamp(Q) = MAX(R-timestamp(Q),事务时间);

    如果事务发出write(Q)

    (1)事务开始时间 < R-timestamp(Q),表明值正在被读取,write被拒绝,事务回滚;

    (2)事务开始时间 < W-timestamp(Q),表明值正在被修改,write被拒绝,事务回滚;;

    (3)否则,write


    参考文章:

    http://www.cnblogs.com/sonic4x/archive/2011/07/05/2098036.html

    版权声明:本文为博主原屙文章,喜欢你就担走。

  • 相关阅读:
    洛谷⑨月月赛Round2 官方比赛 OI
    3243 区间翻转
    3279 奶牛健美操
    1959 拔河比赛
    2144 砝码称重 2
    BZOJ1999 树网的核[数据加强版]
    U4704 函数
    U4687 不无聊的序列
    U4699 鸡蛋
    UVA 11212 Editing a Book
  • 原文地址:https://www.cnblogs.com/leftfist/p/4764250.html
Copyright © 2020-2023  润新知