zhuzilin's Blog

about

6.828 笔记7

date: 2019-02-27
tags: OS  6.828  

这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。

Lecture 8: Interrupts, System calls, and Exceptions

这次的主题就是说当硬件want attention的时候,kernel该如何进行中断。发生这件事主要有3种情况:

  • Exceptions (page fault, divide by zero)
  • System calls (INT, intended exception)
  • Interrupts (devices want attention)

注意在术语上trap是被当前进程引发的,如system call,而interrupt是由外界device触发的。

device interrupt都来自哪里呢?

  • CPUs,
  • LAPICs (Local Advaned Programmable Interrupt Controller),一个负责接受/发送中断的芯片,集成在CPU内部。
  • IOAPIC (I/O Advanced Programmable Interrupt Controller),通常位于南桥,负责外部IO设备发来的中断。
  • devices
  • data bus
  • interrupt bus

中断会告诉kernel,某个设备want attention。kernel中的驱动来负责告诉设备之后该如何do things。

很多时候interrupt handler会直接调用相关的驱动。

trap()函数是如何知道哪个设备出发了中断?

  • kernel设置LAPIC/IOAPIC ,把某个类型的中断设置为对应的vector number

    • page fault也有vectors
    • LAPIC/IOAPIC是PC的常规硬件,其中每个cpu有一个LAPIC
  • IDT (interrupt descriptor table)用vector number来联系一个instruction address

    • 这个table的内容是怎么设置的可以看下面的SETGATE函数。
    • IDT的格式是由Intel定义的,由kernel设置的
  • 每个vector都会跳到alltraps
  • CPU会通过IDT发送各种trap,其中lower 32 IDT entries有特殊的含义。
  • 在xv6中,system call(IRQ)被设置为0x40.
diagram:
  IRQ or trap, IDT table, vectors, alltraps
  IDT:
    0: divide by zero
    13: general protection
    14: page fault
    32-255: device IRQs
    32: timer
    33: keyboard
    46: IDE
    64: INT

How xv6 sets up the iterrupt vector machinery

那么xv6中interrupt vector机制是如何被设置的呢?

首先是在main.c中前后运行了lapicinit()ioapicinit()tvinit()

lapicinit()中告诉LAPIC这个硬件把例如timer设置为对应的vector number(32)。ioapicinit()设置和redirection table(这是啥。。。)相关的中断。最后tvinit()SETGATE这个宏来把vector number指向code at vector[i],也就是vector number对应的中断发生的时候需要运行的代码。

下面来看一下具体的代码:

首先是lapicinit(),这个函数里面是像这样的方式设置:

lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));

而这个函数是:

volatile uint *lapic;  // Initialized in mp.c

//PAGEBREAK!
static void
lapicw(int index, int value)
{
  lapic[index] = value;
  lapic[ID];  // wait for write to finish, by reading
}

这里的lapic的地址在mp.c/mpinit()中初始化了。

然后看一下tvinit()

extern uint vectors[];  // in vectors.S: array of 256 entry pointers
...
void
tvinit(void)
{
  int i;

  for(i = 0; i < 256; i++)
    SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
  SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);

  initlock(&tickslock, "time");
}

上面的这个vectors中的某一个指会对应如下的一段汇编,如:

# vector.S
.globl vector32
vector32:
  pushl $0
  pushl $32
  jmp alltraps

vector.S是由vector.pl生成的(貌似之前提到过)。先pushl一个fakes "error" slot in trapframe(应该是指$0了),因为硬件对于某些trap并不push(没懂)。第二个pushl就是vector number了,对应tf->trapno

下面的这个SETGATE宏是上面用来设置IDT里面的每个vector用的。

// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
//   interrupt gate clears FL_IF, trap gate leaves FL_IF alone
// - sel: Code segment selector for interrupt/trap handler
// - off: Offset in code segment for interrupt/trap handler
// - dpl: Descriptor Privilege Level -
//        the privilege level required for software to invoke
//        this interrupt/trap gate explicitly using an int instruction.
#define SETGATE(gate, istrap, sel, off, d)                \
{                                                         \
  (gate).off_15_0 = (uint)(off) & 0xffff;                \
  (gate).cs = (sel);                                      \
  (gate).args = 0;                                        \
  (gate).rsv1 = 0;                                        \
  (gate).type = (istrap) ? STS_TG32 : STS_IG32;           \
  (gate).s = 0;                                           \
  (gate).dpl = (d);                                       \
  (gate).p = 1;                                           \
  (gate).off_31_16 = (uint)(off) >> 16;                  \
}

设置好之后就可以让中断跳进alltraps了。

tvinit中大多数都是机械性的设置,唯有T_SYSCALL里面设置了istrap=1,也就是让系统在进行system call的时候仍然保留中断,而其他的device interrupt就不保留了。

思考两个问题(我现在还不明白...)

  • 为什么在system call的过程中允许中断?
  • 为什么在interrupt handing之中disable interrupt。

硬件如何知道interrupt里面调用的代码该用user stack还是kernel stack(例如hw xv6 cpu alarm就是用user space)?

  • 当因为中断从user space到kernel space的时候,hardware-defined TSS (task state segment) 让kernel获取CPU的一些详细信息,如寄存器状态,I/O permission之类的。这样就可以知道是应该使用哪个stack了。

    • TSS是one per CPU,从而使得不同CPU可以运行不同的进程,并take traps on different stacks
  • proc.c/scheduler(): one per CPU
  • vm.c/switchuvm()

    tells CPU what kernel stack to use

    tells kernel what page table to use

在trapping into kernel之前,CPU应该把eip保存为正在运行的instruct。

这中间有很长一部分是讲解cpu alarm作业的,这部分内容记录在了对应的作业的博文中。

Interrupt more generally

中断引入了并行的问题:

  • 因为代码运行过程中可能会有其他的代码因中断而被运行。
  • 对于用户代码来说,因为kernel会存储状态,所以影响不大,但是对于kernel代码来说,就可能很糟糕了。

    例如:

    my code:               interrupt:
        %eax = 0
        if %eax = 0 then        %eax = 1
          f()

    我们不知道f是不是被执行了。

    所以为了让一段代码atomic,我们需要关闭中断,也就是使用

    cli()  // 和汇编中的cli,也就是clear interrupt同义
    sti()  // 和汇编中的sti,也就是set interrupt同以

这是我们对并行的初探,之后讨论锁的时候还会再来讨论。

Interrupt evolution

  • Interrupt在归去是相对快的,现在却是相对慢的了。

    因为老的方法是所有事件都会触法中断,硬件简单,软件智能。

    而新的方法是硬件在中断之前会事先完成一些工作。

  • 一些设备可以在不到1微秒的时间内生成中断。比如GB ethernet。
  • 而中断却需要大约微妙时间量级。因为需要保存和恢复状态,同时中断伴随着cache misses。
  • 那么我们该如何处理间隔小于1微秒的中断呢?

Polling: 另一种和设备交互的方法。

  • 处理器spin until device wants attention(按一定周期检查设备是否有需要)

    这种方法虽然在设备很慢的时候很浪费,但是设备很快的时候就很好了,因为不需要保存寄存器等等。

    If events are always waiting, not need to keep alerting the software. (这句没懂)

Polling versus Interrupt

  • 对high-rate device用polling,慢的用interrupt
  • 也可以在polling和interrupt之间相互切换,如果rate is low用interrupt,反之用polling
  • Faster forwarding of interrupts to user space(这里没明白是说polling会帮助还是interrupt可以有更好的机制)

    for page faults and user-handled devices

    h/w delivers directly to user, w/o kernel intervention?

    faster forwarding path through kernel?