9.4. 使用 I/O 內(nèi)存

2018-02-24 15:50 更新

9.4.?使用 I/O 內(nèi)存

盡管 I/O 端口在 x86 世界中流行, 用來和設(shè)備通訊的主要機制是通過內(nèi)存映射的寄存器和設(shè)備內(nèi)存. 2 者都稱為 I/O 內(nèi)存, 因為寄存器和內(nèi)存之間的區(qū)別對軟件是透明的.

I/O 內(nèi)存是簡單的一個象 RAM 的區(qū)域, 它被處理器用來跨過總線存取設(shè)備. 這個內(nèi)存可用作幾個目的, 例如持有視頻數(shù)據(jù)或者以太網(wǎng)報文, 同時實現(xiàn)設(shè)備寄存器就象 I/O 端口一樣的行為(即, 它們有讀和寫它們相關(guān)聯(lián)的邊際效果).

存取 I/O 內(nèi)存的方式依賴計算機體系, 總線, 和使用的設(shè)備, 盡管外設(shè)到處都一樣. 本章的討論主要觸及 ISA 和 PCI 內(nèi)存, 而也試圖傳遞通用的信息. 盡管存取 PCI 內(nèi)存在這里介紹, 一個 PCI 的通透介紹安排在第 12 章.

依賴計算機平臺和使用的總線, I/O 內(nèi)存可以或者不可以通過頁表來存取. 當通過頁表存取, 內(nèi)核必須首先安排從你的驅(qū)動可見的物理地址, 并且這常常意味著你必須調(diào)用 ioremap 在做任何 I/O 之前. 如果不需要頁表, I/O 內(nèi)存位置看來很象 I/O 端口, 并且你只可以使用正確的包裝函數(shù)讀和寫它們.

不管是否需要 ioremap 來存取 I/O 內(nèi)存, 不鼓勵直接使用 I/O 內(nèi)存的指針. 盡管(如同在 "I/O 端口和 I/O 內(nèi)存" 一節(jié)中介紹的 )I/O 內(nèi)存如同在硬件級別的正常 RAM 一樣尋址, 在"I/O 寄存器和傳統(tǒng)內(nèi)存"一節(jié)中概述的額外的小心建議避免正常的指針. 用來存取 I/O 內(nèi)存的包裝函數(shù)在所有平臺上是安全的并且在任何時候直接的指針解引用能夠進行操作時, 會被優(yōu)化掉.

因此, 盡管在 x86 上解引用一個指針能工作(在現(xiàn)在), 不使用正確的宏定義阻礙了驅(qū)動的移植性和可讀性.

9.4.1.?I/O 內(nèi)存分配和映射

I/O 內(nèi)存區(qū)必須在使用前分配. 分配內(nèi)存區(qū)的接口是( 在 <linux/ioport.h> 定義):


struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

這個函數(shù)分配一個 len 字節(jié)的內(nèi)存區(qū), 從 start 開始. 如果一切順利, 一個 非NULL 指針返回; 否則返回值是 NULL. 所有的 I/O 內(nèi)存分配來 /proc/iomem 中列出.

內(nèi)存區(qū)在不再需要時應當釋放:


void release_mem_region(unsigned long start, unsigned long len); 

還有一個舊的檢查 I/O 內(nèi)存區(qū)可用性的函數(shù):


int check_mem_region(unsigned long start, unsigned long len); 

但是, 對于 check_region, 這個函數(shù)是不安全和應當避免的.

在存取內(nèi)存之前, 分配 I/O 內(nèi)嵌不是唯一的要求的步驟. 你必須也保證這個 I/O 內(nèi)存已經(jīng)對內(nèi)核是可存取的. 使用 I/O 內(nèi)存不只是解引用一個指針的事情; 在許多系統(tǒng), I/O 內(nèi)存根本不是可以這種方式直接存取的. 因此必須首先設(shè)置一個映射. 這是 ioremap 函數(shù)的功能, 在第 1 章的 "vmalloc 及其友"一節(jié)中介紹的. 這個函數(shù)設(shè)計來特別的安排虛擬地址給 I/O 內(nèi)存區(qū).

