第5章 中斷和設備驅動

什麼是驅動?

驅動是操作系統中用來管理一個特定設備的代碼:

  • 配置硬件設備;
  • 告訴設備執行操作;
  • 處理設備中斷;
  • 跟可能在等待來自設備的I/O的進程進行交互;

驅動代碼需要一定的技巧,因爲驅動是跟其管理的設備併發執行的。

驅動必須理解設備的硬件接口。

需要操作系統關注的設備通常被配置能生成中斷。

當一個設備觸發一箇中斷時,內核陷阱處理代碼會識別出來,並調用設備的中斷處理程序,比如在xv6中,這種分發機制發生在devintr中。

許多驅動是在兩種上下文中執行代碼的:

  • 上半部分在進程的內核線程裏運行;
  • 下半部分在中斷時執行;

上半部分是通過諸如read和write等需要設備執行I/O的系統調用被調用的。這段代碼可能要求硬件開始一個操作(比如請求磁盤讀取塊等),然後等着操作完成。最終,設備完成了操作,觸發一箇中斷。

下半部分是設備中斷處理程序

  • 指出什麼操作已完成;
  • 如果合適的話,喚醒一個等待的進程;
  • 告訴硬件開始處理任何下一個等待的操作;

5.1 代碼:控制檯輸入

控制檯驅動是對驅動結構的簡單示例。

控制檯驅動接收由人通過附加到RISC-V上的UART串行接口鍵入的字符。

控制檯驅動一次累計一行輸入,可處理特殊輸入字符,比如backspace、control-u等。

諸如shell等用戶進程,使用read系統調用來獲取來自控制檯的輸入行。

當你鍵入輸入給在QEMU中的xv6時,你的擊鍵是通過QEMU模擬的UART硬件傳遞給xv6的。

驅動與之交互的UART硬件是一個由QEMU模擬的16550芯片。在實際的計算機上,16650芯片管理的是一個可連接到終端或者其他計算機的RS232串行鏈接。當運行QEMU時,16650芯片連接的是鍵盤和顯示器。

UART硬件似乎是一組由內存映射控制寄存器組成的軟件。即,RISC-V硬件將一些物理地址連接到UART硬件,使得加載和存儲等指令直接跟UART設備交互,而不是RAM。

UART的內存映射地址UART0開始。有一撮UART控制寄存器,每個寄存器的大小是1字節。
從UART0開始的偏移量定義在uart.c中。

比如LSR寄存器包含的位用來標記是否有輸入字符正等待着被軟件讀取。

RAH寄存器:存儲可用的輸入字符

THR寄存器

每當讀取一個字符,UART硬件就會將該字符從等待字符的內部FIFO刪除;當FIFO變空時,UART就會清除LSR中的ready標記位。

UART的發送硬件跟接收硬件是獨立的。

如果軟件向THR寄存器中寫入一個字節,UART就發送這個字節。

xv6的main函數調用consoleinit來初始化UART硬件。這段代碼配置UART,使得:當UART每接收一個字節的輸入,就生成一個接收中斷;每當UART每發送完一個字節的輸出,就生成一個發送完成中斷

xv6的shell程序通過init.c打開的文件描述符來從終端讀取數據。
對read系統調用的調用通過內核進入到consoleread。
consoleread等待着輸入到達並緩衝在cons.buf中,拷貝輸入到用戶空間,返回給用戶進程。
如果用戶還沒有鍵入一個完整的行,則所有的讀取進程都將以sleep調用的形式等待。

當用戶鍵入一個字符時,發生了哪些事?

  • 當用戶鍵入一個字符時,UART硬件請求RISC-V觸發一箇中斷,中斷會激活xv6的陷阱處理程序。
  • 陷阱處理程序調用devintr
  • devintr查看RISC-V的scause寄存器,發現中斷來自一個外部設備,然後請求PLIC硬件單元來告訴它是哪個設備發生了中斷。如果是UART,則devintr會調用uartintr
  • uartintr從UART硬件中讀取任何等待的輸入字符,將這些字符交給consoleintr
    uartintr不會等待字符,因爲新的輸入會觸發新的中斷。
  • consoleintrcons.buf中累計輸入字符直到一整行輸入到達;consoleintr會特殊處理backspace字符和其他一些字符;當有新行到達時,consoleintr會喚醒一個等待的consoleread
  • 一旦被喚醒,consoleread將觀察到在cons.buf中有一個完成的輸入行,把數據從cons.buf拷貝到用戶空間,並通過系統調用機制返回到用戶空間。

5.2 代碼:控制檯輸出

對連接到控制檯的文件描述符的write系統調用最終會到達uartputc。

設備驅動維護了一個輸出緩衝區,使得寫進程不必鄧艾UART發成發送。

uartputc將每個字符添加到緩衝區,調用uartstart來開始設備發送,並返回。

uartputc等待的唯一情形就是緩衝區已經滿了。

每當UART完成發送一個字節,則UART就生成一箇中斷。

uartintr調用uartstart:檢查設備是否已完成發送,交給設備下一個緩衝的輸出字符。因此,如果一個進程向控制檯寫入了多個字節,通常第一個字節是由uartputc調用uartstart發送的,剩餘的字節是由uartintr調用uartstart作爲發送完成中斷達到來發送。

什麼是I/O併發?

值得注意的模式:通過緩衝和中斷將設備活動與進程活動解耦。即使沒有進程在等待讀,控制檯驅動也能處理輸入;下一次讀將會看到輸入。類似地,進程發送輸出也不用等待設備。這種解耦能提升性能,通過允許進程跟設備I/O併發;當設備很慢或者需要立即關注時,就顯得特備重要。這種思維就是I/O併發。

