美文网首页
操作系统:同步互斥

操作系统:同步互斥

作者: liuzhangjie | 来源:发表于2019-01-21 16:56 被阅读0次

    第9章:同步互斥

    背景
    现实生活中的同步问题
    临界区
    方法1:禁用硬件中断
    方法2:基于软件的解决方法
    方法3:更高级的抽象方法

    9.1 背景

    同步互斥是操作系统当中协调进程之间动作和相互关系的一种机制。

    并发进程的正确性

    • 独立进程:
      不和其他进程共享资源或状态
      确定性–输入状态决定结果
      可重现–能够重现起始条件
      调度顺序不重要
    • 并发进程:
      在多个进程间共享资源
      不确定性
      不可重现
    • 并发进程的正确性:
      执行进程是不确定性和不可重现的
      程序错误可能是间歇性发生的
    • 进程并发执行的好处
      进程需要与计算机中的其他进程和设备进行协作
      好处1:共享资源
      好处2:加速
      I/O操作和CPU计算可以重叠(并行)
      程序可划分成多个模块放在多个处理器上并行执行
      好处3:模块化
      将大程序分解成小程序
      使系统易于复用和扩展

    并发创建新进程时的标识分配

    • 程序可以调用函数fork()来创建一个新的进程
      操作系统需要分配一个新的并且唯一的进程ID
      在内核中,这个系统调用会运行 new_pid = next_pid++
      翻译成机器指令
      两个进程并发执行时的预期结果(假定是 next_pid = 100)
      一个进程得到的ID应该是100
      另一个进程的ID应该是101
      next_pid应该增加到102
    • 新进程标识中的可能错误:
      两个不同的新进程分配了一样的new_pid,且next_pid只增加了1

    原子操作(Atomic Operation)

    defs:原子操作是指一次不存在任何中断或失败的操作
    要么操作成功完成
    要么操作没有执行
    不会出现部分执行的状态
    操作系统需要利用同步机制在并发执行的同时,保证一些操作是原子操作

    9.2 现实生活中的同步问题

    • 互斥(mutual exclusion)
      一个进程占用资源,其他进程不能使用,必须等待适应资源的进程释放
    • 死锁(deadlock)
      多个进程各占用部分资源,形成循环等待
    • 饥饿(starvation)
      其他进程可能轮流占用资源,一个进程一直得不到资源
    • 进程的交互关系:相互感知程度


    9.3 临界区(critical section)

    • 临界区(critical section)
      进程访问临界资源的一段需要互斥执行的代码
    • 进入区(entry section)
      检查可否进入临界区的一段代码
      如可进入,设置相应“正在访问临界区”标志
    • 退出区(exit section)
      清除“正在访问临界区”状态
    • 剩余区(remainder section)
      代码中的其余部分
    • 临界区访问规则
    1. 空闲则入:
      没有进程在临界区时,任何进程可进入
    2. 忙则等待:
      有进程在临界区时,其他进程均不能进入临界区
    3. 有限等待:
      等待进入临界区的进程不能无限等待
    4. 让权等待(可选):
      不能进入临界区的进程应释放CPU(如转换到阻塞状态)

    临界区的实现方法

    1. 禁用中断
    2. 软件方法
    3. 更高级的抽象方法

    禁用中断

    • 其他进程无法打扰正在运行的进程,当前进程对临界区资源的访问也就不会有问题。但对系统的中断响应会有影响。
    • 在硬件还没有支持的时候,尝试用共享变量协调的方式来做,比较复杂
    • 借用操作系统的支持,来使应用提供同步的服务。由于引入管理者,进程间不对等协调

    9.4 禁用硬件中断

    • 没有中断,没有上下文切换,因此没有并发,整个系统由当前进程独占
      硬件将中断处理延迟到中断被启用之后
      现代计算机体系结构都提供指令来实现禁用中断
      进入临界区
      禁止所有中断,并保存标志
      离开临界区
      使能所有中断,并恢复标志
    • 缺点
      禁用中断中,进程无法被停止
      如果进程执行出了问题,那整个系统都没有办法回来
      整个系统都会为此停下来
      可能导致其他进程处于饥饿状态
      临界区可能很长
      无法确定响应中断所需的时间(可能存在硬件影响)
      要小心使用

    9.5 基于软件的同步方法

    在两个进程之间,通过共享变量的访问,来实现线程之间的同步
    线程可通过共享一些共有变量之间来同步它们的行为

    • 第一次尝试
      共享变量
    int turn = 0
    turn == i//表示允许进入临界区的线程 
    1
    2
    线程Ti的代码
    do {
        while (turn != i);//条件不成立就可以进入临界区
        critical section
        trun = j;
        remainder section
    }  while (1);
    1
    2
    3
    4
    5
    6
    

    若turn不是i,则相当于我本身是i。允许另外一个线程进入临界区,i不允许进入临界区,这时它就一直等待,等待另一个线程把它改成i
    当turn变成i之后,它能进去执行临界区的代码,执行完毕退出后,在退出区,它把turn改成j,也就是另一个线程的ID
    满足“忙则等待”,但是有时不满足“空闲则入”
    Ti不在临界区,Tj想要继续运行,但是必须等待Ti进入临界区后

    • 第二次尝试
      共享变量
    int flag[2];
    flag[0] = flag[1] = 0;
    flag[i] == 1;//表示线程Ti是否在临界区
    1
    2
    3
    线程Ti的代码
    do {
        while (flag[j] == 1);
        flag[i] = 1;
        critical section
        flag[i] = 0;
        remainder section 
    } while (1);
    1
    2
    3
    4
    5
    6
    7
    

    如果flag[j]不是1,则另一个线程不在临界区,这时把自己的标识改成1,进入临界区,出来了把自己的标识置为0
    不满足“忙则等待”

    • 第三次尝试
      共享变量
    int flag[2];
    flag[0] = flag[1] = 0;
    flag[i] == 1;//表示线程Ti想要进入临界区
    1
    2
    3
    

    线程Ti的代码

    do {
        flag[i] = 1;
        while (flag[j] == 1);
        critical section
        flag[i] = 0;
        remainder section
    } whlie (1);
    1
    2
    3
    4
    5
    6
    7
    

    满足“忙则等待”,但是不满足“空闲则入”

    Peterson算法

    满足线程Ti和Tj之间互斥的基于软件的解决方法
    共享变量

    int turn;
    boolean flag[];//表示进程是否准备好进入临界区
    1
    2
    进入区代码
    flag[i] = true;
    turn = j;
    while (flag[j] && turn ==j) //两个条件只要一个不满足就能进去
    1
    2
    3
    退出区代码
    flag[i] = false;
    1
    线程Ti的代码
    do {
        flag[i] = true;
        turn = j;
        while (flag[j] && turn == j);
            critical section
        flag[i] = false;
            remainder section
    } while (1);
    1
    2
    3
    4
    5
    6
    7
    8
    

    Dekkers算法

    线程Ti的代码

    flag[0] := false; 
    flag[1] := false;
    turn := 0;//orl
    
    do {
        flag[i] = true;
        while flag[j] == true {
            if turn != i {
                flag[i] := false
                while turn != i{}
                    flag[i] := true
            }
        }
        critical section
        turn := j
        flag[i] = false;
        remainder section
    } while (true);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    

    N线程的软件方法(Eisenberg 和 McGuire)



    特点:

    • 复杂:
      需要两个进程间的共享数据项
      需要忙等待
      浪费CPU时间

    9.6 更高级的抽象方法

    • 硬件提供了一些同步原语
      中断禁用,院子操作指令等
    • 操作系统提供更高级的编程抽象来简化进程同步
      例如:锁、信号量
      用硬件原语来构建

    锁(lock)

    锁是一个抽象的数据结构

    • 一个二进制变量(锁定/解锁)
      Lock :: Acquire()
    • 锁被释放前一直等待,然后得到锁
      Lock :: Release()
      释放锁,唤醒任何等待的进程

    使用锁来控制临界区访问

    lock_next_pid -> Acquire();
    new_pid = next_pid++;
    lock_next_pid -> Release();
    1
    2
    3
    

    原子操作指令

    • 现代CPU体系结构都提供一些特殊的原子操作指令
      测试和置位(Test-and-Set)指令
      从内存单元中读取值
      测试该值是否为1(然后返回真或假)
      内存单元值设置为1
    • 交换指令(exchange)
      交换内存中的两个值

    使用TS指令实现自旋锁(spinlock)

    class Lock {
        int value = 0;//初始化锁里的初值为0
    }
    Lock :: Acquire() {
        while (test-and-set(value));//spin
        //构造它的请求操作原语,即用TS指令去把value的值读出来,并且往里写1。
        //如果是1,则这个操作就相当于是个检查,如果是0,则设为1,循环结束。
    }
    1
    2
    3
    4
    5
    6
    7
    8
    

    如果锁被释放,那么TS指令读取0并将值设置1

    • 锁被设置为忙并且需要等待完成
      如果锁处于忙状态,那么TS指令读取1并将值设置为1

    • 不改变锁的状态并且需要循环

    Lock :: Release() {
        value = 0;
    }
    1
    2
    3
    
    • 特点:
      线程在等待的时候消耗CPU时间

    无忙等待锁

    • 忙等待
    Lock :: Acquire() {
        while (test-and-set(value));//spin
    }
    Lock :: Release() {
        value = 0;
    }
    1
    2
    3
    4
    5
    6
    
    • 无忙等待
    class Lock {
        int value = 0;
        WaitQueue q;//在锁的数据结构里加上一个等待队列,即等待这个锁的相应的进程所排的队列
    }
    Lock :: Acquire() {
        while (test-and-set(value));{
            and this TCB to wait queue q;
            schedule();
    
        }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    

    若查询一次之后,里面是1,就将当前线程放到等待队列,同时执行调度程序,此时其他程序可以继续执行
    一旦切换回来,轮到该线程再运行时,即有释放锁操作,将该线程从等待状态变成就绪状态,此时再去查,若此时它的状态是解锁的状态,则请求完成,进入临界区

    Lock :: Release() {
        value = 0;//表示释放锁
        remove one thread t from q;
        wakeup(t);//将线程从等待队列放到就绪队列。等待时,就处于放弃CPU使用权的状态,实现了让权等待
    }
    1
    2
    3
    4
    5
    

    原子操作指令锁的特征

    • 优点
      运用于单处理器或者共享主存的多处理器中任意数量的进程同步
      中断禁用只适用于单处理机
      简单并且容易证明
      支持多临界区
    • 缺点
      忙等待消耗处理器时间
      可能导致饥饿
      进程离开临界区时有多个等待进程的情况
      并没有做到按先来后到的顺序来使用资源,因为在锁的请求操作当中,放到就绪队列之后会再去检查。若在检查的时候资源出于空闲状态,那就申请到了。回来的时候,实际上各个线程把就绪队列排定的顺序并不见得是申请锁的顺序
      拥有临界区的低优先级进程
      请求访问临界区的高优先级进程获得处理器并等待临界区
      低优先级等待CPU,高优先级等待临界区资源

    同步方法总结

    • 锁是一种高级的同步抽象方法
      互斥可使用锁来实现
      需要硬件支持
    • 常用的三种同步实现方法
      禁用中断(仅限于单处理器)
      软件方法(复杂)
      原子操作指令(单处理器或多处理器均可)

    相关文章

      网友评论

          本文标题:操作系统:同步互斥

          本文链接:https://www.haomeiwen.com/subject/utncjqtx.html