Linux中斷虛擬化之二
PIC虛擬化
計算機系統有很多的外設需要服務,顯然,CPU采用輪詢的方式逐個詢問外設是否需要服務,是非常浪費CPU的計算的,尤其是對那些并不是頻繁需要服務的設備。因此,計算機科學家們設計了外設主動向CPU發起服務請求的方式,這種方式就是中斷。采用中斷方式后,在沒有外設請求時,CPU就可以繼續其他計算任務,而不是進行很多不必要的輪詢,極大地提高了系統的吞吐[1] 在每個指令周期結束后,如果CPU關中斷標識(IF)沒有被設置,那么其會去檢查是否有中斷請求,如果有中斷請求,則運行對應的中斷服務程序,然后返回被中斷的計算任務繼續執行。
CPU不可能為每個硬件都設計專門的管腳接收中斷,管腳數量的限制、電路的復雜度、靈活度等方方面面都不現實,因此,需要設計一個專門管理中斷的單元。由中斷管理單元接受來自外圍設備的請求,確定請求的優先級,并向CPU發出中斷。1981年IBM推出的第1代個人電腦PC/XT使用了一個獨立的8259A作為中斷控制器,自此,8259A就成為了單核時代中斷芯片事實上的標準。因為可以通過軟件編程對其進行控制,比如當管腳收到設備信號時,可以編程控制其發出的中斷向量號,因此,中斷控制器又稱為可編程中斷控制器(programmable interrupt controller),簡稱PIC。單片8259A可以連接8個外設的中斷信號線,可以多片級聯支持更多外設。
8259A和CPU的連接如圖5所示。
圖5 8259A和CPU連接
片選和地址譯碼器相連,當CPU準備訪問8259A前,需要向地址總線發送8259A對應的地址,經過譯碼器后,譯碼器發現是8259A對應的地址,因此會拉低與8259A的CS連接的管腳的電平,從而選中8259A,通知8259A,CPU準備與其交換數據了。
8259A的D0~7管腳與CPU的數據總線相連。從CPU向8259A發送ICW和OCW,從8259A向CPU傳送8259A的狀態以及中斷向量號,都是通過數據總線傳遞的。
當CPU向8259A發送ICW、OCW時,當把數據送上數據總線后,需要通知8259A讀數據,CPU通過拉低WR管腳的電平的方式通知8259A,當8259A的WR管腳收到低電平后,讀取數據總線的數據。類似的,CPU準備好讀取8259A的狀態時,拉低RD管腳通知8259A。
8259A和CPU之間的中斷信號的通知使用專用的連線,8259A的管腳INTR(interrupt request)和INTA(interrupt acknowledge)分別與處理器的INTR和INTA管腳相連。8259A通過管腳INTR向CPU發送中斷請求,CPU通過管腳INTA向PIC發送中斷確認,告訴PIC其收到中斷并且開始處理了。8259A與CPU之間的具體中斷過程如下:
1)8259A的IR0~7管腳高電平有效,所以當中斷源請求服務時,拉高連接IR0~7的管腳,產生中斷請求。
2)8259A需要將這些信號記錄下來,因此其內部有個寄存器IRR(Interrupt Request Register),負責記錄這個中斷請求,針對這個例子,IRR的bit 0將被設置為1。
3)有的時候,我們會屏蔽掉某個設備的中斷。換句話說,就是的當這個中斷源向8259A發送信號后,8259A并不將這個中斷信號發送給CPU。讀者不要將屏蔽和CPU通過cli命令關中斷混淆,CPU關中斷時,中斷還會發送給CPU,只是在關中斷期間CPU不處理中斷。8259A中的寄存器IMR(Interrupt Mask Register)負責記錄某個中斷源是否被屏蔽,比如0號中斷源被設備了屏蔽,那么IMR的bit 0將被設置。那么這個IMR是誰設置的呢?當然是CPU中的操作系統。因此這一步,8259A將會檢查收到的中斷請求是否被屏蔽。
4)在某一個時刻,可能有多個中斷請求,或者是之前存在IRR中的中斷并沒有被處理,8259A中積累了一些中斷。某一個時刻,8259A只能向CPU發送一個中斷請求,因此,當存在多個中斷請求時,8259A需要判斷一下中斷優先級,這個單元叫做priority resolver,priority resolver將在IRR中選出優先級最高的中斷。
5)選出最高優先級的中斷后,8259A拉高管腳INTR的電平,向CPU發出信號。
6)當CPU執行完當前指令周期后,其將檢查寄存器FLAGS的中斷使能位IF(Interrupt enable flag),如果允許中斷,那么將檢查INTR是否有中斷,如果有中斷,那么將通過管腳INTR通知8259A處理器將開始處理中斷。
7)8259A收到CPU發來的INTA信號后,將置位最高優先級的中斷在ISR(In-Service Register)中對應的位,并清空IRR中對應的位。
8)通常,x86 CPU會發送第2次INTA,在收到第2次INTA后,8259A會將中斷向量號(vector)送上數據總線D0~D7。
9)如果8259A設置為AEOI(Automatic End Of Interrupt)模式,那么8259A復位ISR中對應的bit,否則ISR中對應的bit一直保持到收到系統的中斷服務程序發來的EOI命令。
我們知道,中斷服務程序保存在一個數組中,數組中的每一項對應一個中斷服務程序。在實模式下,這個數組稱為IVT(interrupt vector table);在保護模式下,這個數組稱為IDT(Interrupt descriptor table)。
這個數組中保存的服務程序,并不是全部都是外部中斷,還有處理CPU內部異常的以及軟中斷服務程序。x86CPU前32個中斷號(0-31)留給處理器異常的,比如第0個中斷號,是處理器出現除0(Divide by Zero)異常的,不能被占用。因此,假設我們計劃IVT數組中第32個元素存放管腳IR0對應的ISR,那么我們初始化8259A時,通過ICW,設置起始的irq base為32,那么當8259A發出管腳IR0的中斷請求時,則發出的值是32,管腳IR1對應的是33,依此類推。這個32、33就是所謂的中斷向量(vector)。換句話說,中斷向量就是中斷服務程序在IVT/IDT中的索引。下面就是設置irq_base的代碼,在初始化時,通過第2個初始化命令字(ICW2)設置:
- 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;
- …
- }
- }
后來,隨著APIC和MSI的出現,中斷向量設置的更為靈活,可以為每個PCI設置其中斷向量,這個中斷向量存儲在PCI設備的配置空間中。
內核中抽象了一個結構體kvm_kpic_state來記錄每個8259A的狀態:
- 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個外設,如果需要支持更多外設,需要多片8259A級聯。在結構體kvm_pic中,我們看到有2片8259A:pic[0]和pic[1]。KVM定義了結構體kvm_kpic_state記錄8259A的狀態,其中包括我們之前提到的IRR、IMR、ISR等等。
1 虛擬設備向PIC發送中斷請求
如同物理外設請求中斷時拉高與8259A連接的管腳的電壓,虛擬設備請求中斷的方式是通過一個API告訴虛擬的8259A芯片中斷請求,以kvmtool中的virtio blk虛擬設備為例:
- 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;
- }
- …
- }
當Guest內核的塊設備驅動發出I/O通知VIRTIO_PCI_QUEUE_NOTIFY時,將觸發CPU從Guest模式切換到Host模式,KVM中的塊模擬設備開始I/O操作,比如訪問保存Guest文件系統的鏡像文件。virtio blk這個提交,塊設備的I/O處理是同步的,也就是說,一直要等到文件操作完成,才會向Guest發送中斷,返回Guest。當然同步阻塞在這里是不合理的,而是應該馬上返回Guest,這樣Guest可以執行其他的任務,虛擬設備完成I/O操作后,再通知Guest,這是kvmtool初期的實現,后來已經改進為異步的方式。代碼中在一個while循環處理完設備驅動的I/O請求后,調用了函數kvm__irq_line,irq_line對應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");
- }
- 函數kvm__irq_line將irq number和管腳電平信息,這里是1,表示拉高電平了,封裝到結構體kvm_irq_level中,傳遞給內核中的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中組織的中斷信息從用戶空間復制到內核空間中,然后調用虛擬8259A的模塊中提供的API kvm_pic_set_irq,向8259A發出中斷請求。
2 記錄中斷到IRR
中斷處理需要一個過程,從外設發出請求,一直到ISR處理完成發出EOI。而且可能中斷來了并不能馬上處理,或者之前已經累加了一些中斷,大家需要排隊依次請求CPU處理,等等,因此,需要一些寄存器來記錄這些狀態。當外設中斷請求到來時,8259A首先需要將他們記錄下來,這個寄存器就是IRR(Interrupt Request Register),8259A用他來記錄有哪些pending的中斷需要處理。
當KVM模塊收到外設的請求,調用虛擬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;
- }
信號有邊緣觸發和水平觸發,在物理上可以理解為,8329A在前一個周期檢測到管腳信號是0,當前周前檢測到管腳信號是1,如果是上升沿觸發模式,那么8259A就認為外設有請求了,這種觸發模式就是邊緣觸發。對于水平觸發,以高電平觸發為例,當8259A檢測到管腳處于高電平,則認為外設來請求了。
在虛擬8259A的結構體kvm_kpic_state中,寄存器elcr就是用來記錄8259A被設置的觸發模式的。參數level即相當于硬件層面的電信號,0表示低電平,1表示高電平。以邊緣觸發為例,當管腳收到一個低電平時,即level的值為0,代碼進入else分支,結構體kvm_kpic_state中的字段last_irr中會清除該IRQ對應IRR的位,即相當于設置該中斷管腳為低電平狀態。當管腳收到高電平時,即level的值為1,代碼進入if分支,此時8259A將判斷之前該管腳的狀態,也就是判斷結構體kvm_kpic_state中的字段last_irr中該IRQ對應IRR的位,如果為低電平,那么則認為中斷源有中斷請求,將其記錄到IRR中。當然,同時需要在字段last_irr記錄下當前該管腳的狀態。
3 設置中斷標識
當8259A將中斷請求記錄到IRR中后,下一步就是開啟一個中斷評估(evaluate)過程了,包括中斷是否被屏蔽,多個中斷請求的優先級等等,最后將通過管腳INTA通知CPU處理外部中斷。我們看到這里是8259A主動發起中斷過程,但是虛擬中斷有些不同,中斷的發起的時機不再是虛擬中斷芯片主動發起,而是在每次準備切入Guest時,KVM查詢中斷芯片,如果有pending的中斷,則執行中斷注入。模擬8259A在收到中斷請求后,在記錄到IRR后,設置一個變量,后面在切入Guest前KVM會查詢這個變量:
- 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;
- }
在函數vmx_vcpu_run中,在準備切入Guest之前將調用函數vmx_intr_assist去檢查虛擬中斷芯片是否有等待處理的中斷,相關代碼如下:
- 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));
- …
- }
其中函數kvm_cpu_has_interrupt查詢8259A設置的變量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設置了,就說明有外部中斷等待處理,然后接著判斷Guest是否可以被中斷,包括Guest是否中斷了,Guest是否正在執行一些不能被中斷的指令,如果可以注入,則調用vmx_inject_irq完成中斷的注入。其中,傳遞給函數vmx_inject_irq的第2個參數是函數kvm_cpu_get_interrupt返回的結果,該函數獲取需要注入的中斷,這個過程就是中斷評估過程,我們下一節討論。
4 中斷評估
在上一節我們看到在執行注入前,vmx_inject_irq調用函數kvm_cpu_get_interrupt獲取需要注入的中斷。函數kvm_cpu_get_interrupt的核心邏輯就是中斷評估(evaluate),包括:這個pending的中斷有沒有被屏蔽?pending的中斷的優先級是否比CPU正在處理的中斷優先級高?代碼如下:
- 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調用函數pic_get_irq獲取評估后的中斷,如上面黑體標識的,我們可以清楚的看到中斷向量和中斷管腳的關系,疊加了一個irq_base,這個irq_base就是通過初始化8259A時通過ICW設置的,完成從IRn到中斷向量的轉換。
一個中斷芯片通常連接有多個外設,所以在某一個時刻,可能會有多個設備請求到來,這時就有一個優先處理哪個請求的問題,因此,中斷就有了優先級的概念。以8259A為例,典型的有2種中斷優先級模式:
1)固定優先級(Fixedpriority),即優先級是固定的,從IR0到IR7依次降低,IR0的優先級永遠最高,IR7的優先級永遠最低。
2)循環優先級(rotatingpriority),即當前處理完的IRn其優先級調整為最低,當前處理的優先級下個,即IRn+1,調整為優先級最高。比如,當前處理的中斷是irq 2,那么緊接著irq3的優先級設置為是最高,然后依次是irq4、irq5、irq6、irq7、irq1、irq2、irq3。假設此時irq5和irq2同時來了中斷,那么irq5顯然會被優先處理。然后irq6被設置為優先級最高,接下來依次是irq7、irq1、irq2、irq3、irq4、irq5。
理解了循環優先級算法后,從8259A中獲取最高優先級請求的代碼就很容易理解了:
- 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;
- }
函數pic_get_irq分成2部分,第1部分是從當前pending的中斷中取得最高優先級的中斷,當前之前需要濾掉被被屏蔽的中斷。第2部分是獲取正在被CPU處理的中斷的優先級的中斷的優先級,通過這里,讀者更能具體的理解了8259A為什么需要這些寄存器記錄中斷的狀態。然后比較2個中斷的優先級,如果pending的優先級高,那么就通過拉低INTR管腳電壓,向CPU發出中斷請求。
再來看一下計算優先級的函數get_priority。其中變量priority_add記錄的是當前最高優先級的管腳,所以邏輯上就是從當前最高的優先級管腳開始,從高向低依次檢查是否有pending的中斷。比如當前IR4最高,那么priority_add的值就是4。while循環,從管腳IR(4+0)開始,依次檢查管腳IR(4+1)、IR(4+2),依此類推。
5 中斷ACK
在物理上,CPU在準備處理一個中斷請求后,將通過管腳INTA向8259A發出確認脈沖。同樣,軟件模擬上,也需要類似處理。在完成中斷評估后,準備注入Guest前,需要向虛擬8259A執行確認狀態的操作,代碼如下:
- 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);
- }
在中斷評估中,在調用函數kvm_pic_read_irq獲取評估的中斷結果后,馬上就調用函數pic_intack完成了中斷確認的動作。在收到CPU發來的中斷確認后,8259A需要更新自己的狀態,包括,因為中斷已經開始得到服務了,所以從IRR中清除等待服務請求。另外,需要設置ISR位,記錄正在被服務的中斷,但是這里稍微有一點點復雜。
設置ISR表示正在服務的位,表示處理器正在處理中斷。設置ISR的一個典型作用是中斷的作用是當ISR處理完中斷,向8259A發送EOI時,8259A知道正在處理的IRn,比如說如果8259A使用的是循環優先級,那么需要最高優先級為當前處理的IRn的下一個。
如果8259A是AEOI模式,那么就無須設置ISR了,因為中斷服務程序執行完畢后不會發送EOI命令。所以在AEOI模式下(上面代碼的if分支),需要將收到EOI時8259A需要處理的邏輯完成,這部分內容我們在下一節會討論。
對于邊緣觸發的,進入到ISR階段后,需要復位對于IRR。對于level trigger的,在收到中斷請求后8259A處理,不過多討論了,如果讀者有興趣,可以閱讀函數pic_set_irq1中關于水平觸發部分的邏輯。
6 關于EOI的處理
中斷服務程序執行完成后,會向8259A發送EOI,告知8259A中斷處理完成。8259A在收到這個EOI時,復位ISR,如果是采用的循環優先級,還需要設置變量priority_add,使其指向當前處理IRn的下一個:
- 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被設置為AEOI模式,不會再收到后續中斷服務程序的EOI命令,那么8259A在收到CPU的ACK后,就必須把收到EOI命令時執行的邏輯現在處理,前面看到AEOI模式不必設置ISR,所以這里也無需復位ISR,只需要調整變量priority_add,記錄最高優先級位置即可:
- 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 中斷注入
對于外部中斷,每個CPU在指令周期結束后,將會去檢查INTR是否有中斷請求。那么對于處于Guest模式的CPU,其如何知道有中斷請求呢?Intel在VMCS中設置了一個字段:VM-entry interruption-information,在VM entry時CPU將會檢查這個字段,這個字段格式表3-1所示。
表3-1 VM-entry interruption-information格式(部分)
位 |
內容 |
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中的這個字段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時,KVM模塊檢查8259A是否有pending的中斷等待處理。這樣就有可能給中斷帶來一定的延遲,典型如下面2類情況:
(1)CPU可能正處在Guest模式,那么就需要等待下一次VM exit 和VM entry。
(2)VCPU這個線程也許正在睡眠,比如Guest VCPU運行hlt指令時,就會切換回Host模式,線程掛起。
對于第1種情況,是多處理器系統下的一個典型情況,目標CPU的正在運行Guest。KVM需要想辦法觸發Guest發生一次VM exit,切換到Host。我們知道,當處于Guest模式的CPU收到外部中斷時,會觸發VM exit,由Host來處理這次中斷。所以,KVM可以向目標CPU發送一個IPI中斷,觸發目標CPU發生一次VM exit。
對于第2種情況,首先需要喚醒睡眠的VCPU線程,使其進入CPU就緒隊列,準備接受調度。對于多處理器系統,然后再向目標CPU發送一個“重新調度”的IPI中斷,那么被喚醒的VCPU線程很快就會被調度,執行切入Guest的過程,從而完成中斷注入。
所以當有中斷請求時,虛擬中斷芯片將主動“kick”一下目標CPU,這個“踢”的函數就是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線程在睡眠,則“踢醒”他。如果目標CPU運行在Guest模式,則將其從Guest模式“踢”到Host模式,在VM entry時完成中斷注入,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線程睡眠在等待隊列上,則喚醒使其進入CPU的就緒任務隊列。如果是多CPU的情況且目標CPU處于Guest模式,則需要發送核間中斷。如果目標CPU正在執行Guest,那么這個IPI中斷將導致VM exit,從而在下一次進入Guest時,可以注入中斷。
事實上,目標CPU無須執行任何callback,也無須等待IPI返回,所以也無須使用smp_call_function_single,而是直接發送一個請求目標CPU重新調度的IPI即可,因此后來直接調用了函數smp_send_reschedule。函數smp_send_reschedule簡單直接,直接發送了一個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);
- }
本文轉載自微信公眾號「Linux閱碼場」,可以通過以下二維碼關注。轉載本文請聯系Linux閱碼場公眾號。