1. 程式人生 > >XV6學習(8)中斷和裝置驅動

XV6學習(8)中斷和裝置驅動

驅動是作業系統中用於管理特定裝置的程式碼:驅動控制裝置硬體,通知硬體執行操作,處理中斷,與等待該裝置IO的程序進行互動。 當裝置需要與作業系統進行互動時,就會產生中斷(陷阱的一種),之後核心的陷阱處理程式碼就會識別中斷裝置並呼叫對應的驅動處理程式。在XV6這一步發生在`trap.c`的`devintr`中。 大部分裝置驅動在兩個上下文中執行程式碼:頂層部分執行在程序的核心執行緒中,底層部分在中斷處理時執行。頂層部分通過系統呼叫如`read`和`write`來呼叫,這一部分程式碼會請求硬體開始一個操作的執行(如請求硬碟讀取塊);之後就會進入等待狀態等待操作的完成。當裝置完成操作後,就會觸發一箇中斷,驅動的中斷處理程式,即底層部分就會判斷完成的操作,喚醒對應的正在等待的程序,之後通知硬體執行下一個操作。 ## 程式碼:控制檯輸入 控制檯的驅動程式`console.c`是一個驅動結構的簡單抽象。控制檯驅動通過UART(Universal asynchronous receiver-transmitter,通用非同步收發傳輸器)串列埠讀取使用者輸入的字元。驅動程式一次會累積一行的輸入,並處理特定的字元如退格和ctrl-u。使用者程序通過`read`系統呼叫來獲取一行輸入。 驅動呼叫的UART硬體是由QEMU模擬的16550晶片,一個16550晶片可以管理一條連線到終端或其他電腦的RS232序列鏈路。在QEMU中,其連線到鍵盤和顯示器。 UART硬體可以看作一組對映到記憶體中的控制暫存器,對硬體的控制可以直接通過`load`和`store`特定記憶體來完成。UART記憶體對映地址開始於`0x10000000`或`UART0`(定義於`memlayout.h`)。每個控制暫存器的大小為1byte,偏移量定義於`uart.c`。 XV6`main`函式中的`consoleinit`對UART裝置進行初始化,設定UART裝置每接收一個位元組的輸入就產生一個接收中斷,每當完成一個位元組輸出的傳送時就產生一個傳輸完成中斷。 XV6 shell通過`init.c`中開啟的檔案描述符對控制檯進行讀取。`read`系統呼叫將會呼叫`consoleread`函式,該函式等待輸入的到達(通過中斷)並保持在`cons.buf`中,拷貝其到使用者空間,當一整行接收完成後返回到使用者程序中。如果沒有一整行輸入到達,read程序就會在`sleep`呼叫中等待。 當用戶輸入一個字元,UART裝置就會產生一箇中斷,啟用XV6的陷阱處理程式。陷阱處理程式將會呼叫`devintr`,讀取`scause`判斷是否為外部裝置產生的中斷。之後通過PLIC(平臺級中斷控制器)判斷中斷裝置,如果是UART裝置,就會呼叫`uartintr`函式。 `uartinit`從UART裝置中讀取所有輸入字元,並將其交給`consoleintr`處理,此函式不會等待字元的輸入,因為未來的輸入會產生新的中斷。`consoleintr`將輸入保持在buffer中直到一整行到達,同時對一些特殊符號進行處理。當一整行到達後,就會喚醒一個正在等待的`consoleread`。 當`consoleread`被喚醒時,buffer中就儲存了完整的一行輸入,此時就可以將其拷貝到使用者空間並返回。 ## 程式碼:控制檯輸出 `write`系統呼叫對控制檯的寫入最終會呼叫`uartputc`函式,裝置會維護輸出緩衝`uart_tx_buf`,因此寫程序不需要等待UART完成傳送。`uartputc`將字元加入緩衝區後,呼叫`uartstart`函式開始傳輸之後返回,該函式唯一的等待情況是緩衝區已滿。 每當UART傳送一個位元組後,就會產生一次中斷。`uartintr`函式會呼叫`uartstart`函式判斷傳輸是否完成,未完成就開始傳輸下一個緩衝的字元。因此,當程序寫入多個字元時,第一個位元組會通過`uartputc`呼叫`uartstart`進行傳輸,之後的位元組將會被`uartintr`呼叫的`uartstart`進行傳輸。 對於裝置活動和程序活動,常用的解耦方式是通過緩衝和中斷。控制檯驅動可以處理輸入即使沒有程序在等待讀取,一個隨後到來的`read`會讀取到輸入。類似的,程序可以進行輸出而不需要等待裝置響應。解耦可以允許程序並行執行裝置IO從而提高效能,尤其是當裝置速度很慢或需要立即進行響應(如輸入一個字元)。這種思想也被稱作I/O並行。 ## 驅動中的並行 在`consoleread`和`consoleintr`中會呼叫`acquire`函式。這些呼叫會申請一個鎖,用於在並行訪問中保護驅動的資料結構。在這裡有三種並行風險:兩個不同CPU上的程序同時呼叫`consoleread`;當CPU在執行`consoleread`函式時硬體觸發了一箇中斷;當`consoleread`在執行時,硬體在其他CPU上觸發了一箇中斷。 在並行中需要關注的另一個點是一個程序可能會等待裝置的輸入,但是中斷訊號在另一個程序執行時產生,因此中斷處理程式是必須上下文無關的(不允許考慮中斷時的程序或程式碼)。例如中斷處理程式不能安全地在當前程序地頁表上呼叫`copyout`函式。中斷處理程式應該僅執行上下文無關的工作(如拷貝輸入到緩衝區),之後喚醒頂層部分來處理剩餘工作。 ## 定時器中斷 XV6通過定時器中斷來維護時鐘以及進行程序切換;在`usertrap`和`kerneltrap`中的`yield`函式會執行程序切換。定時器中斷會由RISC-V CPU內部的時鐘硬體產生。XV6對此時鐘硬體進行程式設計以定期中斷每個CPU。 RISC-V要求定時器中斷必須在機器模式下執行,而不是在監管模式下執行。RISC-V的機器模式在無分頁環境下執行,並且具有一系列單獨的控制暫存器,因此在機器模式下執行普通的 xv6 核心程式碼是不切實際的。所有XV6的定時器中斷處理程式是和陷阱機制完全分開的。 `start.c`中的程式碼執行於機器模式中,`main`函式之前,在`timerinit`函式中對定時器中斷進行了設定:對CLINT硬體程式設計使其在一定時間後產生一次中斷;設定scratch區域(類似於trapframe),幫助定時器中斷處理程式儲存暫存器和CLINT暫存器的地址。最後函式會設定`mtvec`為`timervec`函式地址並開啟定時器中斷。 定時器中斷會在任何時候發生,核心在執行關鍵操作時也無法禁用定時器中斷。因此定時器中斷處理程式必須保證不會干擾被中斷的核心程式碼執行。處理程式最基本的策略就是產生一個軟體中斷之後立即返回。產生的軟體中斷就可以通過通用的陷阱機制進行處理,並且可以進行關閉。軟體中斷的處理程式在`devintr`函式中。 機器模式的時鐘中斷向量為`timervec`,該函式儲存了三個暫存器在`start`函式準備的`scratch`區域中,通知CLINT下一個中斷的時刻,通過`csrw sip, a1`(`a1`為2)指令觸發一個軟體中斷,最後恢復暫存器並返回。 ## 真實作業系統 XV6執行裝置和時鐘中斷在核心執行時產生。定時器中斷在中斷處理程式中強制執行緒切換,即使是在核心態執行中。這個功能可以使得核心執行緒公平地獲取CPU時間片,尤其是當核心執行緒耗費大量時間進行計算而不返回使用者態。但是,這使得核心程式碼需要考慮到其可能會被暫停並在一段時間後再另一個CPU上恢復,而這給XV6帶來了一定的複雜性。如果裝置中斷和定時器中斷只在使用者程式碼執行時執行觸發,核心可以變得更加簡單。 在許多作業系統中,驅動程式的程式碼量遠遠大於核心本身。要支援所有裝置在計算機上執行是十分繁雜的工作:有大量裝置需要支援,裝置有很多特性,裝置間的協議十分複雜並且缺少文件。 UART裝置通過讀取控制暫存器一次接收一個位元組資料,這種模式稱為程式I/O(programmed I/O),因為資料移動由軟體驅動。這種方式十分簡單但是在高速裝置上是十分緩慢的。高速裝置通常通過DMA方式來進行資料傳輸。DMA裝置硬體可以直接對記憶體進行讀寫,現代硬碟和網路裝置就是通過這種方式進行的。DMA裝置驅動會在記憶體中準備資料,之後通過一次控制暫存器的寫入告訴裝置對準備好的資料進行處理。 當裝置需要在無法預知但不太頻繁的時間上需要進行處理時,中斷是有效的。但是中斷有很大的CPU開銷。因此高速裝置會使用一些技巧來減少中斷次數。一個技巧就是對一整批的輸入或輸出請求發起一次中斷。另一個是驅動完全禁用中斷,轉為定時查詢裝置是否需要處理,這種技術被稱為輪詢(polling)。如果裝置操作執行非常頻繁,那麼輪詢是有意義的,反之如果裝置大部分時間都是空閒的,那麼輪詢會浪費CPU時間。一些驅動會根據裝置負載自動切換輪詢和中斷。 UART驅動先拷貝輸入資料到核心緩衝區,之後再拷貝到使用者空間。這在低資料傳輸率的情況下是有效的,但是對於高速裝置,兩次拷貝會顯著地降低效能。一些作業系統可以直接將資料在使用者態緩衝區和裝置硬體之間移動,通常是通