首页 > 其他分享 >MIT 6.S081 Multiprocessors and locking

MIT 6.S081 Multiprocessors and locking

时间:2023-07-18 22:55:22浏览次数:40  
标签:语句 Multiprocessors locked lock acquire locking lk release S081

why lock

防止多核并行运行下的 race condition 导致的错误。

内核中的数据是典型的 concurrently-accessed 的数据。

race condition and how the lock avoid it

A race condition is a situation in which a memory location is accessed concurrently, and at least one access is a write.

Locks ensure mutual exclusion. 锁的持有与释放之间的语句会被原子化,在 xv6 中,就是 acquirerelease 之间的多条语句,只能被一个进程(CPU)全部执行完了之后,才可能被其他的进程(CPU)执行,这样就避免了 race condition。

acquirerelease 之间的多条指令通常被称为 critical section

lock 在某种意义上是在维护 critical section 中一些数据的不变量(some collection of invariants),这个不变量在 critical section 中可能会被暂时破坏,但是当 critical section 的最开始,以及结束的时候,这个 invariants 一定成立! 例如 kfree 中的 lock,就是在维护 kmem.freelist 一定指向 freelist 的头结点这一不变量。

内核设计的一大主要挑战就是避免锁争用(lock contention)

Code: Locks

xv6 有 spinlocks(自旋锁)和 sleep-locks 两种

spinlock 的实现如下:

// Mutual exclusion lock.
struct spinlock {
    uint locked; // Is the lock held?

    // For debugging:
    char *name;      // Name of lock.
    struct cpu *cpu; // The cpu holding the lock.
};

spinlock 的关键部分在于 locked 字段,locked = 1 时,说明 lock 已经被持有了,locked = 0 时,则说明 lock 未被持有,即 cpu 可以申请持有该锁。

acquire 一个简单但是错误的实现如下:

void acquire(struct spinlock *lk) {
    for (;;) {
        if (lk->locked == 0) {
            lk->locked = 1;
            break;
        }
    }
}

这个实现的问题在与,假设有 cpu1 和 cpu2,cpu1 先观察到 lk->locked == 0 了,正准备执行 lk->locked = 1 时,cpu2 也观察到了 lk->locked == 0,那么就存在两个 cpu 同时拿到锁的情况了。因此,我们必须保证 if (lk->locked == 0)lk->locked = 1 这两条语句被原子(atomic)地执行。即不存在 cpu1 执行完语句 $1$,还没执行语句 $2$ 时就被 cpu1 执行语句 $1$ 的情况。

多核处理器一般会提供原子指令保证这两条语句原子化地执行,在 riscv 中,有 amoswap r, a。该指令读取地址 a 处的值,记为 tmp,然后将寄存器 r 中的值写入 a,再将 tmp 的值写入寄存器 r。该指令实际上就是原子地执行交换地址 a 和寄存器 r 处的值。

所以,acquire 的实现要想不出错,就应该执行原子地交换 val(值为 $1$) 与 l->locked 的值,然后检查 val 的值,如果是 $0$,就说明获取到了锁,否则说明没有获取到,并且执行交换后,l->locked 一定是 $1$。由于执行原子交换,也不可能存在两个 cpu 同时检查到 val 为 $0$ 的情况。

ZfPwFynJepvqgQh

It performs this sequence atomically, using special hardware to prevent any other CPU from using the memory address between the read and the write.

xv6 的 acquire 函数调用了 portable C library 的 __sync_lock_test_and_set 函数,本质上就是上面提到的 amoswap 函数。

xv6 的 release 函数调用 __sync_lock_release(&lk->lock),该函数执行原子地将 lklocked 字段写为 $0$。

为什么不能使用一个普通的写入语句将 lk->locked 的值写为 $0$ 呢?这是因为:普通的赋值语句,riscv 中对应的汇编语句是 store,而 store 本身并不是原子的(可能导致缓存一致性失效?,这里不使用原子指令的副作用感觉不是很清晰 TODO(zwyyy))。

由于每个 cpu 都有自己的 cache,将内存中的某个值写为 $0$,可能要经过先将 $cache$ 中的对应值写为 $0$,再写回内存的过程, 因此 store 指令不是原子的。

编译器在编译代码时,可能会重排指令以获取更好的性能,例如:

acquire(&lock);
++i;
release(&lock);

编译器可能重排指令导致 ++i 语句优先于 acquire 指令,这就违背了我们的初衷。 xv6 的源码中的 acquire 函数的实现如下:

void acquire(struct spinlock *lk) {
    push_off(); // disable interrupts to avoid deadlock.
    if (holding(lk))
        panic("acquire");
    // On RISC-V, sync_lock_test_and_set turns into an atomic swap:
    //   a5 = 1
    //   s1 = &lk->locked
    //   amoswap.w.aq a5, a5, (s1)
    while (__sync_lock_test_and_set(&lk->locked, 1) != 0)
        ;
    __sync_synchronize();
    lk->cpu = mycpu();
}

acquire 实现中有 __sync_sychronize(); 这么一条语句,它保证了 __sync_synchronize 之前的指令不会在编译器优化时,被重排到 __sync_synchronize 之后,即避免了 critaical section 中的指令被重排到 acquire 之前。

release 的实现中同样有 __sync_synchronzie 语句,它又被称作 memory fence。

Using locks

什么时候需要使用锁?一般来说,当多个 CPU(线程)需要访问同一块内存,并且 CPU(线程)对内存的操作涉及写入(而不仅仅是读取)时,就需要使用锁来保护这块内存上存储的数据,防止 race condition。

