梳理linux0.12知識點系列 之 8259A的初始化和時鐘中斷

梳理linux0.12知識點系列

8259A的初始化和時鐘中斷

背景

cpu和設備協同工作的高效方式是使用中斷機制,本例子基於之前的打
開了A20地址線的demo,進一步初始化了8259A終端控制芯片,並
且屏蔽了除了時鐘中斷以外的所有硬件中斷。當始終中斷髮生時,在
屏幕上打印'T' 'I' 'M' 'E' 'R'五個字符

額外修改

除了加入初始化8259A芯片和增加中斷處理函數的邏輯之外,本例子
將gcc和ld的處理方式修改爲32位模式,並且修改了makefile中去掉
二進制頭的操作的偏移量。這是因爲我們會從linux0.12的system.h
代碼中摳出幾個嵌入彙編的宏來使用,這幾個嵌入彙編如果默認用64位
的模式編譯的話生成的二進制是錯誤的

修改細節

  • makefile中

dd if=lan_os of=a.img bs=512 count=17 skip=8 seek=1 conv=notrunc
skip=4096變成了skip=8

ld 加入了 -m elf_i386 選項

gcc 加入了 -m32 選項

  • head.s中

write_char取傳入參數不再從edi寄存器去,而是從[esp+16]這個地址取

mov eax, [esp+16]

cpu與8259A的連接方式

(筆者解讀)從下圖可以看到主板上的物理連接關係,每片8259A有8根輸入線,可以連接8個硬件設備

拿時鐘爲例簡單描述一下8259A和cpu常見的工作流程

1.在機器上電初始化的時候我們對時鐘芯片進行編程,命令它在震動一個固定次數後要給cpu發送信號
2.cpu在運行的時候是不斷的取指令,執行指令的
3.當時鍾芯片到了固定的次數時,它不能直接給cpu的intr引腳發指令,爲什麼呢,因爲如果這麼設計
  cpu就只能夠接收時鐘的消息而不能處理其他硬件的消息了,很顯然我們要處理多種硬件才能使用計
  算機
4.這時候我們需要一個代理人幫我們接收各種硬件的消息,並且有一種機制通知cpu:1)有人發消息
  給你了 2)這個人是某某某。 做這件事情的代理人就是8259A芯片
5.一塊8259A芯片可以連接8個設備,8259A在自己收到消息後通知cpu的intr引腳,cpu在執行完
  當前的最後一條指令後去問8259A是誰發送的中斷請求,這時8259A會把中斷對應的中斷號放在
  數據總線上,cpu通過這個中斷號查找自己的IDT表,找到中斷處理函數並執行。
6.這裏我們能看到8259A的幾個功能
	1)能夠將自己的一個輸入引腳對應到一箇中斷號
	2)必須考慮多箇中斷同時到達的情況,所以8259A可以對不同的中斷有優先級處理和排隊的能力
	3)8259A實際上還有能力屏蔽某個引腳的中斷(這是顯而易見的),在彙編代碼中的實現方式就是
	   給8259A的一個io端口發送一個控制命令字,這個字節是一個掩碼,只有對應位置是0,該中斷
	   才能被轉發給cpu的intr引腳
	4)如下面第一幅圖所示,8259A是可以串聯的,這樣可以控制15個硬件

以下內容摘自《ORANGE’S:一個操作系統的實現》

中斷產生的原因有兩種,一種是外部中斷,也就是由硬件產生的中斷,
另一種是由指令int n產生的中斷。指令int n產生中斷時的情形
如圖3.37所示,n即爲向量號,它類似於調用門的使用。外部中斷的情況
則複雜一些,因爲需要建立硬件中斷與向量號之間的對應關係。外部中斷
分爲不可屏蔽中斷(NMI)和可屏蔽中斷兩種,分別由CPU的兩根引腳NMI
和INTR來接收,如圖3.39所示。

