文章目錄
轉載地址(本文也是轉載):http://blog.chinaunix.net/uid-15443744-id-2772595.html
在這個部分,我將為大家詳細介紹SagaLinux_irq中是如何處理中斷的。為了更好的示範軟硬體互動實現中斷機制的過程,我將在前期實現的SagaLinux上加入對一個新中斷——定時中斷——的支援。
首先,讓我介紹一下SagaLinux_irq中涉及中斷的各部分代碼。這些代碼主要包含在kernel目錄下,包括idt.c,irq.c,i8259.s,boot目錄下的setup.s也和中斷相關,下面將對他們進行討論。
1、boot/setup.s
setup.s中相關於中斷的部分主要集中在pic_init小結,該部分完成了對中斷控制器的初始化。對8259A的編程是通過向其相應的連接埠發送一系列的ICW(初始化命令字)完成的。總共需要發送四個ICW,它們都分別有自己獨特的格式,而且必須按次序發送,並且必鬚髮送到相應的連接埠,具體細節請查閱相關資料。
pic_init:
cli
mov al, 0x11 ; initialize PICs
; 給中斷寄存器編程
; 發送ICW1:使用ICW4,級聯工作
out 0x20, al ; 8259_MASTER
out 0xA0, al ; 8259_SLAVE
; 發送 ICW2,中斷起始號從 0x20 開始(第一片)及 0x28開始(第二片)
mov al, 0x20 ; interrupt start 32
out 0x21, al
mov al, 0x28 ; interrupt start 40
out 0xA1, al
; 發送 ICW3
mov al, 0x04 ; IRQ 2 of 8259_MASTER
out 0x21, al
; 發送 ICW4
mov al, 0x02 ; to 8259_SLAVE
out 0xA1, al
; 工作在80x86架構下
mov al, 0x01 ; 8086 Mode
out 0x21, al
out 0xA1, al
; 設定中斷屏蔽位 OCW1 ,屏蔽所有插斷要求
mov al, 0xFF ; mask all
out 0x21, al
out 0xA1, al
sti
2、kernel/irq.c
irq.c提供了三個函數enable_irq、disable_irq和request_irq,函數原型如下:
void enable_irq(int irq)
void disable_irq(int irq)
void request_irq(int irq, void (*handler)())
enable_irq和disable_irq用來開啟和關閉右參數irq指定的中斷,這兩個函數直接對8259的寄存器進行操作,因此irq對應的是實實在在的中斷號,比如說X86下時鐘中斷一般為0號中斷,那麼啟動時鐘中斷就需要調用enable_irq(1),而鍵盤一般佔用2號中斷,那麼關閉鍵盤中斷就需要調用disable_irq(2)。irq對應的不是中斷向量。
request_irq用來將中斷號和中斷服務程式綁定起來,綁定完成後,命令8259開始接受插斷要求。下面是request_irq的實現代碼:
void request_irq(int irq, void (*handler)())
{
irq_handler[irq] = handler;
enable_irq(irq);
}
其中irq_handler是一個擁有16個元素的數組,數組項是指向函數的指標,每個指標可以指向一個中斷服務程式。irq_handler[irq] = handler 就是一個給數組項賦值的過程,其中隱藏了中斷號向中斷向量映射的過程,在初始化IDT表的部分,我會介紹相關內容。
3、kernel/i8259.s
[2]
i8259.c負責對外部中斷的支援。我們已經討論過了,8259晶片負責接收外部裝置——如定時器、鍵盤、音效卡等——的中斷,兩塊8259共支援16個中斷。
我們也曾討論過,在編寫作業系統的時候,我們不可能知道每個中斷到底對應的是哪個中斷服務程式。實際上,通常在這個時候,中斷服務程式壓根還沒有被編寫出來。可是,X86體系規定,在初始化中斷向量表的時候,必須提供每個向量對應的服務程式的位移地址,以便CPU在接收到中斷時調用相應的服務程式,這該如何是好呢?
巧婦難為無米之炊,此時此刻,我們只有創造所有中斷對應的服務程式,才能完成初始化IDT的工作,於是我們製造出16個函數——__irq0到__irq15,在註冊中斷服務程式的時候,我們就把它們填寫到IDT的描述符中去。(在SagaLinux中當前的實現裡,我並沒有填寫完整的IDT表,為了讓讀者看得較為清楚,我只加入了定時器和鍵盤對應的__irq和__irq1。但這樣一來就帶來一個惡果,讀者會發現在加入新的中斷支援時,需要改動idt.c中的trap_init函數,用set_int_gate對新中斷進行支援。完全背離了我們強調的分隔變化的原則。實際上,只要我們在這裡填寫完整,並提供一個預設的中斷服務函數就可以解決這個問題。我再強調一遍,這不是設計問題,只是為了便於讀者觀察而做的簡化。)
可是,這16個函數怎麼能對未知的中斷進行有針對性的個人化服務呢?當然不能,這16個函數只是一個介面,我們可以在其中留下後門,當新的中斷需要被系統支援時,它實際的中斷服務程式就能被這些函數調用。具體調用關係請參考圖2
圖2:中斷服務程式調用關係
2所示,__irq0到__irq15會被填充到IDT從32到47(之所以映射到這個區間是為了模仿Linux的做法,其實這部分的整個實現都是在模仿Linux)這16個條目的中斷描述符中去,這樣中斷到來的時候就會調用相應的__irq函數。所有irq函數所作的工作基本相同,把中斷號壓入棧中,再調用do_irq函數;它們之間唯一區別的地方就在於不同的irq函數壓入的中斷號不同。
do_irq首先會從棧中取出中斷號,然後根據中斷號計算該中斷對應的中斷服務程式在irq_handler數組中的位置,並跳到該位置上去執行相應的服務程式。
還記得irq.c中介紹的request_irq函數嗎,該函數綁定中斷號和中斷服務程式的實現,其實就是把指向中斷服務程式的指標填寫到中斷號對應的irq_handler數組中去。現在,你應該明白我們是怎樣把一個中斷服務程式加入到SagaLinux中的了吧——通過一個中介層,我們可以做任何事情。
在的實現中,IDT表格中墨綠色的部分——外部中斷對應的部分——可以浮動,也就是說,我們可以任意選擇映射的起始位置,比如說,我們讓__irq0映射到IDT的第128項,只要後續的映射保持連續就可以了。
4、kernel/idt.c
idt.c當然是用來初始化IDT表的了。
在i8259.s中我們介紹了作業系統是如何支援中斷服務程式的添加的,但是,有兩個部分的內容沒有涉及:一是如何把__irq函數填寫到IDT表中,另外一個就是中斷支援了,那異常怎麼支援呢?idt.c負責解決這兩方面的問題。
idt.c提供了trap_init函數來填充IDT表。
void trap_init()
{
int i;
idtr_t idtr;
// 填入系統預設的異常,共17個
set_trap_gate(0, (unsigned int)÷_error);
set_trap_gate(1, (unsigned int)&debug);
set_trap_gate(2, (unsigned int)&nmi);
set_trap_gate(3, (unsigned int)&int3);
set_trap_gate(4, (unsigned int)&overflow);
set_trap_gate(5, (unsigned int)&bounds);
set_trap_gate(6, (unsigned int)&invalid_op);
set_trap_gate(7, (unsigned int)&device_not_available);
set_trap_gate(8, (unsigned int)&double_fault);
set_trap_gate(9, (unsigned int)&coprocessor_segment_overrun);
set_trap_gate(10,(unsigned int) &invalid_TSS);
set_trap_gate(11, (unsigned int)&segment_not_present);
set_trap_gate(12, (unsigned int)&stack_segment);
set_trap_gate(13, (unsigned int)&general_protection);
set_trap_gate(14, (unsigned int)&page_fault);
set_trap_gate(15, (unsigned int)&coprocessor_error);
set_trap_gate(16, (unsigned int)&alignment_check);
// 17到31這15個異常是intel保留的,最好不要佔用
for (i = 17;i<32;i++)
set_trap_gate(i, (unsigned int)&reserved);
// 我們只在IDT中填入定時器和鍵盤要用到的兩個中斷
set_int_gate(32, (unsigned int)&__irq0);
set_int_gate(33, (unsigned int)&__irq1);
// 一共有34個中斷和異常需要支援
idtr.limit = 34*8;
idtr.lowerbase = 0x0000;
idtr.higherbase = 0x0000;
cli();
// 載入IDT表,新的中斷可以用了
__asm__ __volatile__ ("lidt (%0)"
::"p" (&idtr));
sti();
}
首先我們來看看set_trap_gate和set_int_gate函數,下面是它們兩個的實現
void set_trap_gate(int vector, unsigned int handler_offset)
{
trapgd_t* trapgd = (trapgd_t*) IDT_BASE + vector;
trapgd->loffset = handler_offset & 0x0000FFFF;
trapgd->segment_s = CODESEGMENT;
trapgd->reserved = 0x00;
trapgd->options = 0x0F | PRESENT | KERNEL_LEVEL;
trapgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16);
}
void set_int_gate(int vector, unsigned int handler_offset)
{
intgd_t* intgd = (intgd_t*) IDT_BASE + vector;
intgd->loffset = handler_offset & 0x0000FFFF;
intgd->segment_s = CODESEGMENT;
intgd->reserved = 0x0;
intgd->options = 0x0E | PRESENT | KERNEL_LEVEL;
intgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16);
}
我們可以發現,它們所作的工作就是根據中斷向量號計算出應該把指向中斷或異常服務程式的指標放在什麼IDT表中的什麼位置,然後把該指標和中斷描述符設定好就行了。同樣,中斷描述符的格式請查閱有關資料。
現在,來關注一下set_trap_gate的參數,又是指向函數的指標。在這裡,我們看到每個這樣的指標指向一個異常處理函數,如divide_error、debug等:
void divide_error(void)
{
sleep("divide error");
}
void debug(void)
{
sleep("debug");
}
每個函數都調用了sleep,那麼sleep是有何作用?是不是像——do_irq一樣調用具體異常的中斷服務函數呢?
// Nooooo ... just sleep
void sleep(char* message)
{
printk("%s",message);
while(1);
}
看樣子不是,這個函數就是休眠而已!實際上,我們這裡進行了簡化,對於Intel定義好的前17個內部異常,目前SagaLinux還不能做有針對性的處理,因此我們直接讓系統無限制地進入休眠——跟死機區別不大。因此,當然也不用擔心恢複“現場”的問題了,不用考慮棧的影響,所以直接用C函數實現。
此外,由於這17個異常如何處理在這個時候我們已經確定下來了——sleep,既然沒有什麼變化,我們也就不用耗盡心思的考慮去如何支援變化了,直接把函數寫入程式碼就可以了。
Intel規定中斷描述符表的第17-31項保留,為硬體將來可能的擴充用,因此我們這裡將它閑置起來。
void reserved(void)
{
sleep("reserved");
}
下面的部分是對外部中斷的初始化,放在trap_init中是否有些名不正言不順呢?確實如此,這個版本暫時把它放在這裡,以後重構的時候再調整吧。注意,這個部分解釋了我們是如何把中斷服務程式放置到IDT中的。此外,可以看出,我們使用手工方式對中斷向量號進行了映射,__irq0對應32號中斷,而__irq1對應33號中斷。能不能映射成別的向量呢?當然可以,可是別忘了修改setup.s中的pic_init部分,要知道,我們初始化8259的時候定義好了外部中斷對應的向量,如果你希望從8259發來的中斷訊號能正確的觸發相應的中斷服務程式,當然要把所有的接收——處理鏈條上的每個映射關係都改過來。
我們只填充了34個表項,每個表項8位元組長,因此我們把IDT表的長度上限設為34x8,把IDT表放置在邏輯地址起始的地方(如果我們沒有啟用分頁機制,那麼就是線上性空間起始的地方,也就是物理地址的0位置處)。
最後,調用ldtr指令啟用新的中斷處理機制,SagaLinux的初步中斷支援機制就完成了。
擴充新的中斷
下面,我們以定時器(timer)裝置為例,展示如何通過SagaLinux目前提供的中斷服務程式介面來支援裝置的中斷。
IBM PC相容機包含了一種時間測量裝置,叫做可程式化間隔定時器(PIT)。PIT的作用類似於鬧鐘,在設定的時間點到來的時候發出中斷訊號。這種中斷叫做定時中斷(timer interrupt)。在Linux作業系統中,就是它來通知核心又一個時間片斷過去了。與鬧鐘不同,PIT以某一固定的頻率(編程式控制制)不停地發出中斷。每個IBM PC相容機至少都會包含一個PIT,一般來說,它就是一個使用0x40~0x43 I/O連接埠的8254CMOS晶片。
SagaLinux目前的版本還不支援進程調度,因此定時中斷的作用還不明顯,不過,作為一個做常見的中斷源,我們可以讓它每隔一定時間發送一個中斷訊號,而我們在定時中斷的中斷服務程式中計算流逝過去的時間數,然後列印出結果,充分體現中斷的效果。
我們在kernel目錄下編寫了timer.c檔案,也在include目錄下加入了相應的timer.h,下面就是具體的實現。
// 流逝的時間
static volatile ulong_t counter;
// 中斷服務程式
void timer_handler()
{
// 中斷每10毫秒一次
counter += 10;
}
// 初始化硬體和技術器,啟用中斷
void timer_init()
{
ushort_t pit_counter = CLOCK_RATE * INTERVAL / SECOND;
counter = 0;
outb (SEL_CNTR0|RW_LSB_MSB|MODE2|BINARY_STYLE, CONTROL_REG);
outb (pit_counter & 0xFF, COUNTER0_REG);
outb (pit_counter >> 8, COUNTER0_REG);
// 申請0號中斷,TIMER定義為0
request_irq(TIMER, timer_handler);
}
// 返迴流逝過去的時間
ulong_t uptime()
{
return counter;
}
timer_init函數是核心函數,負責硬體的初始化和中斷的申請,對8254的初始化就不多做糾纏了,請查閱有關資料。我們可以看到,申請中斷確實跟預想中的一樣容易,調用request_irq,一行語句就完成了中斷的註冊。
而中斷服務程式非常簡單,由於把8254設定為每10毫秒發送一次中斷,因此每次中斷到來時都在服務程式中對counter加10,所以counter表示的就是流逝的時間。
在kernel.c中,我們調用timer_init進行初始化,此時定時中斷就被啟用了,如果我們的中斷機制運轉順利,那麼流逝時間會不斷增加。為了顯示出這樣的結果,我們編寫一個迴圈不斷的調uptime函數,並把返回的結果列印在螢幕上。如果列印出的數值越來越大,那就說明我們的中斷機制確確實實發揮了作用,定時中斷被驅動起來了。
在kernel.c中:
// 初始化
int i = 0;
timer_init();
i = uptime();
while(1)
{
int temp = uptime();
// 發生變化才列印,否則看不清楚
if (temp != i)
{
printk(" %d ", temp);
i = temp;
}
當SagaLinux_irq引導後,你會發現螢幕上開始不停的列印逐漸增大的數字,系統對定時中斷的支援,確實成功了。
為了驗證中斷支援的一般性,我們又加入了對鍵盤的支援。這樣還可以充分體現中斷對並發執行任務帶來的協助,在你按下鍵盤的時候,定時中斷依然不斷觸發,螢幕上會列印出時間,當然,也會列印出你按下的字元。不過,這裡就不對此做進一步描述了。