不涉及访问同一块内存,就不需要使用锁了吗?答案也是否定的,考虑在 printf 时,我们往往希望一个字符串能够被完整地输出而不与其他进程的 printf 交织。

Deadlock and lock ordering

死锁个人认为就是指锁一直被持有,无法被释放。

最简单的死锁场景就是,一个进程重复申请持有同一个锁,例如:

acquire(lk);
acquire(lk);
++i;
release(lk);

第二个 acquire 必须等第一个 acquire 状态被 release 了才能执行,但是不继续执行又无法走到第一个 release,就产生了死锁。xv6 会检查这种简单的死锁,触发 panic。

再考虑另一种死锁的情况,相比上一种情况更隐蔽,更难探测出来:

// 进程 1
acquire(lk1);
acquire(lk2);
++i;
release(lk2);
release(lk1);

// 进程 2
acquire(lk2);
acquire(lk1);
++i;
release(lk1);
release(lk2);

那么就可能出现这样一种情况,proc1 持有了 lk1,然后 proc2 马上持有了 lk2,接着 proc1 申请持有 lk2,但是 lk2 已经被 proc2 持有了,只能等待 proc2 release(lk2),然后 proc2 申请持有 lk1,同样的, lk1 被 proc1 持有了,因此必须等待 proc1 release(lk1),然而,两个进程都卡在第二个 acquire 这一步,无法执行 release,因此两个进程就只能一直等到地老天荒,这就发生了死锁。

为了避免出现这一问题,我们需要确定不同进程之间,锁对象有一个相同的申请顺序,即都先申请 lk1,再申请 lk2

If a code path through the kernel must hold several locks at the same time, it is important that all code paths acquire those locks in the same order.

标签:语句,Multiprocessors,locked,lock,acquire,locking,lk,release,S081
From: https://www.cnblogs.com/zwyyy456/p/17564357.html

相关文章

  • MIT6.s081/6.828 lectrue1:Introduction and examples
    目前课程官网能够查到2020,2021.2022秋季的课程表,但是视频都是2020年录制的那一版简单复习+回顾下自己的OS学习之旅参考资料:官网:https://pdos.csail.mit.edu/6.828/2022/schedule.html视频翻译:https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/教材英文......
  • MIT6.S081学习笔记--lec 1
    引言操作系统的目标abstractH/W抽象化硬件multiplex多路复用isolation隔离性sharing共享(进程通信,数据共享)security/accesscontrol安全性/权限控制performance性能/内核开销rangeofapplications多应用场景操作系统概览操作系统应该提供的功能:1.多进程支......
  • MIT 6.S081 Isolation & System call entry/exit
    Trap机制程序运行往往需要完成用户空间和内核空间的切换,每当:程序执行系统调用(systemcall);程序出现了pagefault等错误;一个设备触发了中断;都会发生这样的切换。这里用户空间切换到内核空间通常被称为trap,因此有时候我们会说程序“陷入”到内核态。trap机制需要尽可能......
  • CUDA_LAUNCH_BLOCKING=1的作用
    参考资料:[CUDA开发文档]今天在调试Pytorch代码的时候遇到了下面的报错,RuntimeError:CUDAerror:XXX[此处为各种cudaerror]CUDAkernelerrorsmightbeasynchronouslyreportedatsomeotherAPIcall,sothestacktracebelowmightbeincorrect.......
  • MIT 6.S081 操作系统组织架构
    进程概述64位的RISC-V的VAS是39位的,即VA只有39位,而Xv6则只有38位,最大虚拟地址为#defineMAXVA0x3fffffffff。VAS的顶端,即最高位存放了两个page,一个是用于trampoline,一个用于mappingtheprocess'strapframe。Xv6使用这两个page来切换到内核以及返回。......
  • MIT 6.S081 页表
    Paginghardware总的来说,Xv6的虚拟内存到物理内存的映射方式与x64是一致的,都是使用页表来进行映射。区别在于,Xv6只使用了三级页表,而x64则是使用四级页表,另外,二者的页表层级的命名也有区别,对Xv6来说,最高级的页表是L3(其地址存放于寄存器satp中)。每个pagetable含有5......
  • MIT 6.s081 实验环境搭建
    准备工作Linux系统,我是在实验室配的主机上装了DebianBookworm,然后mac通过ssh连接上去进行操作,宿舍里则是使用的wsl2,里面的发行版也是DebianBookworm。开始配置clone源码在~/Documents/code/mit目录下执行gitclonegit://g.csail.mit.edu/xv6-labs-2021,将源码cl......
  • 阻塞队列LinkedBlockingQueue
    入队方法:put和offerput方法共做了以下情况的考虑:(1)队列已满,阻塞等待;(2)队列未满,创建一个node节点放入队列中,如果放完以后队列还有剩余空间,继续唤醒下一个添加线程进行添加。如果放之前队列中没有元素,放完以后要唤醒消费线程进行消费。offer方法仅仅对put方法一点改动,当队列没有可......
  • Java多线程-工具篇-BlockingQueue
    前言:   在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。认......
  • [转]C#阻塞队列BlockingCollection
    BlockingCollection是一个比较冷门的类,我们先看下官方对这个类的定义:简单来说,BlockingCollection就是一个线程安全的阻塞队列,利用阻塞这个特性,我们可以实现进程内的生产者-消费者模式,比如消息转发、日志记录等。下面我们看一个例子,其用来实现消息转发,先定义一个MessageDis......