NMI不可屏蔽,因爲它與IF是否被設置無關。NMI中斷對應的中斷向量號爲2
,這在表3.8中已經有所說明。可屏蔽中斷與CPU的關係是通過對可編程中斷
控制器8259A建立起來的。如果你是第一次聽說8259A,那麼你可以認爲它是
中斷機制中所有外圍設備的一個代理,這個代理不但可以根據優先級在同時
發生中斷的設備中選擇應該處

8259A的初始化

爲什麼要初始化:

1.Intel x86異常機制

cpu除了能夠從intr引腳和nmi引腳接收中斷請求從而進入到相應的處理函數
之外,還能夠在自己發生特殊故障的時候以類似的方式進入故障處理邏輯,這就是
異常機制,《ORANGE'S:一個操作系統的實現》書中的表3.8(上圖)展示了各種
可能出現的異常和對應的向量號
異常處理函數和硬件中斷處理函數需要排布在同一張表中,按照一個互相不衝突的
順序排列,這樣cpu在收到中斷或者是異常的時候會以類似的方式處理。

2.IBM 初始化了8259A

IBM的bios在啓動的時候初始化了`0x8-0xf`這幾個號碼作爲8個硬件中斷,這就
與x86的內部異常號衝突了,所以我們如果想要正常使用cpu的功能的話,就必須
對8259A重新初始化,做的事情很簡單,就是把所有的硬件中斷號制定到intel的
保留中斷號之後,也就是從32號開始。

如何初始化8259A:

以下內容摘自《ORANGE’S:一個操作系統的實現》

還好,8259A是可編程中斷控制器,對它的設置並不複雜,是通過向相應的端口寫入
特定的ICW(InitializationCommandWord)來實現的。主8259A對應的端口地址
是20h和21h,從8259A對應的端口地址是A0h和A1h。ICW共有4個,每一個都是具
有特定格式的字節。爲了先對初始化8259A的過程有一個概括的瞭解,我們過一會兒
再來關注每一個ICW的格式,現在,先來看一下初始化過程:
1.往端口20h(主片)或A0h(從片)寫入ICW1。
2.往端口21h(主片)或A1h(從片)寫入ICW2。
3.往端口21h(主片)或A1h(從片)寫入ICW3。
4.往端口21h(主片)或A1h(從片)寫入ICW4。
這4步的順序是不能顛倒的。我們現在來看一下4個如圖3.40所示的ICW的格式。

摳代碼的過程:

首先我們將書中的初始化8259A的部分摳出來,到我們的head.s中,在init_8259a目錄下

init_8259A:
    mov al, 0x11
    out 0x20, al
    call io_delay
    out 0xa0, al
    call io_delay
    mov al, 0x20
    out 0x21, al
    call io_delay
    mov al, 0x28
    out 0xa1, al
    call io_delay
    mov al, 0x4
    out 0x21, al
    call io_delay
    mov al, 0x2
    out 0xa1, al
    call io_delay
    mov al, 0x1
    out 0x21, al
    call io_delay
    out 0xa1, al
    call io_delay
    mov al, 0xfe
    out 0x21, al
    call io_delay
    mov al, 0xff
    out 0xa1, al
    call io_delay
    ret

io_delay:
    nop
    nop
    nop
    nop
    ret

然後把init_8259A這個函數暴露給c代碼

global write_char, open_a20, idt, init_latch, init_8259A, timer_interrupt, set_sti

這裏面除了init_8259A相比於上一次還多暴露了

  1. init_latch (從linux0.12代碼中搬過來,初始化時鐘芯片10ms發送一箇中斷請求)
  2. timer_interrupt (時鐘中斷的處理函數地址)
  3. set_sti (給c代碼暴露一個函數用來執行sti指令)

我們看一下這三個函數的實現

init_latch:
    ;設置頻率100Hz
    mov byte al, 0x36
    mov dword edx, 0x43
    out dx, al
    mov dword eax, LATCH
    mov dword edx, 0x40
    out dx, al
    mov al, ah
    out dx, al
    