5.3 在驅動裏的併發

注意到在consoleread和consoleintr中,都有調用acquire。

調用acquire會獲得一把鎖,用來保護控制檯驅動的數據結構不受併發訪問的影響。

在這裏有4個併發危險:

  • 在不同CPU上的兩個進程同時調用consoleread;
  • 當CPU已經在consoleread內部執行的同時,硬件可能請求該CPU發出一個控制檯中斷;
  • 在consoleread執行的同時,硬件可能會在另一個不同的CPU上發出一個控制檯中斷;
  • 有一個進程可能正在等待來自某個設備的輸入,但是輸入中斷信號到達的時候可能是另一個不同的進程在運行;

不允許中斷處理程序來考慮被中斷的進程或者代碼。比如即使有了當前進程的頁表,中斷處理程序也沒法安全地調用copyout。通常,中斷處理程序只做相對較少的工作(比如,僅拷貝輸入數據到緩衝區等),然後喚醒上半部分代碼來做剩餘的事情。

5.4 計時器中斷

xv6使用計時器中斷來維護它的時鐘,來實現在計算密集型的進程間切換的功能。

在usertrap和kerneltrap中調用yield會導致這類切換。計時器中斷來自添加到每個RISC-V CPU上的時鐘硬件。xv6對這個時鐘硬件進行編程來週期性地中斷每個CPU。

RISC-V要求計時器中斷必須在機器模式下處理,而不是在內核模式下。因爲RISC-V機器模式執行時不需要分頁,有單獨的控制寄存器集,所以在機器模式下運行普通代碼是不切實際的。因此,xv6處理計時器中斷是方式是完全不同於陷阱機制的。

如何設置計時器中斷?

start.c裏以機器模式運行的代碼在main之前,用來設置接收計時器中斷。一部分任務是對CLINT硬件編程來在指定延遲後生成一箇中斷。另一部分的任務是建立scratch區,來幫助計時器中斷處理程序保存寄存器和CLINT寄存的地址。最後,start設置mevec爲timervec,使得計時器中斷生效。

如何處理計時器中斷?

因爲計時器中斷可以在執行用戶代碼或者內核代碼的任何一個點處發生,且於內核沒有辦法使計時器中斷在關鍵操作期間失效,所以計時器中斷處理程序必須以不干擾被中斷內核代碼的方式來處理計時器中斷。
基本的策略是中斷處理程序請求RISC-V觸發一個軟件中斷,並立即返回。RISC-V將軟件中斷傳遞給使用普通陷阱機制的內核,並允許內核來使其失效。處理由計時器中斷產生的軟件中斷的代碼可參考devintr。

機器模式的計時器中斷向量是timervec。它在有start準備好的scratch區裏保存一些寄存器,告訴CLINT何時生成下一個中斷,請求RISC-V生成一個軟件中斷,檢索寄存器,並返回。注意:在計時器中斷處理程序中沒有C代碼的。

5.5 真實世界

xv6允許:在內核中執行或者執行用戶程序時,發生設備中斷和計時器中斷。

注意:即使是在內核中執行,計時器中斷也會從計時器中斷處理程序中強制進行線程切換(比如調用yield)。

如果內核線程有時花費許多時間在計算而沒有返回用戶空間,則在內核線程之間對CPU進行時間分片就變得很重要了。

由於計時器中斷,內核代碼可能會被掛起,之後在不同的CPU上恢復執行,這是xv6中複雜性的一個來源。
如果僅在執行用戶代碼時纔會發生設備中斷和計時器重案,則內核可以更簡單點。

在一臺典型的計算機上支持所有的設備是一項艱鉅的任務,因爲存在許多設備,每個設備都有許多特徵,在設備和驅動之間的協議可能會很複雜且文檔記錄很差。在許多操作系統上,驅動佔據的代碼要大於核心內核的代碼。

可編程I/O VS DMA

UART驅動通過讀取UART控制寄存器來每次讀取一個字節的數據。這種模式是可編程I/O,因爲軟件驅動着數據移動。可編程I/O雖然簡單,但是太慢了以至於不能用於高數據傳輸速率。

需要高速移動數據的設備通常都使用DMA。DMA設備硬件直接將輸入數據寫入到RAM,直接從RAM讀取輸出數據。現代的磁盤和網絡設備都使用DMA。
DMA驅動會在RAM中準備好數據,然後向控制寄存器中單獨寫入來告訴設備來處理準備好的數據。

中斷 VS 輪訓

當設備需要關注的時不可預測的,和次數不頻繁時,中斷就很有意義。但是,中斷有很高的CPU開銷。因此,諸如網絡和磁盤控制器等高速設備會使用一些技巧來減少對中斷的需要。一個技巧是對批量輸入請求或者輸出請求觸發一次中斷。
另一個技巧是完全使中斷失效,週期性地檢查設備來看是否需要關注。這種技術稱作輪詢。如果設備執行操作非常快,則輪詢就有意義。但是如果設備大部分時間處於空間,則輪詢會浪費CPU時間。有些驅動會根據當前設備的負載在中斷和輪詢之間動態切換。

UART驅動首先將輸入數據拷貝到內核中的緩衝區,然後再從緩衝區拷貝到用戶空間。如果數速率很低,則這樣是有意義的。但是對那些消費或者產生數據非常快的設備來說,這樣的兩次拷貝會嚴重降低性能。一些操作系統能使用DMA在用戶空間的緩衝區和設備硬件之間直接移動數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章