一旦裝備了 ioremap (和iounmap), 一個設(shè)備驅(qū)動可以存取任何 I/O 內(nèi)存地址, 不管是否它是直接映射到虛擬地址空間. 記住, 但是, 從 ioremap 返回的地址不應當直接解引用; 相反, 應當使用內(nèi)核提供的存取函數(shù). 在我們進入這些函數(shù)之前, 我們最好回顧一下 ioremap 原型和介紹幾個我們在前一章略過的細節(jié).

這些函數(shù)根據(jù)下列定義調(diào)用:


#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);

首先, 你注意新函數(shù) ioremap_nacache. 我們在第 8 章沒有涉及它, 因為它的意思是明確地硬件相關(guān)的. 引用自一個內(nèi)核頭文件:"It’s useful if some control registers are in such an area, and write combining or read caching is not desirable.". 實際上, 函數(shù)實現(xiàn)在大部分計算機平臺上與 ioremap 一致: 在所有 I/O 內(nèi)存已經(jīng)通過非緩沖地址可見的地方, 沒有理由使用一個分開的, 非緩沖 ioremap 版本.

9.4.2.?存取 I/O 內(nèi)存

在一些平臺上, 你可能逃過作為一個指針使用 ioremap 的返回值的懲罰. 這樣的使用不是可移植的, 并且, 更加地, 內(nèi)核開發(fā)者已經(jīng)努力來消除任何這樣的使用. 使用 I/O 內(nèi)存的正確方式是通過一系列為此而提供的函數(shù)(通過 <asm/io.h> 定義的).

從 I/O 內(nèi)存讀, 使用下列之一:


unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

這里, addr 應當是從 ioremap 獲得的地址(也許與一個整型偏移); 返回值是從給定 I/O 內(nèi)存讀取的.

有類似的一系列函數(shù)來寫 I/O 內(nèi)存:


void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

如果你必須讀和寫一系列值到一個給定的 I/O 內(nèi)存地址, 你可以使用這些函數(shù)的重復版本:


void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);

void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);

這些函數(shù)讀或?qū)?count 值從給定的 buf 到 給定的 addr. 注意 count 表達為在被寫入的數(shù)據(jù)大小; ioread32_rep 讀取 count 32-位值從 buf 開始.

上面描述的函數(shù)進行所有的 I/O 到給定的 addr. 如果, 相反, 你需要操作一塊 I/O 地址, 你可使用下列之一:


void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

這些函數(shù)行為如同它們的 C 庫類似物.

如果你通覽內(nèi)核源碼, 你可看到許多調(diào)用舊的一套函數(shù), 當使用 I/O 內(nèi)存時. 這些函數(shù)仍然可以工作, 但是它們在新代碼中的使用不鼓勵. 除了別的外, 它們較少安全因為它們不進行同樣的類型檢查. 但是, 我們在這里描述它們:


unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 

這些宏定義用來從 I/O 內(nèi)存獲取 8-位, 16-位, 和 32-位 數(shù)據(jù)值.


void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 

如同前面的函數(shù), 這些函數(shù)(宏)用來寫 8-位, 16-位, 和 32-位數(shù)據(jù)項.

一些 64-位平臺也提供 readq 和 writeq, 為 PCI 總線上的 4-字(8-字節(jié))內(nèi)存操作. 這個 4-字 的命名是一個從所有的真實處理器有 16-位 字的時候的歷史遺留. 實際上, 用作 32-位 值的 L 命名也已變得不正確, 但是命名任何東西可能使事情更混淆.

9.4.3.?作為 I/O 內(nèi)存的端口