timer_interrupt:
    mov al, 0x20
    out 0x20, al
    call do_timer
    iret

set_sti:
	sti
	ret

其中timer_interrupt又調到了c代碼中的do_timer函數去打印字符

在c代碼lan_main.c

extern void write_char(char ch);
extern void open_a20();
extern void init_8259A();
extern void timer_interrupt();
extern void init_latch();
extern void set_sti();

#include "gate_tool.h"

...

void lan_main()
{
	write_char('L');
	write_char('O');
	write_char('V');
	write_char('E');
	open_a20();
	check_a20_valid();
	init_latch();
	init_8259A();
	set_intr_gate(0x20, &timer_interrupt);
	set_sti();
	while(1);
}

...

上次a20地址線的demo中我們的lan_main函數只寫到了check_a20_valid()就結束了,
這次我們繼續往下,分別執行了

  1. init_latch(); 初始化時鐘
  2. init_8259A(); 初始化8259A,這裏按照《ORANGE’S:一個操作系統的實現》,把硬件中斷放到了從0x20開始(linux也是從這裏開始的)
  3. set_intr_gate(0x20, &timer_interrupt); 把第一個硬件中斷0x20的處理函數設置爲timer_interrupt
  4. set_sti(); 使用sti指令開啓中斷

其中set_intr_gate是從linux0.12的system.h文件中摳出來的宏,是幾段嵌入彙編,把它放在了gate_tool.h頭文件中

#ifndef GATE_TOOL_H
#define GATE_TOOL_H

typedef struct desc_struct {
	unsigned long a,b;
} desc_table[256];

extern desc_table idt;

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

#define set_intr_gate(n,addr) \
	_set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)
#endif

關於sti指令的一個疑點

我們從boot.s開始初始化系統的時候執行了cli指令,用來將cpu的eflags寄存器中的是否允許中斷標誌位置空,
從而屏蔽外部中斷
現在我們初始化好了中斷相關的環境了,需要開啓中斷,但是在《linux源碼剖析》的第四章中我們看到

48 pushfl
49 andl $0xffffbfff, (%esp)
50 popfl
51 movl $TSS0_SEL, %eax
52 ltr %ax
53 movl $LDT0_SEL, %eax
54 lldt %ax
55 movl $0, current
56 sti
57 pushl $0x17
58 pushl $init_stack
59 pushfl
60 pushl $0x0f
61 pushl $task0
62 iret

這樣的代碼
這段代碼大概的意思是:已手動構造好了兩個用戶態任務的tss和堆棧,現在準備通過iret指令跳轉到一個任務中
並且使用sti開啓已經設置好的時鐘中斷,在時鐘中斷處理函數中來自動的調度兩個任務
這裏面的疑惑是:
如果在sti之後iret之前時鐘中斷就來的話,邏輯不就有問題?因爲現在的代碼不在任何一個任務中,而切換
任務的前提是已經在一個用戶態任務中。
另外我們又不能先iret到一個用戶態程序中再使用sti開啓中斷,因爲,第一,這不是這個任務該做的事情,
第二,用戶態程序也不能執行sti指令

我們在intel的開發文檔中找到了答案

 The delayed effect of this instruction is provided to allow interrupts to be enabled
 just before returning from a procedure or subroutine. For instance, if an STI 
 instruction is followed by an RET instruction, the RET instruction is allowed to 
 execute before external interrupts are recognized. No interrupts can be recognized 
 if an execution of CLI immediately follow such an execution of STI.

翻譯過來

提供此指令的延遲效果是爲了允許正好在從過程或子例程返回之前啓用中斷。例如,如果STI指令後跟RET指令,
則允許在識別外部中斷之前執行RET指令。如果在這樣的STI執行之後立即執行CLI,則不能識別任何中斷。

就是說如果有一個ret指令在sti之後,則必定是先執行這個ret指令(或者iret),之後纔可能有中斷請求到來。

運行效果

發佈了6 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章