《操作系统真象还原》chapter7笔记与总结

First Post:

Last Update:

PART 1

    首先,计算机的中断根据其来源可分为内部外部。外部中断常常是由外部设备发起,或是计算机遇到了某些遭难性错误而发生,相对于内部中断的发生频率来说要小些。因此也仅做了解。

    而内部中断则要更加常见,根据其发出中断来源分为软中断异常。软中断是由软件主动或被动发起的,一般是 “INT” 族的指令主动调用的。这类指令均已在处理器中编码,并通过数据线连接到芯片上。即实际向处理器发出中断的是8259A芯片组。8259A芯片的几个IRQ接口(Interrupt ReQuest:中断请求接口)已经预先和其他的可能发出中断的设备连接好了,对应关系如下(但这些IRQ并不是所有引脚,8259A每个芯片似乎有28个引脚)。

    如IRQ0的时钟中断会在处理器加电以后自动且定期地向8259A芯片发出中断(定期:根据8253计数器的设定频率发生)。

    接下来,只要对8259A芯片进行编程,就能够实现硬件层面的中断控制了,诸如中断屏蔽或中断优先级等。编程仅分为初始化操作,通过ICW1ICW4(Initialization Command Words)初始化,OCW1OCW3(Operation Command Words)操作。

    ICW1:规定8259的连接方式(单片或级联)与中断源请求信号的有效形式(边沿或电平触发)

    ICW2(中断类型码字):设置中断类型码的初始化命令字

    ICW3(级连控制字):标志主片/从片的初始化命令字

    ICW4(中断结束方式字):方式控制初始化命令字

注:

    ICW必须按照顺序分别写入主片和从片,ICW1写入主片0x20,从片0xA0端口;ICW2~4写入主片0x21,从片0xA1端口。

OCW1:用于对中断屏蔽寄存器IMR进行读/写。

OCW2:用于设定中断优先级

OCW3:设置或清除特殊屏蔽方式和读取寄存器状态(IRR 和 ISR)

    OCW无写入顺序要求,OCW1写入主片0x21,从片0xA1端口;OCW2~3写入主片0x20,从片0xA0端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI

/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);

PART 2

    现在,中断已经会正常触发了。但仅仅只是触发中断而已,触发以后的关键——中断处理程序还没能实现。中断发生流程如下:

设备发出中断信号给8259芯片,芯片检测是否屏蔽该设备发出的中断,若未屏蔽,则通知处理器发生中断且告知处理器中断号,否则直接忽略该信号。处理器收到中断信号以后,先将上下文保存,然后关闭中断,访问IDTR(Interrupt Descriptor Table Register)获取中断描述表,以中断号为索引获得对应的中断描述符,通过描述符内容调用对应的中断处理程序。

注:此处所指的上下文是指SS、ESP、EFLAGS、CS、EIP,以及所有通用寄存器。但如果没有发生特权级转移,SS和ESP则不需要被保存,直接沿用即可。

另外需要注意的是,中断的特权级转移同样指会从低特权级向高特权级转移。因此同样也必须要求,触发中断的调用者特权级低于或等于被调用者的特权级

        门描述符如下:主要用到中断门描述符(8 Byte)

    此类描述符将构成中断描述符表,并将其起始地址加载进IDTR(IDT Register)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount;
uint8_t attribute;
uint16_t func_offset_high_word;
};
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;
}

static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str("idt_desc_init done.\n");
}
//intr_entry_table是中断处理函数的入口函数,仅做保存上下文和调用真正的处理函数这两个工作

static void exception_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
idt_table[i] = general_intr_handler;
intr_name[i] = "unknown";
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
//idt_table是真正的中断处理函数
//intr_entry_table中的中断处理函数入口函数中存在指令:
//call [idt_table + %1*4]

PART 3

    时钟频率。

    计算机是时钟分为外部时钟内部时钟

    内部时钟由主板上的晶体振荡器产生,或称之为“晶振”。处理器和南北桥的通信基于该频率,称之为“外频”。外频×倍频=主频,处理器取指、执行的时钟周期基于主频。内部时钟出厂时固定,一般是最快的,单位常为纳秒ns。

    外部时钟是处理器与外部设备之间通信时采用的时序。一般是毫秒ms或秒s级别。

    处理器的速度显然是远快于外部设备的,但只要外部设备需要同计算机进行数据交换,就需要将双方的时钟按照一定比例同步。

一个简单例子是:

    处理器会在每次中断的时候从外部设备的固定端口读取数据。

    假设处理器频率100HZ,外部设备只能接受最高10HZ的传输速率,为了保证数据的稳定传输,就需要将处理器发送中断的频率降低到10HZ。我们显然不能真的去降低处理器的频率,那样未免有过多的浪费了,因此我们另外引入一个“计时器”,让这个计算器代替处理器去发出时钟中断信号,这样就能保证处理器原有的运行频率,同时降低发出中断的频率了。

    当然,处理器要比100HZ,外部设备一般也不会慢到10HZ,这里只是大个好懂的比方罢了。

        例中的“计时器”便是指8253芯片。该芯片自带三个计数器,每个计数器自带三个寄存器。

    计数初值寄存器、减法寄存器、输出锁存器三个寄存器的功能根据名字便能大概理解了。就是将计数初值寄存器放入减法寄存器,同时将计数器的GATE引脚置1,减法计数器就会在每个CLK到来时降低1,当其值为0时,将会发送信号并停止计数/重新开始。不过值得在意的是,计数器的CLK引脚连接的是10MHZ脉冲,而8253的频率只有2MHZ。

    8253的编程更加容易,其只有一个控制字。三个计数器分别对应的端口是0x40~0x42。控制字结构如下:

    而0x43端口将用于写入初值。

但需要注意的是,计数器0的发送端已经和8259A芯片的IRQ0连接好了。也就是说,默认加电以后,这里面就会被自动赋予一个初值并开始发送时钟中断信号。所以时钟中断信号一开始就从这里发出,想要调节频率,只需要修改计数器0中的初值寄存器中的值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */
static void frequency_set(uint8_t counter_port, \
uint8_t counter_no, \
uint8_t rwl, \
uint8_t counter_mode, \
uint16_t counter_value) {
/* 往控制字寄存器端口0x43中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 rwl << 4 counter_mode << 1));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}

/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done\n");
}

插画ID:93758526