- Linux系統運行基礎知識,如用戶態、內核態。
- Linux內存管理相關知識,如虛擬地址、物理地址、頁表。
- 匯編語言。
- C語言。
- 《深入理解計算機系統》
- 《操作系統導論》
- 《計算機組成與結構(清華大學出版社)》
- 《新一代匯編語言程序設計(高等教育出版社)》
- 博客:設備I/O模型(鏈接:https://github.com/zhangjaycee/real_tech/wiki/distri_018)
- 博客:Linux系統對IO端口和IO內存的管理(鏈接:https://blog.csdn.net/iteye_21199/article/details/82248794)
- 博客:淺談內存映射I/O(MMIO)與端口映射I/O(PMIO)的區別(鏈接:https://www.cnblogs.com/idorax/p/7691334.html)
- 博客:PCI設備的地址空間(鏈接:https://www.cnblogs.com/zszmhd/archive/2012/05/08/2490105.html)
- 博客:PCIE的內存地址空間、I/O地址空間和配置地址空間(鏈接:https://www.cnblogs.com/yangxingsha/p/11551472.html)
- 博客:Linux內存尋址和內存管理(鏈接:https://www.cnblogs.com/zszmhd/archive/2012/08/29/2661461.html)
- 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指令如何讀寫硬盤(鏈接:https://blog.csdn.net/farmwang/article/details/49999879)
一、I/O體系結構
二、I/O設備與總線
本節我們學習I/O分層中的最底層,即與I/O設備相關的硬件知識。
2.1.標准I/O設備
2.2.計算機總線
三、與I/O設備交互
3.1.訪問I/O設備
- 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
3.1.2.MMIO
當CPU訪問某個虛擬內存地址時,該虛擬地址首先轉換為一個物理地址,對該物理地址的訪問,會通過南北橋(現在被合並為I/O橋)的路由機制被定向到物理內存或者I/O設備上。因此,用於訪問內存的CPU指令也可用於訪問I/O設備,並且在內存(的物理)地址空間上,需要給I/O設備預留一個地址區域,該地址區域不能給物理內存使用。
3.1.3.PCI設備
3.2.數據交互流程
3.2.1.標准交互流程
3.2.2.引入中斷
為了深入理解,我們引入一段《操作系統導論》中的代碼:
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個主要函數來實現:
3.2.3.引入DMA
3.2.4.總結與補充
- PIO:即編程的I/O(programmed I/O),CPU參與數據移動,數據流向為"device <-> CPU register <-> memory"。
- DMA:CPU不參與數據移動,它只要啟動I/O設備並向DMA控制器發送數據傳輸相關信息,就可以去執行其他任務,數據流向為"device <-> DMA <-> memory"。
四、Linux的具體實現
不同架構的CPU訪問I/O設備的方式不盡相同,對Linux來說,它需要兼容多種訪問方式,並盡可能提供統一的抽象。
4.1.共享內存地址空間
4.1.1.划分物理地址空間
- 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.用戶虛擬地址空間
4.2.抽象: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設備
4.4.訪問PMIO設備
| 函數 | 說明 |
| 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個字節為一組的連續字節序列 |
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位數據 |
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 }
