date: 2019-02-27
tags: OS 6.828
这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。
这次的主题就是说当硬件want attention的时候,kernel该如何进行中断。发生这件事主要有3种情况:
INT
, intended exception)注意在术语上trap
是被当前进程引发的,如system call,而interrupt是由外界device触发的。
device interrupt都来自哪里呢?
中断会告诉kernel,某个设备want attention。kernel中的驱动来负责告诉设备之后该如何do things。
很多时候interrupt handler会直接调用相关的驱动。
trap()
函数是如何知道哪个设备出发了中断?
kernel设置LAPIC/IOAPIC ,把某个类型的中断设置为对应的vector number
IDT (interrupt descriptor table)用vector number来联系一个instruction address
SETGATE
函数。alltraps
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
那么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就不保留了。
思考两个问题(我现在还不明白...)
硬件如何知道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了。
proc.c/scheduler()
: one per CPUvm.c/switchuvm()
tells CPU what kernel stack to use
tells kernel what page table to use
在trapping into kernel之前,CPU应该把eip
保存为正在运行的instruct。
这中间有很长一部分是讲解cpu alarm作业的,这部分内容记录在了对应的作业的博文中。
中断引入了并行的问题:
对于用户代码来说,因为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在归去是相对快的,现在却是相对慢的了。
因为老的方法是所有事件都会触法中断,硬件简单,软件智能。
而新的方法是硬件在中断之前会事先完成一些工作。
处理器spin until device wants attention(按一定周期检查设备是否有需要)
这种方法虽然在设备很慢的时候很浪费,但是设备很快的时候就很好了,因为不需要保存寄存器等等。
If events are always waiting, not need to keep alerting the software. (这句没懂)
Polling versus Interrupt
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?