I/O(一):基礎知識


本文假設你已經具備一些計算機的基本知識,包括但不限於:
  • Linux系統運行基礎知識,如用戶態、內核態。
  • Linux內存管理相關知識,如虛擬地址、物理地址、頁表。
  • 匯編語言。
  • C語言。
參考書籍和博客列表如下:
 

一、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 }

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM