LEC 9,10,11,12

2023-12-22  本文已影响0人  西部小笼包

LEC 9 Device Drivers

主题:设备驱动程序

编程设备:内存映射I/O

例子设备:UART

内核设备驱动程序如何使用这些寄存器?

设备驱动程序等待的方式?

解决方案:中断

内核如何看到中断?

中断通常只是设备状态可能已更改的提示

void
uartintr(void)
{
  // read and process incoming characters.
  while(1){
    int c = uartgetc();
    if(c == -1)
      break;
    consoleintr(c);
  }

  // send buffered characters.
  acquire(&uart_tx_lock);
  uartstart();
  release(&uart_tx_lock);
}

典型的设备驱动程序结构

让我们看看xv6如何设置中断机制

控制中断的寄存器

xv6的中断设置代码

让我们看看从控制台/UART读取输入的shell

(gdb) c

现在是底半部

kernelvec.S

kerneltrap()

如果多个设备希望同时中断会怎么样?

如果内核在设备请求中断时禁用了中断会怎么样?

// disable device interrupts
static inline void
intr_off()
{
  w_sstatus(r_sstatus() & ~SSTATUS_SIE);
}

中断涉及几种并发形式

  1. 设备和CPU之间
    • 生产者/消费者并行性
  2. 如果启用,中断可能发生在任何两条指令之间!
    • 在代码必须是原子的时候禁用中断
  3. 中断可能在与顶半部并行运行的不同CPU上运行

中断的演进:

如果中断频率非常高呢?

轮询:一种高频事件通知策略

DMA(直接内存访问)可以有效地传输数据

LEC 10 Lock

  1. 为什么要谈论锁?

    • 应用程序希望利用多核处理器以实现并行加速。
    • 内核必须处理并行系统调用以及对内核数据的并行访问
    • 锁有助于正确共享数据,但可能限制并行加速。
  2. 锁的抽象:

    • 锁是一个对象,有acquirerelease两个操作。
    • 锁不一定专门与某个数据关联,由程序员计划数据和锁的对应关系。
  3. 何时需要锁:

    • 当两个线程使用内存位置,且至少有一个是写入时。
    • 持有正确的锁再触碰共享数据。
  4. 锁是否可以自动处理?

    • 考虑语言是否能够将锁与每个数据对象关联起来。
    • 编译器在每次使用周围自动添加acquire/release,减少程序员的遗漏。
    • 通常这个想法太死板,因为程序员通常需要对锁的持有时间进行显式控制。
if present(table1, key1):
      add(table2, key1)

race:

      lock table1
      lock table2
        present(..)
    add()
      unlock table1; unlock table2
  1. 锁的作用:
    • 避免丢失更新。
    • 实现原子多步操作,隐藏中间状态。
    • 帮助操作维护数据结构的不变性。
  1. 锁与模块化

    • 锁使得隐藏模块内部细节变得困难。(防止死锁)
    • 为了避免死锁,需要知道调用的函数获取的锁。
  2. 锁与并行性

    • 锁阻止并行执行,需要以允许每个核使用不同数据和不同锁的方式划分数据和锁。
    • 选择数据/锁的最佳划分是一个设计挑战,可能需要重构代码。
  3. 锁的粒度建议

    • 从大锁开始,例如保护整个模块的一个锁。
    • 只有在必要时才进行细粒度锁设计。
  4. 在xv6中看锁的典型用法

    • uart.c为例,典型的设备驱动布局。
    • uart_tx_lock是唯一的锁,相对较粗粒度。
    • uartputc()uartintr() 的锁的作用。
    • uartputc() -- uart_tx_lock 保护什么?
      1. uart_tx_buf 操作中避免race condition。
      2. 如果队列不为空,UART硬件正在执行队列头部的操作。
      3. 防止对UART写寄存器的并发访问。
  5. 如何实现锁?

    • 为什么不采用如下形式:
      struct lock { int locked; }
      acquire(l) {
        while(1){
          if(l->locked == 0){ // A
            l->locked = 1;    // B
            return;
          }
        }
      }
      
    • 存在A和B之间的竞争,如何原子性地执行A和B?
  6. 原子交换指令:

    • __sync_lock_test_and_set 用于执行原子交换。
    • 硬件中的原子交换保证了在操作期间不会被中断。
    • xv6中的自旋锁实现使用了这个概念。
  7. 为什么使用自旋锁?

    • 自旋锁会浪费CPU
    • 自旋锁指南:持有自旋锁的时间很短,不要在持有自旋锁时放弃CPU。
    • 系统提供"阻塞"锁用于更长的临界区,等待的线程会放弃CPU,但开销较高。
  8. 建议:

    • 如果不必要,不要共享数据。
    • 从少数粗粒度锁开始。
    • 仔细检查代码,了解哪些锁阻止了并行性。
    • 只在需要并行性能时才使用细粒度锁。

LEC 11 Scheduling

上下文切换

proc.h中的struct proc

为什么需要一个单独的调度程序线程?

代码细节:

https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec11-thread-switching-robert/11.6-yield-and-sched

为什么scheduler()在内核中启用中断,使用intr_on()?

可能没有可运行的线程,它们可能都在等待I/O(例如,磁盘或控制台)。启用中断是为了让设备有机会发出完成信号,从而唤醒一个线程。否则,系统将会冻结。

为什么sched()的注释说只能持有p->lock?

为什么在swtch()期间要持有p->lock?

LEC 12 Coordination

线程经常等待特定事件或条件:

  - 等待磁盘读取完成(事件来自中断)
  - 等待管道写入者生成数据(事件来自线程)
  - 等待子进程退出

为什么不直接使用while循环自旋直到事件发生?

示例:uartwrite()和uartintr()在uart.c中的使用

为什么sleep()需要锁参数?

假设只是sleep(chan),我们如何实现?

uart代码如何使用这个(错误的)sleep/wakeup

但是锁怎么办呢?

当uartwrite()在broken_sleep()之前释放锁,会出什么错?

这就是“失去唤醒”的问题。

sleep(chan, lock):调用者必须持有锁,sleep释放锁,返回前重新获取。
wakeup(chan):调用者必须持有锁。

让我们看看proc.c中的wakeup(chan)

让我们看看proc.c中的sleep()

请注意,uartwrite()在循环中包装sleep()
即在sleep()返回后重新检查条件,可能再次休眠的两个原因:

  1. 可能有多个等待者,另一个线程可能已经使用了事件。
  2. kill()即使条件不成立也会唤醒进程。
    所有sleep的使用都包装在循环中,因此它们会重新检查。

另一个协调挑战 - 如何终止一个线程?

问题:线程X不能只是销毁线程Y

问题:一个线程不能释放所有自己的资源

xv6有两种方法来终止进程:exit()kill()

普通情况:进程通过exit()系统调用自愿退出

那么kill(pid)

如果kill()目标正在sleep()呢?

xv6对kill()的解决方案

xv6对kill的规范

总结

上一篇 下一篇

猜你喜欢

热点阅读