本文假設你已經具備一些計算機的基本知識,包括但不限于:
- Linux系統運作基礎知識,如使用者态、核心态。
- Linux記憶體管理相關知識,如虛拟位址、實體位址、頁表。
- 彙編語言。
- C語言。
參考書籍和部落格清單如下:
- 《深入了解計算機系統》
- 《作業系統導論》
- 《計算機組成與結構(清華大學出版社)》
- 《新一代彙編語言程式設計(高等教育出版社)》
- 部落格:裝置I/O模型(連結:https://github.com/zhangjaycee/real_tech/wiki/distri_018)
- 部落格:Linux系統對IO端口和IO記憶體的管理(連結:javascript:void(0))
- 部落格:淺談記憶體映射I/O(MMIO)與端口映射I/O(PMIO)的差別(連結:javascript:void(0))
- 部落格:PCI裝置的位址空間(連結:javascript:void(0))
- 部落格:PCIE的記憶體位址空間、I/O位址空間和配置位址空間(連結:javascript:void(0))
- 部落格:Linux記憶體尋址和記憶體管理(連結:javascript:void(0))
- StackOverflow:What does request_mem_region() actually do and when it is needed?(連結:https://stackoverflow.com/questions/7682422/what-does-request-mem-region-actually-do-and-when-it-is-needed)
- StackOverflow:What is the difference between DMA and memory-mapped IO(連結:https://stackoverflow.com/questions/3851677/what-is-the-difference-between-dma-and-memory-mapped-io)
- 文檔:INS/INSB/INSW/INSD-從端口輸入到字元串(連結:http://www.hgy413.com/hgydocs/IA32/instruct32_hh/vc139.htm)
- 部落格:cpu指令如何讀寫硬碟(連結:javascript:void(0))
一、I/O體系結構
下圖是計算機中I/O體系的分層結構圖,其中作業系統又分為了檔案系統、通用塊層、裝置驅動程式三個層次,分層設計的目的是将具體實作對使用者進行屏蔽,友善上層使用者使用以及下層擴充新類型的硬體I/O裝置。

二、I/O裝置與總線
本節我們學習I/O分層中的最底層,即與I/O裝置相關的硬體知識。
2.1.标準I/O裝置
标準I/O裝置,指的是I/O裝置的模型規範。一般來說,任何一個真實的I/O裝置(例如HDD,即磁盤驅動器)都是基于此标準I/O裝置模型進行設計的:
一個标準的I/O裝置分為兩部分,分别是硬體接口和内部結構:
(1)硬體接口:硬體接口本質就是I/O裝置提供的各式寄存器,系統軟體通過與這些寄存器進行互動,達到控制I/O裝置的目的。
(2)内部結構:實作硬體接口提供的功能,不同的I/O裝置具有不同功能,是以它們的内部實作和包含的元器件也不盡相同。
2.2.計算機總線
總線(Bus)是計算機各種功能部件之間傳送資訊的公共通信幹線,傳送的資訊包括了資料、資料位址和控制信号。下面的圖檔引用自《深入了解計算機原理》,它形象的描述了CPU、記憶體、I/O裝置之間如何通過總線相連:
可以看到,不同的I/O裝置(如鍵盤、滑鼠、磁盤等)需要通過相應的接口電路與總線相連接配接,這些接口電路由“控制器”或“擴充卡”提供(後面統稱為“裝置控制器”)。不同的裝置控制器能夠支援不同的接口協定,下圖引用自《作業系統導論》,它描述了幾種常見的接口協定的I/O裝置能夠接入I/O總線這個事實:
值得一提的是,根據接口協定的性能差別,現代計算機對I/O總線進行了分層。在上圖中,圖像或者其他高性能的I/O裝置通過正常的I/O總線連接配接到系統,在許多現代系統中會是PCI或它的衍生形式。而一些相對較慢的I/O裝置則通過外圍總線(peripheral bus)連接配接到系統,比如使用SCSI、SATA或者USB等協定的I/O裝置。
三、與I/O裝置互動
本節我們站在I/O分層中軟體與硬體的邊界,去學習現代計算機如何與I/O裝置互動。
3.1.通路I/O裝置
主機對I/O裝置進行通路的目标是I/O裝置的寄存器或者記憶體。常見的I/O裝置都隻提供寄存器供主機通路,對于低速外設這樣的模式是足夠的,但是對于需要大量、高速資料互動的外設(如顯示卡、網卡),就需要主機能夠直接通路外設的記憶體了。
現代計算機提供了兩種方式來通路I/O裝置,它們分别是PMIO和MMIO:
- PMIO:端口映射I/O(Port-mapped I/O)。将I/O裝置獨立看待,并使用CPU提供的專用I/O指令(如X86架構的in和out)通路。
- MMIO:記憶體映射I/O(Memory-mapped I/O)。将I/O裝置看作記憶體的一部分,不使用單獨的I/O指令,而是使用記憶體讀寫指令通路。
3.1.1.PMIO
端口映射I/O,又叫做被隔離的I/O(isolated I/O),它提供了一個專門用于I/O裝置“注冊”的位址空間,該位址空間被稱為I/O位址空間,最大尋址範圍為64K,如下圖所示:
為了使I/O位址空間與記憶體位址空間隔離,要麼在CPU實體接口上增加一個I/O引腳,要麼增加一條專用的I/O總線。是以,并不是所有的平台都支援PMIO,常見的ARM平台就不支援PMIO。支援PMIO的CPU通常具有專門執行I/O操作的指令,例如在Intel-X86架構的CPU中,I/O指令是in和out,這兩個指令可以讀/寫1、2、4個位元組(outb, outw, outl)從記憶體到I/O接口上。
由于I/O位址空間比較小,是以I/O裝置一般隻在其中“注冊”自己的寄存器,之後系統可以通過PMIO對它們進行通路。
3.1.2.MMIO
在MMIO中,實體記憶體和I/O裝置共享記憶體位址空間(注意,這裡的記憶體位址空間實際指的是記憶體的實體位址空間),如下圖所示:
當CPU通路某個虛拟記憶體位址時,該虛拟位址首先轉換為一個實體位址,對該實體位址的通路,會通過南北橋(現在被合并為I/O橋)的路由機制被定向到實體記憶體或者I/O裝置上。是以,用于通路記憶體的CPU指令也可用于通路I/O裝置,并且在記憶體(的實體)位址空間上,需要給I/O裝置預留一個位址區域,該位址區域不能給實體記憶體使用。
MMIO是應用得最為廣泛的一種I/O方式,由于記憶體位址空間遠大于I/O位址空間,I/O裝置可以在記憶體位址空間上暴露自己的記憶體或者寄存器,以供主機進行通路。
3.1.3.PCI裝置
PCI及其衍生的接口(如PCIE)主要服務于高速I/O裝置(如顯示卡或網卡),使用PCI接口的裝置又被稱為PCI裝置。與慢速I/O裝置不同,計算機既需要通路它們的寄存器,也需要通路它們的記憶體。
每個PCI裝置都有一個配置空間(實際就是裝置上一組連續的寄存器),大小為256byte。配置空間中包含了6個BAR(Base Address Registers,基址寄存器),BAR中記錄了裝置所需要的位址空間類型、基址以及其他屬性,格式如下:
可以看到,PCI裝置能夠申請兩類位址空間,即記憶體位址空間和I/O位址空間,它們用BAR的最後一位差別開來。是以,PCI裝置可以通過PMIO和MMIO将自己的I/O存儲器(Registers/RAM/ROM)暴露給CPU(通常寄存器使用PMIO,而記憶體使用MMIO的方式暴露)。
配置空間中的每個BAR可以映射一個位址空間,是以每個PCI裝置最多能映射6段位址空間,但實際上很多裝置用不了這麼多。PCI配置空間的初始值是由廠商預設在裝置中的,也就是說,裝置需要哪些位址空間都是其自己定的,這可能會造成不同的PCI裝置所映射的位址空間沖突,是以在PCI裝置枚舉(也叫總線枚舉,由BIOS或者OS在啟動時完成)的過程中,會重新為其配置設定位址空間,然後寫入PCI配置空間中。
在PCI總線之前的ISA總線是使用跳線帽來配置設定外設的實體位址,每插入一個新裝置都要改變跳線帽以配置設定實體位址,這是十分麻煩且易錯的,但這樣的方式似乎我們更容易了解。能夠配置設定自己總線上挂載裝置的實體位址這也是PCI總線相較于I2C、SPI等低速總線一個最大的特色。
3.2.資料互動流程
使用I/O裝置的目的是為了互動資料,不管是網卡、磁盤,亦或是鍵盤,總歸要将資料進行輸入輸出。本小節以循序漸進的方式講解主機與I/O裝置的互動流程(或者稱為“協定”),在其中我們可以看到PMIO的實際使用以及了解PIO、DMA的概念。
3.2.1.标準互動流程
一般來說,主機與I/O裝置要進行資料互動,會經過這樣一個過程:
(1)CPU通過I/O裝置的硬體接口(以下簡稱I/O接口)擷取裝置狀态(即狀态寄存器的值),隻有“就緒”狀态的裝置才能進行資料傳輸。
(2)CPU通過I/O接口下達互動指令:如果是讀資料,則向I/O接口的指令寄存器輸入要擷取的資料在I/O裝置的内部位置以及讀裝置指令;如果是寫資料,則向I/O接口的指令寄存器輸入要存放的資料在I/O裝置的内部位置、寫裝置指令,以及向資料寄存器寫入資料。
(3)I/O裝置内部根據I/O接口中寄存器的值,開始執行資料傳輸工作。
(4)CPU在I/O裝置完成工作後,執行其他操作,完成資料傳送。
标準互動流程實作起來比較簡單,但是難免會有一些低效和不友善。第一個問題就是輪詢過程比較低效,在等待裝置是否滿足某種狀态時浪費大量CPU時間(下圖描述的就是磁盤在執行資料傳輸過程中,CPU不能執行其他任務,隻能等待傳輸完成),如果此時作業系統可以切換執行下一個就緒程序,就可以大大提高CPU的使用率。
3.2.2.引入中斷
為了解決标準互動流程中CPU輪詢低效的問題,我們需要引入中斷來實作計算與I/O重疊。有了中斷機制,CPU向裝置發出I/O請求後,就可以讓對應程序進入睡眠等待,進而切換執行其他程序。當裝置完成I/O請求後,它會抛出一個硬體中斷,引發CPU跳轉執行作業系統預先定義好的中斷處理程式,中斷處理程式會挂起正在執行的程序,同時喚醒等待I/O的程序并繼續執行。如下圖所示,在磁盤執行程序1的I/O過程中,CPU同時執行程序2,并且在I/O請求執行完畢後,回過頭來再次執行程序1:
為了深入了解,我們引入一段《作業系統導論》中的代碼:
1 /**
2 * 等待裝置就緒
3 */
4 static int ide_wait_ready() {
5 while (((int r = inb(0x1f7)) & IDE_BSY) || !(r & IDE_DRDY)))
6 ; //輪詢直到裝置狀态不為busy
7 }
8
9 /**
10 * 開始執行IO請求
11 */
12 static void ide_start_request(struct buf *b) {
13 ide_wait_ready();
14 outb(0x3f6, 0); //向IDE磁盤控制寄存器寫入0,即開啟中斷
15 outb(0x1f2, 1); //向IDE磁盤指令寄存器的0x1f2位址寫入扇區數
16 outb(0x1f3, b->sector & 0xff); //向IDE磁盤指令寄存器的0x1f3位址寫入對應邏輯塊位址的低位元組
17 outb(0x1f4, (b->sector >> 8) & 0xff); //向IDE磁盤指令寄存器的0x1f3位址寫入對應邏輯塊位址的中位元組
18 outb(0x1f5, (b->sector >> 16) & 0xff); //向IDE磁盤指令寄存器的0x1f3位址寫入對應邏輯塊位址的高位元組
19 outb(0x1f6, 0xe0 | ((b->dev&1) << 4) | ((b->sector >> 24) & 0x0f)); //向IDE磁盤指令寄存器的0x1f6位址寫入驅動編号
20 if (b->flags & B_DIRTY) {
21 outb(0x1f7, IDE_CMD_WRITE); //如果是寫操作,向IDE磁盤指令寄存器的0x1f7位址寫入寫操作指令
22 outsl(0x1f0, b->data, 512/4); //向IDE磁盤指令寄存器的0x1f0位址寫入資料
23 } else {
24 outb(0x1f7, IDE_CMD_READ); //如果是讀操作,向IDE磁盤指令寄存器的0x1f7位址寫入讀操作指令
25 }
26 }
27
28 /**
29 * IDE磁盤讀寫
30 */
31 void ide_rw(struct buf *b) {
32 acquire(&ide_lock);
33 for (struct buf **pp = &ide_queue; *pp; pp = &(*pp)->qnext)
34 ; //周遊鍊式隊列,擷取隊尾元素
35 *pp = b; //将請求入隊
36 if (ide_queue == b)
37 ide_start_request(b); //如果隊列為空,直接執行請求
38 while ((b->flags & (B_VALID | B_DIRTY)) != B_VALID)
39 sleep(b, &ide_lock); //程序睡眠等待IO裝置執行完請求,會釋放鎖ide_lock
40 release(&ide_lock);
41 }
42
43 /**
44 * 中斷響應程式
45 */
46 void ide_intr() {
47 struct buf *b;
48 acquire(&ide_lock);
49 if (!(b->flags & B_DIRTY) && ide_wait_ready() >= 0)
50 insl(0x1f0, b->data, 512/4); //如果是讀請求,擷取資料到記憶體
51 b->flags != B_VALID;
52 b->flags &= ~B_DIRTY;
53 wakeup(b); //喚醒等待的主線程
54 if ((ide_queue = b->qnext) != 0)
55 ide_start_request(ide_queue); //如果隊列還有其他請求,則開始新的請求
56 release(&ide_lock);
57 }
這段代碼描述了作業系統通過中斷的方式向IDE磁盤發送I/O請求,通過3個主要函數來實作:
(1)第一個函數是ide_rw():它會将一個請求加入隊列(如果前面還有請求未處理完成),或者直接将請求發送到磁盤(如果隊列為空,直接調用ide_start_request()函數),但不論哪種情況,調用程序進入睡眠狀态,等待請求處理完成。
(2)第二個函數是ide_start_request():它使用outb等函數(這些函數封裝了PMIO的out指令),向I/O接口的指令寄存器寫入指令(見代碼注釋),如果是寫請求,還會向資料寄存器寫入資料。在發起請求之前,ide_start_request()會調用ide_wait_ready(),來確定驅動處于就緒狀态。
(3)第三個函數是ide_intr():它是一個中斷響應處理程式,當IDE磁盤執行完I/O操作,會發出一個硬體中斷,ide_intr()會被調用。如果是寫操作,表示寫操作已經執行完畢;如果是讀操作,表示磁盤已經将内部資料送至I/O接口的資料寄存器,可以進行使用(即insl(0x1f0, b->data, 512/4)這行代碼,作業系統使用in指令讀取到記憶體去)。之後喚醒等待的程序,如果此時在隊列中還有别的未處理的請求,則調用ide_start_request()接着處理下一個I/O請求。
3.2.3.引入DMA
在标準互動流程和引入中斷流程中,資料在硬體中的移動都是通過CPU完成的,比如CPU從記憶體讀取資料到CPU寄存器,然後将CPU寄存器的資料寫入I/O裝置寄存器。但是對CPU來說,它的主要功能是使用内部的算數/邏輯單元(ALU)執行計算,而不是做一個資料搬運工,如果CPU參與大量資料的移動,就白白浪費了寶貴的時間和算力。為了讓CPU從資料移動的工作中解放出來,我們需要引入DMA機制。
DMA,全稱為direct memory access,直接記憶體通路。它是I/O裝置與主存之間由硬體組成的直接資料通路,用于高速I/O裝置與主存之間的成組資料(即資料塊)傳送。實作DMA機制的硬體叫做DMA控制器,一個典型的DMA控制器組成如下:
DMA控制器包含了多個裝置寄存器(如ADR、DBR),以及中斷控制邏輯、DMA控制邏輯、DMA接口連接配接線,這些構件的具體功能,有興趣的讀者可以閱讀《計算機組成與結構(清華大學出版社)》一書的“DMA輸入輸出方式”章節,此處不屬于本文讨論的範疇,略過不表。
引入了DMA機制之後,與I/O裝置的資料互動流程變為下圖所示:
(1)DMA預處理:在進行DMA資料傳送之前要用程式做一些必要的準備工作。先由CPU執行幾條IN/OUT指令,測試裝置狀态,向DMA控制器的裝置位址寄存器中送入I/O裝置位址并啟動I/O裝置,向主存位址寄存器中送入交換資料的主存起始位址,在資料字數寄存器中送入交換的資料個數。這些工作完成之後,CPU繼續執行原來的程式。
(2)DMA控制I/O裝置與主存之間的資料交換,并且在資料交換完畢或者出錯時,向CPU發出結束中斷請求或出錯中斷請求。
(3)CPU中斷程式進行後處理,若需繼續交換資料,則要對DMA控制器進行初始化;若不需要交換資料,則停止外設;若為出錯,則轉錯誤診斷及處理程式。
下圖仍然是與磁盤互動時各硬體執行程序任務的時間軸,可以看到,CPU将原本用于移動程序1的I/O資料的時間用于執行程序2,相應的,DMA代替了資料移動的工作:
3.2.4.總結與補充
如果根據CPU是否參與資料移動來劃分I/O類型,可以将I/O分為以下2種:
- PIO:即程式設計的I/O(programmed I/O),CPU參與資料移動,資料流向為"device <-> CPU register <-> memory"。
- DMA:CPU不參與資料移動,它隻要啟動I/O裝置并向DMA控制器發送資料傳輸相關資訊,就可以去執行其他任務,資料流向為"device <-> DMA <-> memory"。
最後,縱觀上文,我們隻使用PMIO來通路I/O裝置,以磁盤通路的C代碼為例,如何使用MMIO的方式向裝置寫入控制指令呢?Linux為我們封裝了一切,隻需要使I/O裝置通過MMIO來通路,然後使用Linux提供的MMIO函數即可,我們将在下面小節詳細讨論Linux是如何支援PMIO與MMIO的。
四、Linux的具體實作
不同架構的CPU通路I/O裝置的方式不盡相同,對Linux來說,它需要相容多種通路方式,并盡可能提供統一的抽象。
4.1.共享記憶體位址空間
PMIO的I/O位址空間獨立于記憶體位址空間,管理起來比較簡單,而MMIO需要I/O裝置和實體記憶體共享記憶體(的實體)位址空間,Linux必須精心管理記憶體以實作共享。我并不打算從頭開始講解Linux如何管理記憶體,而是在分頁和分段記憶體管理的基礎上進一步深入讨論。
4.1.1.劃分實體位址空間
以X86架構為例,32位CPU最大支援4G實體位址空間,該空間被劃分為若幹段:
- ZONE_DMA:範圍是0~16M,該段的記憶體頁專門供I/O裝置的DMA使用。之是以需要單獨管理DMA的實體頁,是因為DMA使用實體位址通路記憶體,不經過MMU,并且需要連續的緩沖區,是以為了能夠提供實體上連續的緩沖區,必須從實體位址空間專門劃分一段區域用于DMA。其中640K~1M這段位址空間被BIOS和VGA擴充卡所占據。
- ZONE_NORMAL:範圍是16M~896M,該區域的實體頁是核心能夠直接使用的。
- ZONE_HIGHMEM:範圍是896M~結束,該區域即為高端記憶體,核心不能直接使用。
可以看到,ZONE_DMA中640K~1M的區域以及ZONE_HIGHMEM中用于MMIO的區域,其被I/O裝置等占用。當CPU通路這兩個區域的實體位址時,北橋會自動将實體位址路由到相應的I/O裝置上,不會發送給實體記憶體,是以在此處的實體記憶體無法被通路,進而形成RAM空洞。
4.1.2.核心虛拟位址空間
虛拟位址空間中核心使用的部分與實體位址空間存在映射關系:
Linux使用分頁機制管理記憶體,核心想要通路實體位址空間的話,必須先建立映射關系,然後通過虛拟位址來通路。為了能夠通路所有的實體位址空間,就要将全部實體位址空間映射到1G的虛拟位址空間中,這顯然不可能,于是核心采用了分類的思想來解決這個問題:
(1)核心将0~896M的實體位址空間一對一映射到自己的虛拟位址空間中,這樣它便可以随時通路ZONE_DMA和ZONE_NORMAL裡的實體頁面,是以核心會将頻繁使用的資料,如kernel代碼、GDT、IDT、PGD、mem_map數組等放在ZONE_NORMAL裡。
(2)此時核心剩下的128M虛拟位址空間不足以完全映射所有ZONE_HIGHMEM,Linux采取了動态映射的方法,即按需的将ZONE_HIGHMEM裡的實體頁面映射到kernel space的最後128M虛拟位址空間裡,使用完之後釋放映射關系,以供其它實體頁面映射,雖然這樣存在效率的問題,但是核心畢竟可以正常的通路所有的實體位址空間了。128M虛拟位址空間主要由3部分組成,分别為vmalloc area、持久化核心映射區、臨時核心映射區,類似使用者資料、頁表(PT)等不常用資料放在ZONE_HIGHMEM裡,隻在要通路這些資料時才建立映射關系。
4.1.3.使用者虛拟位址空間
虛拟位址空間中使用者使用的部分與實體位址空間存在映射關系,下圖是實際情況中的一種映射關系:
可以看到,使用者虛拟位址空間是無法通路核心直接映射的0~896M這一塊實體位址,這與使用者态無法通路核心使用的記憶體保持了一緻。使用者虛拟位址空間的詳細布局如下,每個分段的功能不屬于本文所講内容範圍:
總的來說,核心态可以通路所有的實體位址空間,而使用者态隻能通路ZONE_HIGHMEM區域中的實體位址空間,并且其中被核心動态映射的部分無法通路,而且不能超過其虛拟位址空間中使用者區域大小。
4.2.抽象:I/O資源
為了統一管理PMIO和MMIO這兩種通路方式的I/O裝置,Linux提供了一個統一的抽象,叫做“I/O資源”,它是一個樹狀結構,每個結點記錄已配置設定位址的裝置資訊,包括裝置名稱、位址範圍、狀态/權限辨別、父節點/兄弟節點/孩子節點指針,并且PMIO和MMIO有各自獨立的I/O資源。I/O資源的結構體定義代碼如下:
1 struct resource {
2 resource_size_t start; //資源範圍的開始
3 resource_size_t end; //資源範圍的結束
4 const char *name; //資源擁有者的名字
5 unsigned long flags; //各種标志
6 struct resource *parent, *sibling, *child; //指向資源樹中父親,兄弟和孩子的指針
7 };
此外,Linux為PMIO和MMIO提供了2個獨立的函數用于申請I/O資源,它們分别是request_region()、request_mem_region(),在使用I/O裝置前,必須先通過它們申請I/O資源。我們可以簡單看一下這兩個函數的部分代碼和注釋:
1 //可以看到,這兩個函數本質上都是宏定義,真正調用的是函數__request_region,但是傳入的第一個參數不同
2 #define request_region(start, n, name) __request_region(&ioport_resource, (start), (n), (name))
3 #define request_mem_region(start, n, name) __request_region(&iomem_resource, (start), (n), (name))
4
5 //ioport_resouce,是I/O資源resource結構體的一個變量
6 struct resource ioport_resource = {
7 .name = "PCI IO",
8 .start = 0x0000,
9 .end = IO_SPACE_LIMIT,
10 .flags = IORESOURCE_IO,
11 };
12
13 //iomem_resource,也是I/O資源resource結構體的一個變量
14 struct resource iomem_resource = {
15 .name = "PCI mem",
16 .start = 0UL,
17 .end = ~0UL,
18 .flags = IORESOURCE_MEM,
19 };
20
21 //__request_region方法,代碼略。該函數沒有做實際性的映射工作,隻是告訴核心要使用一塊記憶體位址,
22 //并聲明占有,核心會為其找到符合條件的一塊記憶體位址
23 struct resource * __request_region(struct resource *parent, unsigned long start, unsigned long n, const char *name) {
24 //......
25 }
Linux在使用I/O裝置之前必須先申請I/O資源的做法,目的是告訴核心某個I/O裝置的驅動程式将使用此範圍的I/O位址,這将防止其他驅動程式對同一位址區域重複申請使用,該函數不進行任何類型的映射,它隻是一種純保留機制。
4.3.通路MMIO裝置
為了使用MMIO尋址方式來通路I/O裝置,第一步先調用request_mem_region()申請I/O資源,此時隻是完成了對該I/O裝置使用的聲明,在實體位址空間上完成了占用。接着調用ioremap()将I/O裝置的實體位址映射到作業系統核心虛拟位址空間,之後就可以通過Linux提供的函數通路這些I/O裝置接口了。通路完成後,釋放申請的位址映射以及I/O資源。
4.4.通路PMIO裝置
Linux實作了2種方式來通路PMIO的I/O裝置。
第一種方式比較好了解,就是直接使用IN/OUT指令,Linux為in、out、ins和outs彙編指令包裝了一系列輔助函數:
函數 | 說明 |
inb()、inw()、inl() | 分别從I/O接口讀取1、2或4個連續位元組。字尾“b”、“w”、“l”分别代表一個位元組(8位)、一個字(16位)以及一個長整型(32位) |
inb_p()、inw_p()、inl_p() | 分别從I/O接口讀取1、2或4個連續位元組,然後執行一條“啞元(dummy,即空指令)”指令使CPU暫停 |
outb()、outw()、outl() | 分别向一個I/O接口寫入1、2或4個連續位元組 |
outb_p()、outw_p()、outl_p() | 分别向一個I/O端口寫入1、2或4個連續位元組,然後執行一條“啞元”指令使CPU暫停 |
insb()、insw()、insl() | 分别從I/O端口讀入以1、2或4個位元組為一組的連續位元組序列,位元組序列的長度由該函數的參數給出 |
outsb()、outsw()、outsl() | 分别向I/O端口寫入以1、2或4個位元組為一組的連續位元組序列 |
使用這些輔助函數,I/O裝置通路流程如下:
第二種方式在第一種方式上增加了一層映射,目的是使用與MMIO相同的輔助函數來通路PMIO下的I/O裝置,流程如下:
可以看到,第二種方式的整體流程與MMIO非常相似,都是先調用request_region()申請I/O資源,然後調用ioport_map()這個函數,将其配置設定的位址映射到一個新的“記憶體位址”,接着可以使用Linux提供的包裝輔助函數來通路I/O裝置,通路完畢後,釋放映射與I/O資源。值得重點關注的是,ioport_map()函數到底做了什麼“記憶體映射”,是否同MMIO的ioremap()一樣?下面是ioremap的源碼:
1 void __iomem *ioport_map(unsigned long port, unsigned int nr) {
2 if (port > PIO_MASK)
3 return NULL;
4 return (void __iomem *) (unsigned long) (port + PIO_OFFSET);
5 }
ioport_map僅僅是将I/O裝置接口的實體位址簡單加上PIO_OFFSET(64k),這樣PMIO的64k位址空間就被映射到64k~128k之間,而ioremap()傳回的虛拟位址則肯定在3G之上。ioport_map所謂的映射到記憶體空間行為實際上是給開發人員制造的一個“假象”,它并沒有實際映射到核心虛拟位址,僅僅是為了讓使用者可以使用統一的輔助函數來通路I/O接口,這些輔助函數如下:
unsigned int ioread8(void *addr) | 在I/O裝置的端口位址被映射到虛拟位址之後,盡管可以直接通過指針通路這些位址,但是還是建議使用Linux核心的提供的函數來通路I/O映射記憶體。此函數用于讀取指定I/O映射記憶體位址的連續8位 |
unsigned int ioread16(void *addr) | 使用方式同ioread8,功能為讀取指定I/O映射記憶體位址的連續16位 |
unsigned int ioread32(void *addr) | 使用方式同ioread8,功能為讀取指定I/O映射記憶體位址的連續32位 |
void iowrite8(u8 value, void *addr) | 使用方式同ioread8,功能為向指定I/O映射記憶體位址寫入8位資料 |
void iowrite16(u16 value, void *addr) | 使用方式同ioread8,功能為向指定I/O映射記憶體位址寫入16位資料 |
void iowrite32(u32 value, void *addr) | 使用方式同ioread8,功能為向指定I/O映射記憶體位址寫入32位資料 |
最後來看一下ioread8的源碼,它内部對虛拟位址進行了判斷,以區分PMIO映射位址和MMIO映射位址,然後分别使用inb/outb和readb/writeb來讀寫,readb/writeb是普通的記憶體通路函數:
1 //ioread8源碼,調用一個宏指令
2 unsigned int fastcall ioread8(void __iomem *addr) {
3 IO_COND(addr, return inb(port), return readb(addr));
4 }
5
6 //宏指令IO_COND
7 #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)
8 #define IO_COND(addr, is_pio, is_mmio) do {
9 unsigned long port = (unsigned long __force)addr;
10 if (port < PIO_RESERVED) {
11 VERIFY_PIO(port);
12 port &= PIO_MASK;
13 is_pio;
14 } else {
15 is_mmio;
16 }
17 } while (0)
18
19 //宏展開後的ioread8源碼
20 unsigned int fastcall ioread8(void __iomem *addr)
21 {
22 unsigned long port = (unsigned long __force)addr;
23 if( port < 0x40000UL ) {
24 BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );
25 port &= PIO_MASK;
26 return inb(port);
27 }else{
28 return readb(addr);
29 }
30 }