一些硬件有一個有趣的特性: 一些版本使用 I/O 端口, 而其他的使用 I/O 內(nèi)存. 輸出給處理器的寄存器在任一種情況中相同, 但是存取方法是不同的. 作為一個使驅(qū)動處理這類硬件的生活容易些的方式, 并且作為一個使 I/O 端口和內(nèi)存存取的區(qū)別最小化的方法, 2.6 內(nèi)核提供了一個函數(shù), 稱為 ioport_map:


void *ioport_map(unsigned long port, unsigned int count); 

這個函數(shù)重映射 count I/O 端口和使它們出現(xiàn)為 I/O 內(nèi)存. 從這點以后, 驅(qū)動可以在返回的地址上使用 ioread8 和其友并且根本忘記它在使用 I/O 端口.

這個映射應當在它不再被使用時恢復:


void ioport_unmap(void *addr); 

這些函數(shù)使 I/O 端口看來象內(nèi)存. 但是, 注意 I/O 端口必須仍然使用 request_region 在它們以這種方式被重映射前分配.

9.4.4.?重用 short 為 I/O 內(nèi)存

short 例子模塊, 在存取 I/O 端口前介紹的, 也能用來存取 I/O 內(nèi)存. 為此, 你必須告訴它使用 I/O 內(nèi)存在加載時; 還有, 你需要改變基地址來使它指向你的 I/O 區(qū).

例如, 這是我們?nèi)绾问褂?short 來點亮調(diào)試 LED, 在一個 MIPS 開發(fā)板上:


mips.root# ./short_load use_mem=1 base=0xb7ffffc0
mips.root# echo -n 7 > /dev/short0

使用 short 給 I/O 內(nèi)存是與它用在 I/O 端口上同樣的.

下列片段顯示了 short 在寫入一個內(nèi)存位置時用的循環(huán):


while (count--) {
 iowrite8(*ptr++, address);
    wmb(); 
} 

注意, 這里使用一個寫內(nèi)存屏障. 因為在很多體系上 iowrites8 可能轉(zhuǎn)變?yōu)橐粋€直接賦值, 需要內(nèi)存屏障來保證以希望的順序來發(fā)生.

short 使用 inb 和 outb 來顯示它如何完成. 對于讀者它可能是一個直接的練習, 但是, 改變 short 來使用 ioport_map 重映射 I/O 端口, 并且相當?shù)睾喕O碌拇a.

9.4.5.?在 1 MB 之下的 ISA 內(nèi)存

一個最著名的 I/O 內(nèi)存區(qū)是在個人計算機上的 ISA 范圍. 這是在 640 KB(0xA0000)和 1 MB(0x100000)之間的內(nèi)存范圍. 因此, 它正好出現(xiàn)于常規(guī)內(nèi)存 RAM 中間. 這個位置可能看起來有點奇怪; 它是一個在 1980 年代早期所作的決定的產(chǎn)物, 當時 640 KB 內(nèi)存看來多于任何人可能用到的大小.

這個內(nèi)存方法屬于非直接映射的內(nèi)存類別. [36]你可以讀/寫幾個字節(jié)在這個內(nèi)存范圍, 如同前面解釋的使用 short 模塊, 就是, 通過在加載時設(shè)置 use_mem.

盡管 ISA I/O 內(nèi)存只在 x86-類 計算機中存在, 我們認為值得用幾句話和一個例子驅(qū)動.

我們不會談論 PCI 在本章, 因為它是最干凈的一類 I/O 內(nèi)存: 一旦你知道內(nèi)存地址, 你可簡單地重映射和存取它. PCI I/O 內(nèi)存的"問題"是它不能為本章提供一個能工作的例子, 因為我們不能事先知道你的 PCI 內(nèi)存映射到的物理地址, 或者是否它是安全的來存取任一這些范圍. 我們選擇來描述 ISA 內(nèi)存范圍, 因為它不但少干凈并且更適合運行例子代碼.

為演示存取 ISA 內(nèi)存, 我們還使用另一個 silly 小模塊( 例子源碼的一部分). 實際上, 這個稱為 silly, 作為 Simple Tool for Unloading and Printing ISA Data 的縮寫, 或者如此的東東.

