Linux中斷虛擬化之二
PIC虛擬化
計(jì)算機(jī)系統(tǒng)有很多的外設(shè)需要服務(wù),顯然,CPU采用輪詢的方式逐個(gè)詢問(wèn)外設(shè)是否需要服務(wù),是非常浪費(fèi)CPU的計(jì)算的,尤其是對(duì)那些并不是頻繁需要服務(wù)的設(shè)備。因此,計(jì)算機(jī)科學(xué)家們?cè)O(shè)計(jì)了外設(shè)主動(dòng)向CPU發(fā)起服務(wù)請(qǐng)求的方式,這種方式就是中斷。采用中斷方式后,在沒(méi)有外設(shè)請(qǐng)求時(shí),CPU就可以繼續(xù)其他計(jì)算任務(wù),而不是進(jìn)行很多不必要的輪詢,極大地提高了系統(tǒng)的吞吐[1] 在每個(gè)指令周期結(jié)束后,如果CPU關(guān)中斷標(biāo)識(shí)(IF)沒(méi)有被設(shè)置,那么其會(huì)去檢查是否有中斷請(qǐng)求,如果有中斷請(qǐng)求,則運(yùn)行對(duì)應(yīng)的中斷服務(wù)程序,然后返回被中斷的計(jì)算任務(wù)繼續(xù)執(zhí)行。
CPU不可能為每個(gè)硬件都設(shè)計(jì)專門的管腳接收中斷,管腳數(shù)量的限制、電路的復(fù)雜度、靈活度等方方面面都不現(xiàn)實(shí),因此,需要設(shè)計(jì)一個(gè)專門管理中斷的單元。由中斷管理單元接受來(lái)自外圍設(shè)備的請(qǐng)求,確定請(qǐng)求的優(yōu)先級(jí),并向CPU發(fā)出中斷。1981年IBM推出的第1代個(gè)人電腦PC/XT使用了一個(gè)獨(dú)立的8259A作為中斷控制器,自此,8259A就成為了單核時(shí)代中斷芯片事實(shí)上的標(biāo)準(zhǔn)。因?yàn)榭梢酝ㄟ^(guò)軟件編程對(duì)其進(jìn)行控制,比如當(dāng)管腳收到設(shè)備信號(hào)時(shí),可以編程控制其發(fā)出的中斷向量號(hào),因此,中斷控制器又稱為可編程中斷控制器(programmable interrupt controller),簡(jiǎn)稱PIC。單片8259A可以連接8個(gè)外設(shè)的中斷信號(hào)線,可以多片級(jí)聯(lián)支持更多外設(shè)。
8259A和CPU的連接如圖5所示。
圖5 8259A和CPU連接
片選和地址譯碼器相連,當(dāng)CPU準(zhǔn)備訪問(wèn)8259A前,需要向地址總線發(fā)送8259A對(duì)應(yīng)的地址,經(jīng)過(guò)譯碼器后,譯碼器發(fā)現(xiàn)是8259A對(duì)應(yīng)的地址,因此會(huì)拉低與8259A的CS連接的管腳的電平,從而選中8259A,通知8259A,CPU準(zhǔn)備與其交換數(shù)據(jù)了。
8259A的D0~7管腳與CPU的數(shù)據(jù)總線相連。從CPU向8259A發(fā)送ICW和OCW,從8259A向CPU傳送8259A的狀態(tài)以及中斷向量號(hào),都是通過(guò)數(shù)據(jù)總線傳遞的。
當(dāng)CPU向8259A發(fā)送ICW、OCW時(shí),當(dāng)把數(shù)據(jù)送上數(shù)據(jù)總線后,需要通知8259A讀數(shù)據(jù),CPU通過(guò)拉低WR管腳的電平的方式通知8259A,當(dāng)8259A的WR管腳收到低電平后,讀取數(shù)據(jù)總線的數(shù)據(jù)。類似的,CPU準(zhǔn)備好讀取8259A的狀態(tài)時(shí),拉低RD管腳通知8259A。
8259A和CPU之間的中斷信號(hào)的通知使用專用的連線,8259A的管腳INTR(interrupt request)和INTA(interrupt acknowledge)分別與處理器的INTR和INTA管腳相連。8259A通過(guò)管腳INTR向CPU發(fā)送中斷請(qǐng)求,CPU通過(guò)管腳INTA向PIC發(fā)送中斷確認(rèn),告訴PIC其收到中斷并且開(kāi)始處理了。8259A與CPU之間的具體中斷過(guò)程如下:
1)8259A的IR0~7管腳高電平有效,所以當(dāng)中斷源請(qǐng)求服務(wù)時(shí),拉高連接IR0~7的管腳,產(chǎn)生中斷請(qǐng)求。
2)8259A需要將這些信號(hào)記錄下來(lái),因此其內(nèi)部有個(gè)寄存器IRR(Interrupt Request Register),負(fù)責(zé)記錄這個(gè)中斷請(qǐng)求,針對(duì)這個(gè)例子,IRR的bit 0將被設(shè)置為1。
3)有的時(shí)候,我們會(huì)屏蔽掉某個(gè)設(shè)備的中斷。換句話說(shuō),就是的當(dāng)這個(gè)中斷源向8259A發(fā)送信號(hào)后,8259A并不將這個(gè)中斷信號(hào)發(fā)送給CPU。讀者不要將屏蔽和CPU通過(guò)cli命令關(guān)中斷混淆,CPU關(guān)中斷時(shí),中斷還會(huì)發(fā)送給CPU,只是在關(guān)中斷期間CPU不處理中斷。8259A中的寄存器IMR(Interrupt Mask Register)負(fù)責(zé)記錄某個(gè)中斷源是否被屏蔽,比如0號(hào)中斷源被設(shè)備了屏蔽,那么IMR的bit 0將被設(shè)置。那么這個(gè)IMR是誰(shuí)設(shè)置的呢?當(dāng)然是CPU中的操作系統(tǒng)。因此這一步,8259A將會(huì)檢查收到的中斷請(qǐng)求是否被屏蔽。
4)在某一個(gè)時(shí)刻,可能有多個(gè)中斷請(qǐng)求,或者是之前存在IRR中的中斷并沒(méi)有被處理,8259A中積累了一些中斷。某一個(gè)時(shí)刻,8259A只能向CPU發(fā)送一個(gè)中斷請(qǐng)求,因此,當(dāng)存在多個(gè)中斷請(qǐng)求時(shí),8259A需要判斷一下中斷優(yōu)先級(jí),這個(gè)單元叫做priority resolver,priority resolver將在IRR中選出優(yōu)先級(jí)最高的中斷。
5)選出最高優(yōu)先級(jí)的中斷后,8259A拉高管腳INTR的電平,向CPU發(fā)出信號(hào)。
6)當(dāng)CPU執(zhí)行完當(dāng)前指令周期后,其將檢查寄存器FLAGS的中斷使能位IF(Interrupt enable flag),如果允許中斷,那么將檢查INTR是否有中斷,如果有中斷,那么將通過(guò)管腳INTR通知8259A處理器將開(kāi)始處理中斷。
7)8259A收到CPU發(fā)來(lái)的INTA信號(hào)后,將置位最高優(yōu)先級(jí)的中斷在ISR(In-Service Register)中對(duì)應(yīng)的位,并清空IRR中對(duì)應(yīng)的位。
8)通常,x86 CPU會(huì)發(fā)送第2次INTA,在收到第2次INTA后,8259A會(huì)將中斷向量號(hào)(vector)送上數(shù)據(jù)總線D0~D7。
9)如果8259A設(shè)置為AEOI(Automatic End Of Interrupt)模式,那么8259A復(fù)位ISR中對(duì)應(yīng)的bit,否則ISR中對(duì)應(yīng)的bit一直保持到收到系統(tǒng)的中斷服務(wù)程序發(fā)來(lái)的EOI命令。
我們知道,中斷服務(wù)程序保存在一個(gè)數(shù)組中,數(shù)組中的每一項(xiàng)對(duì)應(yīng)一個(gè)中斷服務(wù)程序。在實(shí)模式下,這個(gè)數(shù)組稱為IVT(interrupt vector table);在保護(hù)模式下,這個(gè)數(shù)組稱為IDT(Interrupt descriptor table)。
這個(gè)數(shù)組中保存的服務(wù)程序,并不是全部都是外部中斷,還有處理CPU內(nèi)部異常的以及軟中斷服務(wù)程序。x86CPU前32個(gè)中斷號(hào)(0-31)留給處理器異常的,比如第0個(gè)中斷號(hào),是處理器出現(xiàn)除0(Divide by Zero)異常的,不能被占用。因此,假設(shè)我們計(jì)劃IVT數(shù)組中第32個(gè)元素存放管腳IR0對(duì)應(yīng)的ISR,那么我們初始化8259A時(shí),通過(guò)ICW,設(shè)置起始的irq base為32,那么當(dāng)8259A發(fā)出管腳IR0的中斷請(qǐng)求時(shí),則發(fā)出的值是32,管腳IR1對(duì)應(yīng)的是33,依此類推。這個(gè)32、33就是所謂的中斷向量(vector)。換句話說(shuō),中斷向量就是中斷服務(wù)程序在IVT/IDT中的索引。下面就是設(shè)置irq_base的代碼,在初始化時(shí),通過(guò)第2個(gè)初始化命令字(ICW2)設(shè)置:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- static void pic_ioport_write(void *opaque, u32addr, u32 val)
- {
- …
- switch(s->init_state) {
- …
- case 1:
- s->irq_base = val & 0xf8;
- …
- }
- }
后來(lái),隨著APIC和MSI的出現(xiàn),中斷向量設(shè)置的更為靈活,可以為每個(gè)PCI設(shè)置其中斷向量,這個(gè)中斷向量存儲(chǔ)在PCI設(shè)備的配置空間中。
內(nèi)核中抽象了一個(gè)結(jié)構(gòu)體kvm_kpic_state來(lái)記錄每個(gè)8259A的狀態(tài):
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- struct kvm_kpic_state {
- u8last_irr; /* edge detection */
- u8 irr; /* interrupt request register */
- u8imr; /* interrupt mask register */
- u8isr; /* interrupt service register */
- …
- };
- struct kvm_pic {
- structkvm_kpic_state pics[2]; /* 0 is master pic, 1 is slave pic*/
- irq_request_func *irq_request;
- void*irq_request_opaque;
- intoutput; /* intr from master PIC */
- structkvm_io_device dev;
- };
1片8259A只能連接最多8個(gè)外設(shè),如果需要支持更多外設(shè),需要多片8259A級(jí)聯(lián)。在結(jié)構(gòu)體kvm_pic中,我們看到有2片8259A:pic[0]和pic[1]。KVM定義了結(jié)構(gòu)體kvm_kpic_state記錄8259A的狀態(tài),其中包括我們之前提到的IRR、IMR、ISR等等。
1 虛擬設(shè)備向PIC發(fā)送中斷請(qǐng)求
如同物理外設(shè)請(qǐng)求中斷時(shí)拉高與8259A連接的管腳的電壓,虛擬設(shè)備請(qǐng)求中斷的方式是通過(guò)一個(gè)API告訴虛擬的8259A芯片中斷請(qǐng)求,以kvmtool中的virtio blk虛擬設(shè)備為例:
- commit 4155ba8cda055b7831489e4c4a412b073493115b
- kvm: Fix virtio block device support some more
- kvmtool.git/blk-virtio.c
- static bool blk_virtio_out(…)
- {
- …
- caseVIRTIO_PCI_QUEUE_NOTIFY: {
- …
- while(queue->vring.avail->idx != queue->last_avail_idx) {
- if(!blk_virtio_read(self, queue))
- return false;
- }
- kvm__irq_line(self, VIRTIO_BLK_IRQ, 1);
- break;
- }
- …
- }
當(dāng)Guest內(nèi)核的塊設(shè)備驅(qū)動(dòng)發(fā)出I/O通知VIRTIO_PCI_QUEUE_NOTIFY時(shí),將觸發(fā)CPU從Guest模式切換到Host模式,KVM中的塊模擬設(shè)備開(kāi)始I/O操作,比如訪問(wèn)保存Guest文件系統(tǒng)的鏡像文件。virtio blk這個(gè)提交,塊設(shè)備的I/O處理是同步的,也就是說(shuō),一直要等到文件操作完成,才會(huì)向Guest發(fā)送中斷,返回Guest。當(dāng)然同步阻塞在這里是不合理的,而是應(yīng)該馬上返回Guest,這樣Guest可以執(zhí)行其他的任務(wù),虛擬設(shè)備完成I/O操作后,再通知Guest,這是kvmtool初期的實(shí)現(xiàn),后來(lái)已經(jīng)改進(jìn)為異步的方式。代碼中在一個(gè)while循環(huán)處理完設(shè)備驅(qū)動(dòng)的I/O請(qǐng)求后,調(diào)用了函數(shù)kvm__irq_line,irq_line對(duì)應(yīng)8259A的管腳IR0~7,其代碼如下:
- commit 4155ba8cda055b7831489e4c4a412b073493115b
- kvm: Fix virtio block device support some more
- kvmtool.git/kvm.c
- void kvm__irq_line(struct kvm *self, int irq, intlevel)
- {
- structkvm_irq_level irq_level;
- irq_level= (struct kvm_irq_level) {
- {
- .irq = irq,
- },
- .level = level,
- };
- if(ioctl(self->vm_fd, KVM_IRQ_LINE, &irq_level) < 0)
- die_perror("KVM_IRQ_LINE failed");
- }
- 函數(shù)kvm__irq_line將irq number和管腳電平信息,這里是1,表示拉高電平了,封裝到結(jié)構(gòu)體kvm_irq_level中,傳遞給內(nèi)核中的KVM模塊:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/kvm_main.c
- static long kvm_vm_ioctl(…)
- {
- …
- caseKVM_IRQ_LINE: {
- …
- kvm_pic_set_irq(pic_irqchip(kvm),
- irq_event.irq,
- irq_event.level);
- …
- break;
- }
- …
- }
KVM模塊將kvmtool中組織的中斷信息從用戶空間復(fù)制到內(nèi)核空間中,然后調(diào)用虛擬8259A的模塊中提供的API kvm_pic_set_irq,向8259A發(fā)出中斷請(qǐng)求。
2 記錄中斷到IRR
中斷處理需要一個(gè)過(guò)程,從外設(shè)發(fā)出請(qǐng)求,一直到ISR處理完成發(fā)出EOI。而且可能中斷來(lái)了并不能馬上處理,或者之前已經(jīng)累加了一些中斷,大家需要排隊(duì)依次請(qǐng)求CPU處理,等等,因此,需要一些寄存器來(lái)記錄這些狀態(tài)。當(dāng)外設(shè)中斷請(qǐng)求到來(lái)時(shí),8259A首先需要將他們記錄下來(lái),這個(gè)寄存器就是IRR(Interrupt Request Register),8259A用他來(lái)記錄有哪些pending的中斷需要處理。
當(dāng)KVM模塊收到外設(shè)的請(qǐng)求,調(diào)用虛擬8259A的API kvm_pic_set_irq是,其第1件事就是將中斷記錄到IRR寄存器中:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- void kvm_pic_set_irq(void *opaque, int irq, intlevel)
- {
- structkvm_pic *s = opaque;
- pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);
- ……
- }
- static inline void pic_set_irq1(structkvm_kpic_state *s,
- int irq, int level)
- {
- int mask;
- mask = 1<< irq;
- if(s->elcr & mask) /* level triggered */
- …
- else /* edge triggered */
- if(level) {
- if((s->last_irr & mask) == 0)
- s->irr |= mask;
- s->last_irr |= mask;
- } else
- s->last_irr &= ~mask;
- }
信號(hào)有邊緣觸發(fā)和水平觸發(fā),在物理上可以理解為,8329A在前一個(gè)周期檢測(cè)到管腳信號(hào)是0,當(dāng)前周前檢測(cè)到管腳信號(hào)是1,如果是上升沿觸發(fā)模式,那么8259A就認(rèn)為外設(shè)有請(qǐng)求了,這種觸發(fā)模式就是邊緣觸發(fā)。對(duì)于水平觸發(fā),以高電平觸發(fā)為例,當(dāng)8259A檢測(cè)到管腳處于高電平,則認(rèn)為外設(shè)來(lái)請(qǐng)求了。
在虛擬8259A的結(jié)構(gòu)體kvm_kpic_state中,寄存器elcr就是用來(lái)記錄8259A被設(shè)置的觸發(fā)模式的。參數(shù)level即相當(dāng)于硬件層面的電信號(hào),0表示低電平,1表示高電平。以邊緣觸發(fā)為例,當(dāng)管腳收到一個(gè)低電平時(shí),即level的值為0,代碼進(jìn)入else分支,結(jié)構(gòu)體kvm_kpic_state中的字段last_irr中會(huì)清除該IRQ對(duì)應(yīng)IRR的位,即相當(dāng)于設(shè)置該中斷管腳為低電平狀態(tài)。當(dāng)管腳收到高電平時(shí),即level的值為1,代碼進(jìn)入if分支,此時(shí)8259A將判斷之前該管腳的狀態(tài),也就是判斷結(jié)構(gòu)體kvm_kpic_state中的字段last_irr中該IRQ對(duì)應(yīng)IRR的位,如果為低電平,那么則認(rèn)為中斷源有中斷請(qǐng)求,將其記錄到IRR中。當(dāng)然,同時(shí)需要在字段last_irr記錄下當(dāng)前該管腳的狀態(tài)。
3 設(shè)置中斷標(biāo)識(shí)
當(dāng)8259A將中斷請(qǐng)求記錄到IRR中后,下一步就是開(kāi)啟一個(gè)中斷評(píng)估(evaluate)過(guò)程了,包括中斷是否被屏蔽,多個(gè)中斷請(qǐng)求的優(yōu)先級(jí)等等,最后將通過(guò)管腳INTA通知CPU處理外部中斷。我們看到這里是8259A主動(dòng)發(fā)起中斷過(guò)程,但是虛擬中斷有些不同,中斷的發(fā)起的時(shí)機(jī)不再是虛擬中斷芯片主動(dòng)發(fā)起,而是在每次準(zhǔn)備切入Guest時(shí),KVM查詢中斷芯片,如果有pending的中斷,則執(zhí)行中斷注入。模擬8259A在收到中斷請(qǐng)求后,在記錄到IRR后,設(shè)置一個(gè)變量,后面在切入Guest前KVM會(huì)查詢這個(gè)變量:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- void kvm_pic_set_irq(void *opaque, int irq, intlevel)
- {
- structkvm_pic *s = opaque;
- pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);
- pic_update_irq(s);
- }
- static void pic_update_irq(struct kvm_pic *s)
- {
- …
- irq =pic_get_irq(&s->pics[0]);
- if (irq>= 0)
- s->irq_request(s->irq_request_opaque, 1);
- else
- s->irq_request(s->irq_request_opaque, 0);
- }
- static void pic_irq_request(void *opaque, intlevel)
- {
- struct kvm*kvm = opaque;
- pic_irqchip(kvm)->output = level;
- }
在函數(shù)vmx_vcpu_run中,在準(zhǔn)備切入Guest之前將調(diào)用函數(shù)vmx_intr_assist去檢查虛擬中斷芯片是否有等待處理的中斷,相關(guān)代碼如下:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/vmx.c
- static int vmx_vcpu_run(…)
- {
- …
- vmx_intr_assist(vcpu);
- …
- }
- static void vmx_intr_assist(struct kvm_vcpu*vcpu)
- {
- …
- has_ext_irq= kvm_cpu_has_interrupt(vcpu);
- …
- if(!has_ext_irq)
- return;
- interrupt_window_open =
- ((vmcs_readl(GUEST_RFLAGS) & X86_EFLAGS_IF) &&
- (vmcs_read32(GUEST_INTERRUPTIBILITY_INFO) & 3) == 0);
- if(interrupt_window_open)
- vmx_inject_irq(vcpu, kvm_cpu_get_interrupt(vcpu));
- …
- }
其中函數(shù)kvm_cpu_has_interrupt查詢8259A設(shè)置的變量output:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/irq.c
- int kvm_cpu_has_interrupt(struct kvm_vcpu *v)
- {
- structkvm_pic *s = pic_irqchip(v->kvm);
- if(s->output) /* PIC */
- return1;
- return 0;
- }
如果有output設(shè)置了,就說(shuō)明有外部中斷等待處理,然后接著判斷Guest是否可以被中斷,包括Guest是否中斷了,Guest是否正在執(zhí)行一些不能被中斷的指令,如果可以注入,則調(diào)用vmx_inject_irq完成中斷的注入。其中,傳遞給函數(shù)vmx_inject_irq的第2個(gè)參數(shù)是函數(shù)kvm_cpu_get_interrupt返回的結(jié)果,該函數(shù)獲取需要注入的中斷,這個(gè)過(guò)程就是中斷評(píng)估過(guò)程,我們下一節(jié)討論。
4 中斷評(píng)估
在上一節(jié)我們看到在執(zhí)行注入前,vmx_inject_irq調(diào)用函數(shù)kvm_cpu_get_interrupt獲取需要注入的中斷。函數(shù)kvm_cpu_get_interrupt的核心邏輯就是中斷評(píng)估(evaluate),包括:這個(gè)pending的中斷有沒(méi)有被屏蔽?pending的中斷的優(yōu)先級(jí)是否比CPU正在處理的中斷優(yōu)先級(jí)高?代碼如下:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/irq.c
- int kvm_cpu_get_interrupt(struct kvm_vcpu *v)
- {
- ……
- vector =kvm_pic_read_irq(s);
- if (vector!= -1)
- returnvector;
- …
- }
- linux.git/drivers/kvm/i8259.c
- int kvm_pic_read_irq(struct kvm_pic *s)
- {
- int irq,irq2, intno;
- irq =pic_get_irq(&s->pics[0]);
- if (irq>= 0) {
- …
- intno = s->pics[0].irq_base + irq;
- } else {
- …
- returnintno;
- }
kvm_pic_read_irq調(diào)用函數(shù)pic_get_irq獲取評(píng)估后的中斷,如上面黑體標(biāo)識(shí)的,我們可以清楚的看到中斷向量和中斷管腳的關(guān)系,疊加了一個(gè)irq_base,這個(gè)irq_base就是通過(guò)初始化8259A時(shí)通過(guò)ICW設(shè)置的,完成從IRn到中斷向量的轉(zhuǎn)換。
一個(gè)中斷芯片通常連接有多個(gè)外設(shè),所以在某一個(gè)時(shí)刻,可能會(huì)有多個(gè)設(shè)備請(qǐng)求到來(lái),這時(shí)就有一個(gè)優(yōu)先處理哪個(gè)請(qǐng)求的問(wèn)題,因此,中斷就有了優(yōu)先級(jí)的概念。以8259A為例,典型的有2種中斷優(yōu)先級(jí)模式:
1)固定優(yōu)先級(jí)(Fixedpriority),即優(yōu)先級(jí)是固定的,從IR0到IR7依次降低,IR0的優(yōu)先級(jí)永遠(yuǎn)最高,IR7的優(yōu)先級(jí)永遠(yuǎn)最低。
2)循環(huán)優(yōu)先級(jí)(rotatingpriority),即當(dāng)前處理完的IRn其優(yōu)先級(jí)調(diào)整為最低,當(dāng)前處理的優(yōu)先級(jí)下個(gè),即IRn+1,調(diào)整為優(yōu)先級(jí)最高。比如,當(dāng)前處理的中斷是irq 2,那么緊接著irq3的優(yōu)先級(jí)設(shè)置為是最高,然后依次是irq4、irq5、irq6、irq7、irq1、irq2、irq3。假設(shè)此時(shí)irq5和irq2同時(shí)來(lái)了中斷,那么irq5顯然會(huì)被優(yōu)先處理。然后irq6被設(shè)置為優(yōu)先級(jí)最高,接下來(lái)依次是irq7、irq1、irq2、irq3、irq4、irq5。
理解了循環(huán)優(yōu)先級(jí)算法后,從8259A中獲取最高優(yōu)先級(jí)請(qǐng)求的代碼就很容易理解了:
- commit85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- static int pic_get_irq(struct kvm_kpic_state *s)
- {
- int mask,cur_priority, priority;
- mask =s->irr & ~s->imr;
- priority =get_priority(s, mask);
- if(priority == 8)
- return-1;
- …
- mask =s->isr;
- …
- cur_priority = get_priority(s, mask);
- if(priority < cur_priority)
- /*
- *higher priority found: an irq should be generated
- */
- return(priority + s->priority_add) & 7;
- else
- return-1;
- }
- static inline int get_priority(structkvm_kpic_state *s, int mask)
- {
- intpriority;
- if (mask== 0)
- return8;
- priority =0;
- while((mask & (1 << ((priority + s->priority_add) & 7))) == 0)
- priority++;
- returnpriority;
- }
函數(shù)pic_get_irq分成2部分,第1部分是從當(dāng)前pending的中斷中取得最高優(yōu)先級(jí)的中斷,當(dāng)前之前需要濾掉被被屏蔽的中斷。第2部分是獲取正在被CPU處理的中斷的優(yōu)先級(jí)的中斷的優(yōu)先級(jí),通過(guò)這里,讀者更能具體的理解了8259A為什么需要這些寄存器記錄中斷的狀態(tài)。然后比較2個(gè)中斷的優(yōu)先級(jí),如果pending的優(yōu)先級(jí)高,那么就通過(guò)拉低INTR管腳電壓,向CPU發(fā)出中斷請(qǐng)求。
再來(lái)看一下計(jì)算優(yōu)先級(jí)的函數(shù)get_priority。其中變量priority_add記錄的是當(dāng)前最高優(yōu)先級(jí)的管腳,所以邏輯上就是從當(dāng)前最高的優(yōu)先級(jí)管腳開(kāi)始,從高向低依次檢查是否有pending的中斷。比如當(dāng)前IR4最高,那么priority_add的值就是4。while循環(huán),從管腳IR(4+0)開(kāi)始,依次檢查管腳IR(4+1)、IR(4+2),依此類推。
5 中斷ACK
在物理上,CPU在準(zhǔn)備處理一個(gè)中斷請(qǐng)求后,將通過(guò)管腳INTA向8259A發(fā)出確認(rèn)脈沖。同樣,軟件模擬上,也需要類似處理。在完成中斷評(píng)估后,準(zhǔn)備注入Guest前,需要向虛擬8259A執(zhí)行確認(rèn)狀態(tài)的操作,代碼如下:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- int kvm_pic_read_irq(struct kvm_pic *s)
- {
- int irq,irq2, intno;
- irq =pic_get_irq(&s->pics[0]);
- if (irq>= 0) {
- pic_intack(&s->pics[0], irq);
- …
- }
- static inline void pic_intack(structkvm_kpic_state *s, int irq)
- {
- if(s->auto_eoi) {
- …
- } else
- s->isr |= (1 << irq);
- /*
- * Wedon't clear a level sensitive interrupt here
- */
- if(!(s->elcr & (1 << irq)))
- s->irr &= ~(1 << irq);
- }
在中斷評(píng)估中,在調(diào)用函數(shù)kvm_pic_read_irq獲取評(píng)估的中斷結(jié)果后,馬上就調(diào)用函數(shù)pic_intack完成了中斷確認(rèn)的動(dòng)作。在收到CPU發(fā)來(lái)的中斷確認(rèn)后,8259A需要更新自己的狀態(tài),包括,因?yàn)橹袛嘁呀?jīng)開(kāi)始得到服務(wù)了,所以從IRR中清除等待服務(wù)請(qǐng)求。另外,需要設(shè)置ISR位,記錄正在被服務(wù)的中斷,但是這里稍微有一點(diǎn)點(diǎn)復(fù)雜。
設(shè)置ISR表示正在服務(wù)的位,表示處理器正在處理中斷。設(shè)置ISR的一個(gè)典型作用是中斷的作用是當(dāng)ISR處理完中斷,向8259A發(fā)送EOI時(shí),8259A知道正在處理的IRn,比如說(shuō)如果8259A使用的是循環(huán)優(yōu)先級(jí),那么需要最高優(yōu)先級(jí)為當(dāng)前處理的IRn的下一個(gè)。
如果8259A是AEOI模式,那么就無(wú)須設(shè)置ISR了,因?yàn)橹袛喾?wù)程序執(zhí)行完畢后不會(huì)發(fā)送EOI命令。所以在AEOI模式下(上面代碼的if分支),需要將收到EOI時(shí)8259A需要處理的邏輯完成,這部分內(nèi)容我們?cè)谙乱还?jié)會(huì)討論。
對(duì)于邊緣觸發(fā)的,進(jìn)入到ISR階段后,需要復(fù)位對(duì)于IRR。對(duì)于level trigger的,在收到中斷請(qǐng)求后8259A處理,不過(guò)多討論了,如果讀者有興趣,可以閱讀函數(shù)pic_set_irq1中關(guān)于水平觸發(fā)部分的邏輯。
6 關(guān)于EOI的處理
中斷服務(wù)程序執(zhí)行完成后,會(huì)向8259A發(fā)送EOI,告知8259A中斷處理完成。8259A在收到這個(gè)EOI時(shí),復(fù)位ISR,如果是采用的循環(huán)優(yōu)先級(jí),還需要設(shè)置變量priority_add,使其指向當(dāng)前處理IRn的下一個(gè):
- commit85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/i8259.c
- static void pic_ioport_write(void *opaque, u32addr, u32 val)
- {
- …
- case1: /* end of interrupt */
- case5:
- priority = get_priority(s, s->isr);
- if(priority != 8) {
- irq = (priority + s->priority_add) & 7;
- s->isr &= ~(1 << irq);
- if(cmd == 5)
- s->priority_add = (irq + 1) & 7;
- pic_update_irq(s->pics_state);
- }
- break;
- …
- }
如果8259A被設(shè)置為AEOI模式,不會(huì)再收到后續(xù)中斷服務(wù)程序的EOI命令,那么8259A在收到CPU的ACK后,就必須把收到EOI命令時(shí)執(zhí)行的邏輯現(xiàn)在處理,前面看到AEOI模式不必設(shè)置ISR,所以這里也無(wú)需復(fù)位ISR,只需要調(diào)整變量priority_add,記錄最高優(yōu)先級(jí)位置即可:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- static inline void pic_intack(structkvm_kpic_state *s, int irq)
- {
- if(s->auto_eoi) {
- if(s->rotate_on_auto_eoi)
- s->priority_add = (irq + 1) & 7;
- } else
- …
- }
7 中斷注入
對(duì)于外部中斷,每個(gè)CPU在指令周期結(jié)束后,將會(huì)去檢查INTR是否有中斷請(qǐng)求。那么對(duì)于處于Guest模式的CPU,其如何知道有中斷請(qǐng)求呢?Intel在VMCS中設(shè)置了一個(gè)字段:VM-entry interruption-information,在VM entry時(shí)CPU將會(huì)檢查這個(gè)字段,這個(gè)字段格式表3-1所示。
表3-1 VM-entry interruption-information格式(部分)
位 |
內(nèi)容 |
7:0 |
中斷或異常向量 |
10:8 |
中斷類型: 0: External interrupt 1: Reserved 2: Non-maskable interrupt (NMI) 3: Hardware exception 4: Software interrupt 5: Privileged software exception 6: Software exception 7: Other event |
31 |
是否有效[2] |
在VM entry前,KVM模塊檢查虛擬8259A中如果有pending中斷需要處理,則將需要處理的中斷信息寫入到VMCS中的這個(gè)字段VM-entry
- interruption-information:
- commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126
- KVM: Add support for in-kernel PIC emulation
- linux.git/drivers/kvm/vmx.c
- static void vmx_inject_irq(struct kvm_vcpu *vcpu,int irq)
- {
- …
- vmcs_write32(VM_ENTRY_INTR_INFO_FIELD,
- irq |INTR_TYPE_EXT_INTR | INTR_INFO_VALID_MASK);
- }
前面我們看到,中斷注入是在每次VM entry時(shí),KVM模塊檢查8259A是否有pending的中斷等待處理。這樣就有可能給中斷帶來(lái)一定的延遲,典型如下面2類情況:
(1)CPU可能正處在Guest模式,那么就需要等待下一次VM exit 和VM entry。
(2)VCPU這個(gè)線程也許正在睡眠,比如Guest VCPU運(yùn)行hlt指令時(shí),就會(huì)切換回Host模式,線程掛起。
對(duì)于第1種情況,是多處理器系統(tǒng)下的一個(gè)典型情況,目標(biāo)CPU的正在運(yùn)行Guest。KVM需要想辦法觸發(fā)Guest發(fā)生一次VM exit,切換到Host。我們知道,當(dāng)處于Guest模式的CPU收到外部中斷時(shí),會(huì)觸發(fā)VM exit,由Host來(lái)處理這次中斷。所以,KVM可以向目標(biāo)CPU發(fā)送一個(gè)IPI中斷,觸發(fā)目標(biāo)CPU發(fā)生一次VM exit。
對(duì)于第2種情況,首先需要喚醒睡眠的VCPU線程,使其進(jìn)入CPU就緒隊(duì)列,準(zhǔn)備接受調(diào)度。對(duì)于多處理器系統(tǒng),然后再向目標(biāo)CPU發(fā)送一個(gè)“重新調(diào)度”的IPI中斷,那么被喚醒的VCPU線程很快就會(huì)被調(diào)度,執(zhí)行切入Guest的過(guò)程,從而完成中斷注入。
所以當(dāng)有中斷請(qǐng)求時(shí),虛擬中斷芯片將主動(dòng)“kick”一下目標(biāo)CPU,這個(gè)“踢”的函數(shù)就是kvm_vcpu_kick:
- commit b6958ce44a11a9e9425d2b67a653b1ca2a27796f
- KVM: Emulate hlt in the kernel
- linux.git/drivers/kvm/i8259.c
- static void pic_irq_request(void *opaque, intlevel)
- {
- …
- pic_irqchip(kvm)->output = level;
- if (vcpu)
- kvm_vcpu_kick(vcpu);
- }
如果虛擬CPU線程在睡眠,則“踢醒”他。如果目標(biāo)CPU運(yùn)行在Guest模式,則將其從Guest模式“踢”到Host模式,在VM entry時(shí)完成中斷注入,kick的手段就是我們剛剛提到的IPI,代碼如下:
- commit b6958ce44a11a9e9425d2b67a653b1ca2a27796f
- KVM: Emulate hlt in the kernel
- linux.git/drivers/kvm/irq.c
- void kvm_vcpu_kick(struct kvm_vcpu *vcpu)
- {
- intipi_pcpu = vcpu->cpu;
- if(waitqueue_active(&vcpu->wq)) {
- wake_up_interruptible(&vcpu->wq);
- ++vcpu->stat.halt_wakeup;
- }
- if(vcpu->guest_mode)
- smp_call_function_single(ipi_pcpu, vcpu_kick_intr,
- vcpu, 0, 0);
- }
如果VCPU線程睡眠在等待隊(duì)列上,則喚醒使其進(jìn)入CPU的就緒任務(wù)隊(duì)列。如果是多CPU的情況且目標(biāo)CPU處于Guest模式,則需要發(fā)送核間中斷。如果目標(biāo)CPU正在執(zhí)行Guest,那么這個(gè)IPI中斷將導(dǎo)致VM exit,從而在下一次進(jìn)入Guest時(shí),可以注入中斷。
事實(shí)上,目標(biāo)CPU無(wú)須執(zhí)行任何callback,也無(wú)須等待IPI返回,所以也無(wú)須使用smp_call_function_single,而是直接發(fā)送一個(gè)請(qǐng)求目標(biāo)CPU重新調(diào)度的IPI即可,因此后來(lái)直接調(diào)用了函數(shù)smp_send_reschedule。函數(shù)smp_send_reschedule簡(jiǎn)單直接,直接發(fā)送了一個(gè)RESCHEDULE的IPI:
- commit 32f8840064d88cc3f6e85203aec7b6b57bebcb97
- KVM: use smp_send_reschedule in kvm_vcpu_kick
- linux.git/arch/x86/kvm/x86.c
- void kvm_vcpu_kick(struct kvm_vcpu *vcpu)
- {
- …
- smp_send_reschedule(cpu);
- …
- }
- linux.git/arch/x86/kernel/smp.c
- static void native_smp_send_reschedule(int cpu)
- {
- …
- apic->send_IPI_mask(cpumask_of(cpu), RESCHEDULE_VECTOR);
- }
本文轉(zhuǎn)載自微信公眾號(hào)「Linux閱碼場(chǎng)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Linux閱碼場(chǎng)公眾號(hào)。