模塊補充了 short 的功能, 通過存取整個 384-KB 內(nèi)存空間和通過顯示所有的不同 I/O 功能. 它特有 4 個設(shè)備節(jié)點來進行同樣的任務, 使用不同的數(shù)據(jù)傳輸函數(shù). silly 設(shè)備作為一個 I/O 內(nèi)存上的窗口, 以類似 /dev/mem 的方式. 你可以讀和寫數(shù)據(jù), 并且lseek 到一個任意 I/O 內(nèi)存地址.

因為 silly 提供了對 ISA 內(nèi)存的存取, 它必須開始于從映射物理 ISA 地址到內(nèi)核虛擬地址. 在 Linux 內(nèi)核的早期, 一個人可以簡單地安排一個指針給一個感興趣的 ISA 地址, 接著直接對它解引用. 在現(xiàn)代世界, 但是, 我們必須首先使用虛擬內(nèi)存系統(tǒng)和重映射內(nèi)存范圍. 這個映射使用 ioremap 完成, 如同前面為 short 解釋的:


#define ISA_BASE 0xA0000
#define ISA_MAX 0x100000 /* for general memory access */

/* this line appears in silly_init */
io_base = ioremap(ISA_BASE, ISA_MAX - ISA_BASE); 

ioremap 返回一個指針值, 它能被用來使用 ioread8 和其他函數(shù), 在"存取 I/O 內(nèi)存"一節(jié)中解釋.

讓我們回顧我們的例子模塊來看看這些函數(shù)如何被使用. /dev/sillyb, 特有次編號 0, 存取 I/O 內(nèi)存使用 ioread8 和 iowrite8. 下列代碼顯示了讀的實現(xiàn), 它使地址范圍 0xA0000-0xFFFF 作為一個虛擬文件在范圍 0-0x5FFF. 讀函數(shù)構(gòu)造為一個 switch 語句在不同存取模式上; 這是 sillyb 例子:


case M_8:
 while (count) {
 *ptr = ioread8(add);
 add++;
 count--;
 ptr++;

 }
 break;

實際上, 這不是完全正確. 內(nèi)存范圍是很小和很頻繁的使用, 以至于內(nèi)核在啟動時建立頁表來存取這些地址. 但是, 這個用來存取它們的虛擬地址不是同一個物理地址, 并且因此無論如何需要 ioremap.

下 2 個設(shè)備是 /dev/sillyw (次編號 1) 和 /dev/silly1 (次編號 2). 它們表現(xiàn)象 /dev/sillyb, 除了它們使用 16-位 和 32-位 函數(shù). 這是 sillyl 的寫實現(xiàn), 又一次部分 switch:


case M_32:
 while (count >= 4) {
 iowrite8(*(u32 *)ptr, add);
 add += 4;
 count -= 4;
 ptr += 4;

 }
 break;

最后的設(shè)備是 /dev/sillycp (次編號 3), 它使用 memcpy_*io 函數(shù)來進行同樣的任務. 這是它的讀實現(xiàn)的核心:


case M_memcpy:
 memcpy_fromio(ptr, add, count);
 break;

因為 ioremap 用來提供對 ISA 內(nèi)存區(qū)的存取, silly 必須調(diào)用 iounmap 當模塊卸載時:


iounmap(io_base); 

9.4.6.?isa_readb 和其友

看一下內(nèi)核源碼會展現(xiàn)另一套函數(shù), 有如 isareadb 的名子. 實際上, 每個剛才描述的函數(shù)都有一個 isa 對等體. 這些函數(shù)提供對 ISA 內(nèi)存的存取不需要一個單獨的 ioremap 步驟. 但是, 來自內(nèi)核開發(fā)者的話, 是這些函數(shù)打算用來作為暫時的驅(qū)動移植輔助, 并且它可能將來消失. 因此, 你應當避免使用它們.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號