- 論壇徽章:
- 0
|
盡管擺弄 scull 和類似的玩具是對(duì)于 Linux 設(shè)備驅(qū)動(dòng)的軟件接口一個(gè)很好的入門, 但是實(shí)現(xiàn)一個(gè)真正的設(shè)備需要硬件. 驅(qū)動(dòng)是軟件概念和硬件電路之間的抽象層; 如同這樣, 需要與兩者溝通. 直到現(xiàn)在, 我們已經(jīng)檢查了軟件概念的內(nèi)部; 本章完成這個(gè)圖像通過(guò)向你展示一個(gè)驅(qū)動(dòng)如何存取 I/O 端口和 I/O 內(nèi)存, 同時(shí)在各種 Linux 平臺(tái)是可移植的.
本章繼續(xù)盡可能保持獨(dú)立于特殊的硬件的傳統(tǒng). 但是, 在需要一個(gè)特殊例子的地方, 我們使用簡(jiǎn)單的數(shù)字 I/O 端口(例如標(biāo)準(zhǔn)的 PC 并口)來(lái)展示 I/O 指令如何工作, 以及正常的幀緩存視頻內(nèi)存來(lái)展示內(nèi)存映射的I/O.
我們選擇簡(jiǎn)單的數(shù)字 I/O, 因?yàn)樗且粋(gè)輸入/輸出打開的最簡(jiǎn)單形式. 同樣, 并口實(shí)現(xiàn)原始 I/O 并且在大部分計(jì)算機(jī)都有: 寫到設(shè)備的數(shù)據(jù)位出現(xiàn)在輸出管腳上, 并且處理器可直接存取到輸入管腳上的電平. 實(shí)際上, 你不得不連接 LED 或者一個(gè)打印機(jī)到端口上來(lái)真正地看到一個(gè)數(shù)組 I/O 操作的結(jié)果, 但是底層硬件非常易于使用.
9.1. I/O 端口和 I/O 內(nèi)存
每個(gè)外設(shè)都是通過(guò)讀寫它的寄存器來(lái)控制. 大部分時(shí)間一個(gè)設(shè)備有幾個(gè)寄存器, 并且在連續(xù)地址存取它們, 或者在內(nèi)存地址空間或者在 I/O 地址空間.
在硬件級(jí)別上, 內(nèi)存區(qū)和 I/O 區(qū)域沒(méi)有概念上的區(qū)別: 它們都是通過(guò)在地址總線和控制總線上發(fā)出電信號(hào)來(lái)存取(即, 讀寫信號(hào))[
32
]并且讀自或者寫到數(shù)據(jù)總線.
但是一些 CPU 制造商在他們的芯片上實(shí)現(xiàn)了一個(gè)單個(gè)地址空間, 有人認(rèn)為外設(shè)不同于內(nèi)存, 因此, 應(yīng)該有一個(gè)分開的地址空間. 一些處理器(最有名的是 x86 家族)有分開的讀和寫電線給 I/O 端口和特殊的 CPU 指令來(lái)存取端口.
因?yàn)橥庠O(shè)被建立來(lái)適合一個(gè)外設(shè)總線, 并且大部分流行的 I/O 總線成型在個(gè)人計(jì)算機(jī)上, 即便那些沒(méi)有單獨(dú)地址空間給 I/O 端口的處理器, 也必須在存取一些特殊設(shè)備時(shí)偽裝讀寫端口, 常常利用外部的芯片組或者 CPU 核的額外電路. 后一個(gè)方法在用在嵌入式應(yīng)用的小處理器中常見.
由于同樣的理由, Linux 在所有它運(yùn)行的計(jì)算機(jī)平臺(tái)上實(shí)現(xiàn)了 I/O 端口的概念, 甚至在那些 CPU 實(shí)現(xiàn)一個(gè)單個(gè)地址空間的平臺(tái)上. 端口存取的實(shí)現(xiàn)有時(shí)依賴特殊的主機(jī)制造和型號(hào)( 因?yàn)椴煌男吞?hào)使用不同的芯片組來(lái)映射總線傳送到內(nèi)存地址空間).
即便外設(shè)總線有一個(gè)單獨(dú)的地址空間給 I/O 端口, 不是所有的設(shè)備映射它們的寄存器到 I/O 端口. 雖然對(duì)于 ISA 外設(shè)板使用 I/O 端口是普遍的, 大部分 PCI 設(shè)備映射寄存器到一個(gè)內(nèi)存地址區(qū). 這種 I/O 內(nèi)存方法通常是首選的, 因?yàn)樗恍枰褂锰厥饽康奶幚砥髦噶? CPU 核存取內(nèi)存更加有效, 并且編譯器當(dāng)存取內(nèi)存時(shí)有更多自由在寄存器分配和尋址模式的選擇上.
9.1.1. I/O 寄存器和常規(guī)內(nèi)存
不管硬件寄存器和內(nèi)存之間的強(qiáng)相似性, 存取 I/O 寄存器的程序員必須小心避免被 CPU(或者編譯器)優(yōu)化所戲弄, 它可能修改希望的 I/O 行為.
I/O 寄存器和 RAM 的主要不同是 I/O 操作有邊際效果, 而內(nèi)存操作沒(méi)有: 一個(gè)內(nèi)存寫的唯一效果是存儲(chǔ)一個(gè)值到一個(gè)位置, 并且一個(gè)內(nèi)存讀返回最近寫到那里的值. 因?yàn)閮?nèi)存存取速度對(duì) CPU 性能是至關(guān)重要的, 這種無(wú)邊際效果的情況已被多種方式優(yōu)化: 值被緩存, 并且 讀/寫指令被重編排.
編譯器能夠緩存數(shù)據(jù)值到 CPU 寄存器而不寫到內(nèi)存, 并且即便它存儲(chǔ)它們, 讀和寫操作都能夠在緩沖內(nèi)存中進(jìn)行而不接觸物理 RAM. 重編排也可能在編譯器級(jí)別和在硬件級(jí)別都發(fā)生: 常常一個(gè)指令序列能夠執(zhí)行得更快, 如果它以不同于在程序文本中出現(xiàn)的順序來(lái)執(zhí)行, 例如, 為避免在 RISC 流水線中的互鎖. 在CISC 處理器, 要花費(fèi)相當(dāng)數(shù)量時(shí)間的操作能夠和其他的并發(fā)執(zhí)行, 更快的.
當(dāng)應(yīng)用于傳統(tǒng)內(nèi)存時(shí)(至少在單處理器系統(tǒng))這些優(yōu)化是透明和有益的, 但是它們可能對(duì)正確的 I/O 操作是致命的, 因?yàn)樗鼈兏蓴_了那些"邊際效果", 這是主要的原因?yàn)槭裁匆粋(gè)驅(qū)動(dòng)存取 I/O 寄存器. 處理器無(wú)法預(yù)見這種情形, 一些其他的操作(在一個(gè)獨(dú)立處理器上運(yùn)行, 或者發(fā)生在一個(gè) I/O 控制器的事情)依賴內(nèi)存存取的順序. 編譯器或者 CPU 可能只盡力勝過(guò)你并且重編排你請(qǐng)求的操作; 結(jié)果可能是奇怪的錯(cuò)誤而非常難于調(diào)試. 因此, 一個(gè)驅(qū)動(dòng)必須確保沒(méi)有進(jìn)行緩沖并且在存取寄存器時(shí)沒(méi)有發(fā)生讀或?qū)懙闹鼐幣?
硬件緩沖的問(wèn)題是最易面對(duì)的:底層的硬件已經(jīng)配置(或者自動(dòng)地或者通過(guò) Linux 初始化代碼)成禁止任何硬件緩沖, 當(dāng)存取 I/O 區(qū)時(shí)(不管它們是內(nèi)存還是端口區(qū)域).
對(duì)編譯器優(yōu)化和硬件重編排的解決方法是安放一個(gè)內(nèi)存屏障在必須以一個(gè)特殊順序?qū)τ布?或者另一個(gè)處理器)可見的操作之間. Linux 提供 4 個(gè)宏來(lái)應(yīng)對(duì)可能的排序需要:
#include
void barrier(void)
這個(gè)函數(shù)告知編譯器插入一個(gè)內(nèi)存屏障但是對(duì)硬件沒(méi)有影響. 編譯的代碼將所有的當(dāng)前改變的并且駐留在 CPU 寄存器的值存儲(chǔ)到內(nèi)存, 并且后來(lái)重新讀取它們當(dāng)需要時(shí). 對(duì)屏障的調(diào)用阻止編譯器跨越屏障的優(yōu)化, 而留給硬件自由做它的重編排.
#include
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
這些函數(shù)插入硬件內(nèi)存屏障在編譯的指令流中; 它們的實(shí)際實(shí)例是平臺(tái)相關(guān)的. 一個(gè) rmb ( read memory barrier) 保證任何出現(xiàn)于屏障前的讀在執(zhí)行任何后續(xù)讀之前完成. wmb 保證寫操作中的順序, 并且 mb 指令都保證. 每個(gè)這些指令是一個(gè)屏障的超集.
read_barrier_depends 是讀屏障的一個(gè)特殊的, 弱些的形式. 而 rmb 阻止所有跨越屏障的讀的重編排, read_barrier_depends 只阻止依賴來(lái)自其他讀的數(shù)據(jù)的讀的重編排. 區(qū)別是微小的, 并且它不在所有體系中存在. 除非你確切地理解做什么, 并且你有理由相信, 一個(gè)完整的讀屏障確實(shí)是一個(gè)過(guò)度地性能開銷, 你可能應(yīng)當(dāng)堅(jiān)持使用 rmb.
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
屏障的這些版本僅當(dāng)內(nèi)核為 SMP 系統(tǒng)編譯時(shí)插入硬件屏障; 否則, 它們都擴(kuò)展為一個(gè)簡(jiǎn)單的屏障調(diào)用.
在一個(gè)設(shè)備驅(qū)動(dòng)中一個(gè)典型的內(nèi)存屏障的用法可能有這樣的形式:
writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb();
writel(dev->registers.control, DEV_GO);
在這種情況, 是重要的, 確保所有的控制一個(gè)特殊操作的設(shè)備寄存器在告訴它開始前已被正確設(shè)置. 內(nèi)存屏障強(qiáng)制寫以需要的順序完成.
因?yàn)閮?nèi)存屏障影響性能, 它們應(yīng)當(dāng)只用在確實(shí)需要它們的地方. 屏障的不同類型也有不同的性能特性, 因此值得使用最特定的可能類型. 例如, 在 x86 體系上, wmb() 目前什么都不做, 因?yàn)閷懙教幚砥魍獠槐恢鼐幣? 但是, 讀被重編排, 因此 mb() 被 wmb() 慢.
值得注意大部分的其他的處理同步的內(nèi)核原語(yǔ), 例如自旋鎖和原子的 _t 操作, 如同內(nèi)存屏障一樣是函數(shù). 還值得注意的是一些外設(shè)總線(例如 PCI 總線)有它們自己的緩沖問(wèn)題; 我們?cè)谝院笳鹿?jié)遇到時(shí)討論它們.
一些體系允許一個(gè)賦值和一個(gè)內(nèi)存屏障的有效組合. 內(nèi)核提供了幾個(gè)宏來(lái)完成這個(gè)組合; 在缺省情況下, 它們?nèi)缦露x:
#define set_mb(var, value) do {var = value; mb();} while 0
#define set_wmb(var, value) do {var = value; wmb();} while 0
#define set_rmb(var, value) do {var = value; rmb();} while 0
在合適的地方, 定義這些宏來(lái)使用體系特定的指令來(lái)很快完成任務(wù). 注意 set_rmb 只在少量體系上定義. (一個(gè) do...while 結(jié)構(gòu)的使用是一個(gè)標(biāo)準(zhǔn) C 用語(yǔ), 來(lái)使被擴(kuò)展的宏作為一個(gè)正常的 C 語(yǔ)句可在所有上下文中工作).
[
32
] 不是所有的計(jì)算機(jī)平臺(tái)使用一個(gè)讀和一個(gè)寫信號(hào); 有些有不同的方法來(lái)尋址外部電路. 這個(gè)不同在軟件層次是無(wú)關(guān)的, 但是, 我們將假設(shè)全部有讀和寫來(lái)簡(jiǎn)化討論.
9.2. 使用 I/O 端口
I/O 端口是驅(qū)動(dòng)用來(lái)和很多設(shè)備通訊的方法, 至少部分時(shí)間. 這節(jié)涉及可用的各種函數(shù)來(lái)使用 I/O 端口; 我們也觸及一些可移植性問(wèn)題.
9.2.1. I/O 端口分配
如同你可能希望的, 你不應(yīng)當(dāng)離開并開始抨擊 I/O 端口而沒(méi)有首先確認(rèn)你對(duì)這些端口有唯一的權(quán)限. 內(nèi)核提供了一個(gè)注冊(cè)接口以允許你的驅(qū)動(dòng)來(lái)聲明它需要的端口. 這個(gè)接口中的核心的函數(shù)是 request_region:
#include
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
這個(gè)函數(shù)告訴內(nèi)核, 你要使用 n 個(gè)端口, 從 first 開始. name 參數(shù)應(yīng)當(dāng)是你的設(shè)備的名子. 如果分配成功返回值是非 NULL. 如果你從 request_region 得到 NULL, 你將無(wú)法使用需要的端口.
所有的的端口分配顯示在 /proc/ioports 中. 如果你不能分配一個(gè)需要的端口組, 這是地方來(lái)看看誰(shuí)先到那里了.
當(dāng)你用完一組 I/O 端口(在模塊卸載時(shí), 也許), 應(yīng)當(dāng)返回它們給系統(tǒng), 使用:
void release_region(unsigned long start, unsigned long n);
還有一個(gè)函數(shù)以允許你的驅(qū)動(dòng)來(lái)檢查是否一個(gè)給定的 I/O 端口組可用:
int check_region(unsigned long first, unsigned long n);
這里, 如果給定的端口不可用, 返回值是一個(gè)負(fù)錯(cuò)誤碼. 這個(gè)函數(shù)是不推薦的, 因?yàn)樗姆祷刂挡槐WC是否一個(gè)分配會(huì)成功; 檢查和后來(lái)的分配不是一個(gè)原子的操作. 我們列在這里因?yàn)閹讉(gè)驅(qū)動(dòng)仍然在使用它, 但是你調(diào)用一直使用 request_region, 它進(jìn)行要求的加鎖來(lái)保證分配以一個(gè)安全的原子的方式完成.
9.2.2. 操作 I/O 端口
在驅(qū)動(dòng)硬件請(qǐng)求了在它的活動(dòng)中需要使用的 I/O 端口范圍之后, 它必須讀且/或?qū)懙竭@些端口. 為此, 大部分硬件區(qū)別8-位, 16-位, 和 32-位端口. 常常你無(wú)法混合它們, 象你正常使用系統(tǒng)內(nèi)存存取一樣.[
33
]
一個(gè) C 程序, 因此, 必須調(diào)用不同的函數(shù)來(lái)存取不同大小的端口. 如果在前一節(jié)中建議的, 只支持唯一內(nèi)存映射 I/O 寄存器的計(jì)算機(jī)體系偽裝端口 I/O , 通過(guò)重新映射端口地址到內(nèi)存地址, 并且內(nèi)核向驅(qū)動(dòng)隱藏了細(xì)節(jié)以便易于移植. Linux 內(nèi)核頭文件(特別地, 體系依賴的頭文件 ) 定義了下列內(nèi)聯(lián)函數(shù)來(lái)存取 I/O 端口:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
讀或?qū)懽止?jié)端口( 8 位寬 ). port 參數(shù)定義為 unsigned long 在某些平臺(tái)以及 unsigned short 在其他的上. inb 的返回類型也是跨體系而不同的.
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
這些函數(shù)存取 16-位 端口( 一個(gè)字寬 ); 在為 S390 平臺(tái)編譯時(shí)它們不可用, 它只支持字節(jié) I/O.
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
這些函數(shù)存取 32-位 端口. longword 聲明為或者 unsigned long 或者 unsigned int, 根據(jù)平臺(tái). 如同字 I/O, "Long" I/O 在 S390 上不可用.
從現(xiàn)在開始, 當(dāng)我們使用 unsigned 沒(méi)有進(jìn)一步類型規(guī)定時(shí), 我們指的是一個(gè)體系相關(guān)的定義, 它的確切特性是不相關(guān)的. 函數(shù)幾乎一直是可移植的, 因?yàn)榫幾g器自動(dòng)轉(zhuǎn)換值在賦值時(shí) -- 它們是 unsigned 有助于阻止編譯時(shí)的警告. 這樣的轉(zhuǎn)換不丟失信息, 只要程序員安排明智的值來(lái)避免溢出. 我們堅(jiān)持這個(gè)"未完成的類型"傳統(tǒng)貫串本章.
注意, 沒(méi)有定義 64-位 端口 I/O 操作. 甚至在 64-位 體系中, 端口地址空間使用一個(gè)32-位(最大)的數(shù)據(jù)通路.
9.2.3. 從用戶空間的 I/O 存取
剛剛描述的這些函數(shù)主要打算被設(shè)備驅(qū)動(dòng)使用, 但它們也可從用戶空間使用, 至少在 PC-類 的計(jì)算機(jī). GNU C 庫(kù)在 中定義它們. 下列條件應(yīng)當(dāng)應(yīng)用來(lái)對(duì)于 inb 及其友在用戶空間代碼中使用:
程序必須使用 -O 選項(xiàng)編譯來(lái)強(qiáng)制擴(kuò)展內(nèi)聯(lián)函數(shù).
ioperm 和 iopl 系統(tǒng)調(diào)用必須用來(lái)獲得權(quán)限來(lái)進(jìn)行對(duì)端口的 I/O 操作. ioperm 為單獨(dú)端口獲取許可, 而 iopl 為整個(gè) I/O 空間獲取許可. 這 2 個(gè)函數(shù)都是 x86 特有的.
程序必須作為 root 來(lái)調(diào)用 ioperm 或者 iopl.[
34
] 可選地, 一個(gè)它的祖先必須已贏得作為 root 運(yùn)行的端口權(quán)限.
如果主機(jī)平臺(tái)沒(méi)有 ioperm 和 iopl 系統(tǒng)調(diào)用, 用戶空間仍然可以存取 I/O 端口, 通過(guò)使用 /dev/prot 設(shè)備文件. 注意, 但是, 這個(gè)文件的含義是非常平臺(tái)特定的, 并且對(duì)任何東西除了 PC 不可能有用.
例子源碼 misc-progs/inp.c 和 misc-progs/outp.c 是一個(gè)從命令行讀寫端口的小工具, 在用戶空間. 它們希望被安裝在多個(gè)名子下(例如, inb, inw, 和 inl 并且操作字節(jié), 字, 或者長(zhǎng)端口依賴于用戶調(diào)用哪個(gè)名子). 它們使用 ioperm 或者 iopl 在 x86下, 在其他平臺(tái)是 /dev/port.
程序可以做成 setuid root, 如果你想過(guò)危險(xiǎn)生活并且在不要求明確的權(quán)限的情況下使用你的硬件. 但是, 請(qǐng)不要在產(chǎn)品系統(tǒng)上以 set-uid 安裝它們; 它們是設(shè)計(jì)上的安全漏洞.
9.2.4. 字串操作
除了單發(fā)地輸入和輸出操作, 一些處理器實(shí)現(xiàn)了特殊的指令來(lái)傳送一系列字節(jié), 字, 或者 長(zhǎng)字 到和自一個(gè)單個(gè) I/O 端口或者同樣大小. 這是所謂的字串指令, 并且它們完成任務(wù)比一個(gè) C 語(yǔ)言循環(huán)能做的更快. 下列宏定義實(shí)現(xiàn)字串處理的概念或者通過(guò)使用一個(gè)單個(gè)機(jī)器指令或者通過(guò)執(zhí)行一個(gè)緊湊的循環(huán), 如果目標(biāo)處理器沒(méi)有進(jìn)行字串 I/O 的指令. 當(dāng)編譯為 S390 平臺(tái)時(shí)這些宏定義根本不定義. 這應(yīng)當(dāng)不是個(gè)移植性問(wèn)題, 因?yàn)檫@個(gè)平臺(tái)通常不與其他平臺(tái)共享設(shè)備驅(qū)動(dòng), 因?yàn)樗耐庠O(shè)總線是不同的.
字串函數(shù)的原型是:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
讀或?qū)憦膬?nèi)存地址 addr 開始的 count 字節(jié). 數(shù)據(jù)讀自或者寫入單個(gè) port 端口.
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
讀或?qū)?16-位 值到一個(gè)單個(gè) 16-位 端口.
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
讀或?qū)?32-位 值到一個(gè)單個(gè) 32-位 端口.
有件事要記住, 當(dāng)使用字串函數(shù)時(shí): 它們移動(dòng)一個(gè)整齊的字節(jié)流到或自端口. 當(dāng)端口和主系統(tǒng)有不同的字節(jié)對(duì)齊規(guī)則, 結(jié)果可能是令人驚訝的. 使用 inw 讀取一個(gè)端口交換這些字節(jié), 如果需要, 來(lái)使讀取的值匹配主機(jī)字節(jié)序. 字串函數(shù), 相反, 不進(jìn)行這個(gè)交換.
9.2.5. 暫停 I/O
一些平臺(tái) - 最有名的 i386 - 可能有問(wèn)題當(dāng)處理器試圖太快傳送數(shù)據(jù)到或自總線. 當(dāng)處理器對(duì)于外設(shè)總線被過(guò)度鎖定時(shí)可能引起問(wèn)題( 想一下 ISA )并且可能當(dāng)設(shè)備單板太慢時(shí)表現(xiàn)出來(lái). 解決方法是插入一個(gè)小的延時(shí)在每個(gè) I/O 指令后面, 如果跟隨著另一個(gè)指令. 在 x86 上, 這個(gè)暫停是通過(guò)進(jìn)行一個(gè) outb 指令到端口 0x80 ( 正常地不是常常用到 )實(shí)現(xiàn)的, 或者通過(guò)忙等待. 細(xì)節(jié)見你的平臺(tái)的 asm 子目錄的 io.h 文件.
如果你的設(shè)備丟失一些數(shù)據(jù), 或者如果你擔(dān)心它可能丟失一些, 你可以使用暫停函數(shù)代替正常的那些. 暫停函數(shù)正如前面列出的, 但是它們的名子以 _p 結(jié)尾; 它們稱為 inb_p, outb_p, 等等. 這些函數(shù)定義給大部分被支持的體系, 盡管它們常常擴(kuò)展為與非暫停 I/O 同樣的代碼, 因?yàn)闆](méi)有必要額外暫停, 如果體系使用一個(gè)合理的現(xiàn)代外設(shè)總線.
9.2.6. 平臺(tái)依賴性
I/O 指令, 由于它們的特性, 是高度處理器依賴的. 因?yàn)樗鼈兪褂锰幚砥魅绾翁幚硪七M(jìn)移出的細(xì)節(jié), 是非常難以隱藏系統(tǒng)間的不同. 作為一個(gè)結(jié)果, 大部分的關(guān)于端口 I/O 的源碼是平臺(tái)依賴的.
你可以看到一個(gè)不兼容, 數(shù)據(jù)類型, 通過(guò)回看函數(shù)的列表, 這里參數(shù)是不同的類型, 基于平臺(tái)間的體系不同點(diǎn). 例如, 一個(gè)端口是 unsigned int 在 x86 (這里處理器支持一個(gè) 64-KB I/O 空間), 但是在別的平臺(tái)是 unsiged long, 這里的端口只是同內(nèi)存一樣的同一個(gè)地址空間中的特殊位置.
其他的平臺(tái)依賴性來(lái)自處理器中的基本的結(jié)構(gòu)性不同, 并且, 因此, 無(wú)可避免地. 我們不會(huì)進(jìn)入這個(gè)依賴性的細(xì)節(jié), 因?yàn)槲覀兗俣悴粫?huì)給一個(gè)特殊的系統(tǒng)編寫設(shè)備驅(qū)動(dòng)而沒(méi)有理解底層的硬件. 相反, 這是一個(gè)內(nèi)核支持的體系的能力的概括:
IA-32 (x86)
x86_64
這個(gè)體系支持所有的本章描述的函數(shù). 端口號(hào)是 unsigned short 類型.
IA-64 (Itanium)
支持所有函數(shù); 端口是 unsigned long(以及內(nèi)存映射的)). 字串函數(shù)用 C 實(shí)現(xiàn).
Alpha
支持所有函數(shù), 并且端口是內(nèi)存映射的. 端口 I/O 的實(shí)現(xiàn)在不同 Alpha 平臺(tái)上是不同的, 根據(jù)它們使用的芯片組. 字串函數(shù)用 C 實(shí)現(xiàn)并且定義在 arch/alpha/lib/io.c 中定義. 端口是 unsigned long.
ARM
端口是內(nèi)存映射的, 并且支持所有函數(shù); 字串函數(shù)用 C 實(shí)現(xiàn). 端口是 unsigned int 類型.
Cris
這個(gè)體系不支持 I/O 端口抽象, 甚至在一個(gè)模擬模式; 各種端口操作定義成什么不做.
M68k
M68k
端口是內(nèi)存映射的. 支持字串函數(shù), 并且端口類型是 unsigned char.
MIPS
MIPS64
MIPS 端口支持所有的函數(shù). 字串操作使用緊湊匯編循環(huán)來(lái)實(shí)現(xiàn), 因?yàn)樘幚砥魅狈C(jī)器級(jí)別的字串 I/O. 端口是內(nèi)存映射的; 它們是 unsigned long.
PA
支持所有函數(shù); 端口是 int 在基于 PCI 的系統(tǒng)上以及 unsigned short 在 EISA 系統(tǒng), 除了字串操作, 它們使用 unsigned long 端口號(hào).
PowerPC
PowerPC64
支持所有函數(shù); 端口有 unsigned char * 類型在 32-位 系統(tǒng)上并且 unsigned long 在 64-位 系統(tǒng)上.
S390
類似于 M68k, 這個(gè)平臺(tái)的頭文件只支持字節(jié)寬的端口 I/O, 而沒(méi)有字串操作. 端口是 char 指針并且是內(nèi)存映射的.
Super
端口是 unsigned int ( 內(nèi)存映射的 ), 并且支持所有函數(shù).
SPARC SPARC64
再一次, I/O 空間是內(nèi)存映射的. 端口函數(shù)的版本定義來(lái)使用 unsigned long 端口.
好奇的讀者能夠從 io.h 文件中獲得更多信息, 這個(gè)文件有時(shí)定義幾個(gè)結(jié)構(gòu)特定的函數(shù), 加上我們?cè)诒菊轮忻枋龅哪切? 但是, 警告有些這些文件是相當(dāng)難讀的.
有趣的是注意沒(méi)有 x86 家族之外的處理器具備一個(gè)不同的地址空間給端口, 盡管幾個(gè)被支持的家族配備有 ISA 和/或 PCI 插槽 ( 并且 2 種總線實(shí)現(xiàn)分開的 I/O 和地址空間 ).
更多地, 有些處理器(最有名的是早期的 Alphas)缺乏一次移動(dòng)一個(gè)或 2 個(gè)字節(jié)的指令.[
35
] 因此, 它們的外設(shè)芯片組模擬 8-位 和 16-位 I/O 存取, 通過(guò)映射它們到內(nèi)存地址空間的特殊的地址范圍. 因此, 操作同一個(gè)端口的一個(gè) inb 和 一個(gè) inw 指令, 通過(guò) 2 個(gè)操作不同地址的 32-位內(nèi)存讀來(lái)實(shí)現(xiàn). 幸運(yùn)的是, 所有這些都對(duì)設(shè)備驅(qū)動(dòng)編寫者隱藏了, 通過(guò)本節(jié)中描述的宏的內(nèi)部, 但是我們覺(jué)得它是一個(gè)要注意的有趣的特性. 如果你想深入探究, 查找在 include/asm-alpha/core_lca.h 中的例子.
在每個(gè)平臺(tái)的程序員手冊(cè)中充分描述了I/O 操作如何在每個(gè)平臺(tái)上進(jìn)行; 這些手冊(cè)常常在 WEB 上作為 PDF 下載.
[
33
] 有時(shí) I/O 端口象內(nèi)存一樣安排, 并且你可(例如)綁定 2 個(gè) 8-位 寫為一個(gè)單個(gè) 16-位 操作. 例如, 這應(yīng)用于 PC 視頻板. 但是通常, 你不能指望這個(gè)特色.
[
34
] 技術(shù)上, 它必須有 CAP_SYS_RAWIO 能力, 但是在大部分當(dāng)前系統(tǒng)中這是與作為 root 運(yùn)行是同樣的.
[
35
] 單字節(jié) I/O 不是一個(gè)人可能想象的那么重要, 因?yàn)樗且粋(gè)稀少的操作. 為讀/寫一個(gè)單字節(jié)到任何地址空間, 你需要實(shí)現(xiàn)一個(gè)數(shù)據(jù)通道, 連接寄存器組的數(shù)據(jù)總線的低位到外部數(shù)據(jù)總線的任意字節(jié)位置. 這些數(shù)據(jù)通道需要額外的邏輯門在每個(gè)數(shù)據(jù)傳輸?shù)耐ǖ郎? 丟掉字節(jié)寬的載入和存儲(chǔ)能夠使整個(gè)系統(tǒng)性能受益.
9.3. 一個(gè) I/O 端口例子
我們用來(lái)展示一個(gè)設(shè)備驅(qū)動(dòng)內(nèi)的端口 I/O 的例子代碼, 操作通用的數(shù)字 I/O 端口; 這樣的端口在大部分計(jì)算機(jī)系統(tǒng)中找到.
一個(gè)數(shù)字 I/O 端口, 在它的大部分的普通的化身中, 是一個(gè)字節(jié)寬的 I/O 位置, 或者內(nèi)存映射的或者端口映射的. 當(dāng)你寫一個(gè)值到一個(gè)輸出位置, 在輸出管腳上見到的電信號(hào)根據(jù)寫入的單個(gè)位而改變. 當(dāng)你從一個(gè)輸入位置讀取一個(gè)值, 輸入管腳上所見的當(dāng)前邏輯電平作為單個(gè)位的值被返回.
這樣的 I/O 端口的實(shí)際實(shí)現(xiàn)和軟件接口各個(gè)系統(tǒng)不同. 大部分時(shí)間, I/O 管腳由 2 個(gè) I/O 位置控制: 一個(gè)允許選擇使用那些位作為輸入, 哪些位作為輸出, 以及一個(gè)可以實(shí)際讀或?qū)戇壿嬰娖降? 有時(shí), 但是, 事情可能更簡(jiǎn)單, 并且這些位是硬連線為輸入或輸出(但是, 在這個(gè)情況下, 它們不再是所謂的"通用 I/O"); 在所有個(gè)人計(jì)算機(jī)上出現(xiàn)的并口是這樣一個(gè)非通用 I/O 端口. 任一方式, I/O 管腳對(duì)我們馬上介紹的例子代碼是可用的.
9.3.1. 并口縱覽
因?yàn)槲覀兤谕蟛糠肿x者以所謂的"個(gè)人計(jì)算機(jī)"的形式使用一個(gè) x86 平臺(tái), 我們覺(jué)得值得解釋一下 PC 并口如何設(shè)計(jì)的. 并口是在個(gè)人計(jì)算機(jī)上運(yùn)行數(shù)字 I/O 例子代碼的外設(shè)接口選擇. 盡管大部分讀者可能有并口規(guī)范用, 為你的方便, 我們?cè)谶@里總結(jié)一下它們.
并口, 在它的最小配置中 ( 我們?yōu)g覽一下 ECP 和 EPP 模式) 由 3 個(gè) 8-位端口組成. PC 標(biāo)準(zhǔn)在 0x378 開始第一個(gè)并口的 I/O 端口并且第 2 個(gè)在 0x278. 第一個(gè)端口是一個(gè)雙向數(shù)據(jù)寄存器; 它直接連接到物理連接器的管腳 2 - 9. 第 2 個(gè)端口是一個(gè)只讀狀態(tài)寄存器; 當(dāng)并口為打印機(jī)使用, 這個(gè)寄存器報(bào)告打印機(jī)狀態(tài)的幾個(gè)方面, 例如正在線, 缺紙, 或者忙. 第 3 個(gè)端口是一個(gè)只出控制寄存器, 它, 在其他東西中, 控制是否中斷使能.
并口通訊中使用的信號(hào)電平是標(biāo)準(zhǔn)的 TTL 電平: 0 和 5 伏特, 邏輯門限在大概 1.2 伏特. 你可依靠端口至少符合標(biāo)準(zhǔn) TTL LS 電流規(guī)格, 盡管大部分現(xiàn)代并口在電流和電壓額定值都工作的好.
并口連接器和計(jì)算機(jī)內(nèi)部電路不隔離, 當(dāng)你想直接連接邏輯門到這個(gè)端口是有用的. 但是你不得不小心地正確連接線; 并口電路當(dāng)你使用你自己的定制電路時(shí)容易損壞, 除非你給你的電路增加絕緣. 你可以選擇使用插座并口如果你害怕會(huì)損壞你的主板.
位的規(guī)范在圖
并口的管腳
中概述. 你可以存取 12 個(gè)輸出位和 5 個(gè)輸入位, 有些是在它們地信號(hào)路徑上邏輯地翻轉(zhuǎn)了. 唯一的沒(méi)有關(guān)聯(lián)信號(hào)管腳的位是端口 2 的位 4 (0x10), 它使能來(lái)自并口的中斷. 我們使用這個(gè)位作為我們的在第 10 章中的中斷處理的實(shí)現(xiàn)的一部分.
圖 9.1. 并口的管腳
![]()
9.3.2. 一個(gè)例子驅(qū)動(dòng)
我們介紹的驅(qū)動(dòng)稱為 short (Simple Hardware Operations and Raw Tests). 所有它做的是讀和寫幾個(gè) 8-位 端口, 從你在加載時(shí)選擇的開始. 缺省地, 它使用分配給 PC 并口的端口范圍. 每個(gè)設(shè)備節(jié)點(diǎn)(有一個(gè)獨(dú)特的次編號(hào))存取一個(gè)不同的端口. short 驅(qū)動(dòng)不做任何有用的事情; 它只是隔離來(lái)作為操作端口的單個(gè)指令給外部使用. 如果你習(xí)慣端口 I/O, 你可以使用 short 來(lái)熟悉它; 你能夠測(cè)量它花費(fèi)來(lái)通過(guò)端口傳送數(shù)據(jù)的時(shí)間或者其他游戲的時(shí)間.
為 short 在你的系統(tǒng)上運(yùn)行, 必須有存取底層硬件設(shè)備的自由(缺省地, 并口); 因此, 不能有其他驅(qū)動(dòng)已經(jīng)分配了它. 大部分現(xiàn)代發(fā)布設(shè)置并口驅(qū)動(dòng)作為只在需要時(shí)加載的模塊, 因此對(duì) I/O 地址的競(jìng)爭(zhēng)常常不是個(gè)問(wèn)題. 如果, 但是, 你從 short 得到一個(gè)"無(wú)法獲得 I/O 地址" 錯(cuò)誤(在控制臺(tái)上或者在系統(tǒng) log 文件), 一些其他的驅(qū)動(dòng)可能已經(jīng)獲得這個(gè)端口. 一個(gè)快速瀏覽 /proc/ioports 常常告訴你哪個(gè)驅(qū)動(dòng)在搗亂. 同樣的告誡應(yīng)用于另外 I/O 設(shè)備如果你沒(méi)有在使用并口.
從現(xiàn)在開始, 我們只是用"并口"來(lái)簡(jiǎn)化討論. 但是, 你能夠設(shè)置基本的模塊參數(shù)在加載時(shí)來(lái)重定向 short 到其他 I/O 設(shè)備. 這個(gè)特性允許例子代碼在任何 Linux 平臺(tái)上運(yùn)行, 這里你對(duì)一個(gè)數(shù)字 I/O 接口有權(quán)限通過(guò) outb 和 inb 存取( 盡管實(shí)際的硬件是內(nèi)存映射的, 除 x86 外的所有平臺(tái)). 后面, 在"使用 I/O 內(nèi)存"的一節(jié), 我們展示 short 如何用來(lái)使用通用的內(nèi)存映射數(shù)字 I/O.
為觀察在并口上發(fā)生了什么以及如果你有使用硬件的愛(ài)好, 你可以焊接盡管 LED 到輸出管腳. 每個(gè) LED 應(yīng)當(dāng)串連一個(gè) 1-K 電阻導(dǎo)向一個(gè)地引腳(除非, 當(dāng)然, 你的 LED 有內(nèi)嵌的電阻). 如果你連接一個(gè)輸出引腳到一個(gè)輸入管腳, 你會(huì)產(chǎn)生你自己的輸入能夠從輸入端口讀到.
注意, 你無(wú)法只連接一個(gè)打印機(jī)到并口并且看到數(shù)據(jù)發(fā)向 short. 這個(gè)驅(qū)動(dòng)實(shí)現(xiàn)簡(jiǎn)單的對(duì) I/O 端口的存取, 并且沒(méi)有進(jìn)行與打印機(jī)需要的來(lái)操作數(shù)據(jù)的握手; 在下一章, 我們展示了一個(gè)例子驅(qū)動(dòng)(稱為 shortprint ), 它能夠驅(qū)動(dòng)并口打印機(jī); 這個(gè)驅(qū)動(dòng)使用中斷, 但是, 因此我們還是不能到這一點(diǎn).
如果你要查看并口數(shù)據(jù)通過(guò)焊接 LED 到一個(gè) D-型 連接器, 我們建議你不要使用管腳 9 和管腳 10, 因?yàn)槲覀冎筮B接它們?cè)谝黄饋?lái)運(yùn)行第 10 章展示的例子代碼.
只考慮到 short, /dev/short0 寫到和讀自位于 I/O 基地址的 8-bit 端口( 0x378, 除非在加載時(shí)間改變). /dev/short1 寫到位于基址 + 1 的 8-位, 等等直到基址 + 7.
/dev/short0 進(jìn)行的實(shí)際輸出操作是基于使用 outb 的一個(gè)緊湊循環(huán). 一個(gè)內(nèi)存屏障指令用來(lái)保證輸出操作實(shí)際發(fā)生并且不被優(yōu)化掉:
while (count--) {
outb(*(ptr++), port);
wmb();
}
你可以運(yùn)行下列命令來(lái)點(diǎn)亮你的 LED:
echo -n "any string" > /dev/short0
每個(gè) LED 監(jiān)視一個(gè)單個(gè)的輸出端口位. 記住只有最后寫入的字符, 保持穩(wěn)定在輸出管腳上足夠長(zhǎng)時(shí)間你的眼睛能感覺(jué)到. 因此, 我們建議你阻止自動(dòng)插入一個(gè)結(jié)尾新行, 通過(guò)傳遞一個(gè) -n 選項(xiàng)給 echo.
讀是通過(guò)一個(gè)類似的函數(shù), 圍繞 inb 而不是 outb 建立的. 為了從并口讀"有意義的"值, 你需要某個(gè)硬件連接到連接器的輸入管腳來(lái)產(chǎn)生信號(hào). 如果沒(méi)有信號(hào), 你會(huì)讀到一個(gè)相同字節(jié)的無(wú)結(jié)尾的流. 如果你選擇從一個(gè)輸出端口讀取, 你極可能得到寫到端口的最后的值(這適用于并口和普通使用的其他數(shù)字 I/O 電路). 因此, 那些不喜歡拿出他們的烙鐵的人可以讀取當(dāng)前的輸出值在端口 0x378, 通過(guò)運(yùn)行這樣一個(gè)命令:
dd if=/dev/short0 bs=1 count=1 | od -t x1
為演示所有 I/O 指令的使用, 每個(gè) short 設(shè)備有 3 個(gè)變形: /dev/short0 進(jìn)行剛剛展示的循環(huán), /dev/short0p 使用 outb_p 和 inb_p 代替"快速"函數(shù), 并且 /dev/short0s 使用字串指令. 有 8 個(gè)這樣的設(shè)備, 從 short0 到 short7. 盡管 PC 并口只有 3 個(gè)端口, 你可能需要它們更多如果使用不同的 I/O 設(shè)備來(lái)運(yùn)行你的測(cè)試.
short 驅(qū)動(dòng)進(jìn)行一個(gè)非常少的硬件控制, 但是足夠來(lái)展示如何使用 I/O 端口指令. 感興趣的讀者可能想看看 parpor 和 parport_pc 模塊的源碼, 來(lái)知道這個(gè)設(shè)備在真實(shí)生活中能有多復(fù)雜來(lái)支持一系列并口上的設(shè)備(打印機(jī), 磁帶備份, 網(wǎng)絡(luò)接口)
9.4. 使用 I/O 內(nèi)存
盡管 I/O 端口在 x86 世界中流行, 用來(lái)和設(shè)備通訊的主要機(jī)制是通過(guò)內(nèi)存映射的寄存器和設(shè)備內(nèi)存. 2 者都稱為 I/O 內(nèi)存, 因?yàn)榧拇嫫骱蛢?nèi)存之間的區(qū)別對(duì)軟件是透明的.
I/O 內(nèi)存是簡(jiǎn)單的一個(gè)象 RAM 的區(qū)域, 它被處理器用來(lái)跨過(guò)總線存取設(shè)備. 這個(gè)內(nèi)存可用作幾個(gè)目的, 例如持有視頻數(shù)據(jù)或者以太網(wǎng)報(bào)文, 同時(shí)實(shí)現(xiàn)設(shè)備寄存器就象 I/O 端口一樣的行為(即, 它們有讀和寫它們相關(guān)聯(lián)的邊際效果).
存取 I/O 內(nèi)存的方式依賴計(jì)算機(jī)體系, 總線, 和使用的設(shè)備, 盡管外設(shè)到處都一樣. 本章的討論主要觸及 ISA 和 PCI 內(nèi)存, 而也試圖傳遞通用的信息. 盡管存取 PCI 內(nèi)存在這里介紹, 一個(gè) PCI 的通透介紹安排在第 12 章.
依賴計(jì)算機(jī)平臺(tái)和使用的總線, I/O 內(nèi)存可以或者不可以通過(guò)頁(yè)表來(lái)存取. 當(dāng)通過(guò)頁(yè)表存取, 內(nèi)核必須首先安排從你的驅(qū)動(dòng)可見的物理地址, 并且這常常意味著你必須調(diào)用 ioremap 在做任何 I/O 之前. 如果不需要頁(yè)表, I/O 內(nèi)存位置看來(lái)很象 I/O 端口, 并且你只可以使用正確的包裝函數(shù)讀和寫它們.
不管是否需要 ioremap 來(lái)存取 I/O 內(nèi)存, 不鼓勵(lì)直接使用 I/O 內(nèi)存的指針. 盡管(如同在 "I/O 端口和 I/O 內(nèi)存" 一節(jié)中介紹的 )I/O 內(nèi)存如同在硬件級(jí)別的正常 RAM 一樣尋址, 在"I/O 寄存器和傳統(tǒng)內(nèi)存"一節(jié)中概述的額外的小心建議避免正常的指針. 用來(lái)存取 I/O 內(nèi)存的包裝函數(shù)在所有平臺(tái)上是安全的并且在任何時(shí)候直接的指針解引用能夠進(jìn)行操作時(shí), 會(huì)被優(yōu)化掉.
因此, 盡管在 x86 上解引用一個(gè)指針能工作(在現(xiàn)在), 不使用正確的宏定義阻礙了驅(qū)動(dòng)的移植性和可讀性.
9.4.1. I/O 內(nèi)存分配和映射
I/O 內(nèi)存區(qū)必須在使用前分配. 分配內(nèi)存區(qū)的接口是( 在 定義):
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
這個(gè)函數(shù)分配一個(gè) len 字節(jié)的內(nèi)存區(qū), 從 start 開始. 如果一切順利, 一個(gè) 非NULL 指針?lè)祷? 否則返回值是 NULL. 所有的 I/O 內(nèi)存分配來(lái) /proc/iomem 中列出.
內(nèi)存區(qū)在不再需要時(shí)應(yīng)當(dāng)釋放:
void release_mem_region(unsigned long start, unsigned long len);
還有一個(gè)舊的檢查 I/O 內(nèi)存區(qū)可用性的函數(shù):
int check_mem_region(unsigned long start, unsigned long len);
但是, 對(duì)于 check_region, 這個(gè)函數(shù)是不安全和應(yīng)當(dāng)避免的.
在存取內(nèi)存之前, 分配 I/O 內(nèi)嵌不是唯一的要求的步驟. 你必須也保證這個(gè) I/O 內(nèi)存已經(jīng)對(duì)內(nèi)核是可存取的. 使用 I/O 內(nèi)存不只是解引用一個(gè)指針的事情; 在許多系統(tǒng), I/O 內(nèi)存根本不是可以這種方式直接存取的. 因此必須首先設(shè)置一個(gè)映射. 這是 ioremap 函數(shù)的功能, 在第 1 章的 "vmalloc 及其友"一節(jié)中介紹的. 這個(gè)函數(shù)設(shè)計(jì)來(lái)特別的安排虛擬地址給 I/O 內(nèi)存區(qū).
一旦裝備了 ioremap (和iounmap), 一個(gè)設(shè)備驅(qū)動(dòng)可以存取任何 I/O 內(nèi)存地址, 不管是否它是直接映射到虛擬地址空間. 記住, 但是, 從 ioremap 返回的地址不應(yīng)當(dāng)直接解引用; 相反, 應(yīng)當(dāng)使用內(nèi)核提供的存取函數(shù). 在我們進(jìn)入這些函數(shù)之前, 我們最好回顧一下 ioremap 原型和介紹幾個(gè)我們?cè)谇耙徽侣赃^(guò)的細(xì)節(jié).
這些函數(shù)根據(jù)下列定義調(diào)用:
#include
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. 我們?cè)诘?8 章沒(méi)有涉及它, 因?yàn)樗囊馑际敲鞔_地硬件相關(guān)的. 引用自一個(gè)內(nèi)核頭文件:"It’s useful if some control registers are in such an area, and write combining or read caching is not desirable.". 實(shí)際上, 函數(shù)實(shí)現(xiàn)在大部分計(jì)算機(jī)平臺(tái)上與 ioremap 一致: 在所有 I/O 內(nèi)存已經(jīng)通過(guò)非緩沖地址可見的地方, 沒(méi)有理由使用一個(gè)分開的, 非緩沖 ioremap 版本.
9.4.2. 存取 I/O 內(nèi)存
在一些平臺(tái)上, 你可能逃過(guò)作為一個(gè)指針使用 ioremap 的返回值的懲罰. 這樣的使用不是可移植的, 并且, 更加地, 內(nèi)核開發(fā)者已經(jīng)努力來(lái)消除任何這樣的使用. 使用 I/O 內(nèi)存的正確方式是通過(guò)一系列為此而提供的函數(shù)(通過(guò) 定義的).
從 I/O 內(nèi)存讀, 使用下列之一:
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
這里, addr 應(yīng)當(dāng)是從 ioremap 獲得的地址(也許與一個(gè)整型偏移); 返回值是從給定 I/O 內(nèi)存讀取的.
有類似的一系列函數(shù)來(lái)寫 I/O 內(nèi)存:
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
如果你必須讀和寫一系列值到一個(gè)給定的 I/O 內(nèi)存地址, 你可以使用這些函數(shù)的重復(fù)版本:
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 表達(dá)為在被寫入的數(shù)據(jù)大小; ioread32_rep 讀取 count 32-位值從 buf 開始.
上面描述的函數(shù)進(jìn)行所有的 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 庫(kù)類似物.
如果你通覽內(nèi)核源碼, 你可看到許多調(diào)用舊的一套函數(shù), 當(dāng)使用 I/O 內(nèi)存時(shí). 這些函數(shù)仍然可以工作, 但是它們?cè)谛麓a中的使用不鼓勵(lì). 除了別的外, 它們較少安全因?yàn)樗鼈儾贿M(jìn)行同樣的類型檢查. 但是, 我們?cè)谶@里描述它們:
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
這些宏定義用來(lái)從 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ù)(宏)用來(lái)寫 8-位, 16-位, 和 32-位數(shù)據(jù)項(xiàng).
一些 64-位平臺(tái)也提供 readq 和 writeq, 為 PCI 總線上的 4-字(8-字節(jié))內(nèi)存操作. 這個(gè) 4-字 的命名是一個(gè)從所有的真實(shí)處理器有 16-位 字的時(shí)候的歷史遺留. 實(shí)際上, 用作 32-位 值的 L 命名也已變得不正確, 但是命名任何東西可能使事情更混淆.
9.4.3. 作為 I/O 內(nèi)存的端口
一些硬件有一個(gè)有趣的特性: 一些版本使用 I/O 端口, 而其他的使用 I/O 內(nèi)存. 輸出給處理器的寄存器在任一種情況中相同, 但是存取方法是不同的. 作為一個(gè)使驅(qū)動(dòng)處理這類硬件的生活容易些的方式, 并且作為一個(gè)使 I/O 端口和內(nèi)存存取的區(qū)別最小化的方法, 2.6 內(nèi)核提供了一個(gè)函數(shù), 稱為 ioport_map:
void *ioport_map(unsigned long port, unsigned int count);
這個(gè)函數(shù)重映射 count I/O 端口和使它們出現(xiàn)為 I/O 內(nèi)存. 從這點(diǎn)以后, 驅(qū)動(dòng)可以在返回的地址上使用 ioread8 和其友并且根本忘記它在使用 I/O 端口.
這個(gè)映射應(yīng)當(dāng)在它不再被使用時(shí)恢復(fù):
void ioport_unmap(void *addr);
這些函數(shù)使 I/O 端口看來(lái)象內(nèi)存. 但是, 注意 I/O 端口必須仍然使用 request_region 在它們以這種方式被重映射前分配.
9.4.4. 重用 short 為 I/O 內(nèi)存
short 例子模塊, 在存取 I/O 端口前介紹的, 也能用來(lái)存取 I/O 內(nèi)存. 為此, 你必須告訴它使用 I/O 內(nèi)存在加載時(shí); 還有, 你需要改變基地址來(lái)使它指向你的 I/O 區(qū).
例如, 這是我們?nèi)绾问褂?short 來(lái)點(diǎn)亮調(diào)試 LED, 在一個(gè) 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 在寫入一個(gè)內(nèi)存位置時(shí)用的循環(huán):
while (count--) {
iowrite8(*ptr++, address);
wmb();
}
注意, 這里使用一個(gè)寫內(nèi)存屏障. 因?yàn)樵诤芏囿w系上 iowrites8 可能轉(zhuǎn)變?yōu)橐粋(gè)直接賦值, 需要內(nèi)存屏障來(lái)保證以希望的順序來(lái)發(fā)生.
short 使用 inb 和 outb 來(lái)顯示它如何完成. 對(duì)于讀者它可能是一個(gè)直接的練習(xí), 但是, 改變 short 來(lái)使用 ioport_map 重映射 I/O 端口, 并且相當(dāng)?shù)睾?jiǎn)化剩下的代碼.
9.4.5. 在 1 MB 之下的 ISA 內(nèi)存
一個(gè)最著名的 I/O 內(nèi)存區(qū)是在個(gè)人計(jì)算機(jī)上的 ISA 范圍. 這是在 640 KB(0xA0000)和 1 MB(0x100000)之間的內(nèi)存范圍. 因此, 它正好出現(xiàn)于常規(guī)內(nèi)存 RAM 中間. 這個(gè)位置可能看起來(lái)有點(diǎn)奇怪; 它是一個(gè)在 1980 年代早期所作的決定的產(chǎn)物, 當(dāng)時(shí) 640 KB 內(nèi)存看來(lái)多于任何人可能用到的大小.
這個(gè)內(nèi)存方法屬于非直接映射的內(nèi)存類別. [
36
]你可以讀/寫幾個(gè)字節(jié)在這個(gè)內(nèi)存范圍, 如同前面解釋的使用 short 模塊, 就是, 通過(guò)在加載時(shí)設(shè)置 use_mem.
盡管 ISA I/O 內(nèi)存只在 x86-類 計(jì)算機(jī)中存在, 我們認(rèn)為值得用幾句話和一個(gè)例子驅(qū)動(dòng).
我們不會(huì)談?wù)?PCI 在本章, 因?yàn)樗亲罡蓛舻囊活?I/O 內(nèi)存: 一旦你知道內(nèi)存地址, 你可簡(jiǎn)單地重映射和存取它. PCI I/O 內(nèi)存的"問(wèn)題"是它不能為本章提供一個(gè)能工作的例子, 因?yàn)槲覀儾荒苁孪戎滥愕?PCI 內(nèi)存映射到的物理地址, 或者是否它是安全的來(lái)存取任一這些范圍. 我們選擇來(lái)描述 ISA 內(nèi)存范圍, 因?yàn)樗坏俑蓛舨⑶腋m合運(yùn)行例子代碼.
為演示存取 ISA 內(nèi)存, 我們還使用另一個(gè) silly 小模塊( 例子源碼的一部分). 實(shí)際上, 這個(gè)稱為 silly, 作為 Simple Tool for Unloading and Printing ISA Data 的縮寫, 或者如此的東東.
模塊補(bǔ)充了 short 的功能, 通過(guò)存取整個(gè) 384-KB 內(nèi)存空間和通過(guò)顯示所有的不同 I/O 功能. 它特有 4 個(gè)設(shè)備節(jié)點(diǎn)來(lái)進(jìn)行同樣的任務(wù), 使用不同的數(shù)據(jù)傳輸函數(shù). silly 設(shè)備作為一個(gè) I/O 內(nèi)存上的窗口, 以類似 /dev/mem 的方式. 你可以讀和寫數(shù)據(jù), 并且lseek 到一個(gè)任意 I/O 內(nèi)存地址.
因?yàn)?silly 提供了對(duì) ISA 內(nèi)存的存取, 它必須開始于從映射物理 ISA 地址到內(nèi)核虛擬地址. 在 Linux 內(nèi)核的早期, 一個(gè)人可以簡(jiǎn)單地安排一個(gè)指針給一個(gè)感興趣的 ISA 地址, 接著直接對(duì)它解引用. 在現(xiàn)代世界, 但是, 我們必須首先使用虛擬內(nèi)存系統(tǒng)和重映射內(nèi)存范圍. 這個(gè)映射使用 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 返回一個(gè)指針值, 它能被用來(lái)使用 ioread8 和其他函數(shù), 在"存取 I/O 內(nèi)存"一節(jié)中解釋.
讓我們回顧我們的例子模塊來(lái)看看這些函數(shù)如何被使用. /dev/sillyb, 特有次編號(hào) 0, 存取 I/O 內(nèi)存使用 ioread8 和 iowrite8. 下列代碼顯示了讀的實(shí)現(xiàn), 它使地址范圍 0xA0000-0xFFFF 作為一個(gè)虛擬文件在范圍 0-0x5FFF. 讀函數(shù)構(gòu)造為一個(gè) switch 語(yǔ)句在不同存取模式上; 這是 sillyb 例子:
case M_8:
while (count) {
*ptr = ioread8(add);
add++;
count--;
ptr++;
}
break;
實(shí)際上, 這不是完全正確. 內(nèi)存范圍是很小和很頻繁的使用, 以至于內(nèi)核在啟動(dòng)時(shí)建立頁(yè)表來(lái)存取這些地址. 但是, 這個(gè)用來(lái)存取它們的虛擬地址不是同一個(gè)物理地址, 并且因此無(wú)論如何需要 ioremap.
下 2 個(gè)設(shè)備是 /dev/sillyw (次編號(hào) 1) 和 /dev/silly1 (次編號(hào) 2). 它們表現(xiàn)象 /dev/sillyb, 除了它們使用 16-位 和 32-位 函數(shù). 這是 sillyl 的寫實(shí)現(xiàn), 又一次部分 switch:
case M_32:
while (count >= 4) {
iowrite8(*(u32 *)ptr, add);
add += 4;
count -= 4;
ptr += 4;
}
break;
最后的設(shè)備是 /dev/sillycp (次編號(hào) 3), 它使用 memcpy_*io 函數(shù)來(lái)進(jìn)行同樣的任務(wù). 這是它的讀實(shí)現(xiàn)的核心:
case M_memcpy:
memcpy_fromio(ptr, add, count);
break;
因?yàn)?ioremap 用來(lái)提供對(duì) ISA 內(nèi)存區(qū)的存取, silly 必須調(diào)用 iounmap 當(dāng)模塊卸載時(shí):
iounmap(io_base);
9.4.6. isa_readb 和其友
看一下內(nèi)核源碼會(huì)展現(xiàn)另一套函數(shù), 有如 isa_readb 的名子. 實(shí)際上, 每個(gè)剛才描述的函數(shù)都有一個(gè) isa_ 對(duì)等體. 這些函數(shù)提供對(duì) ISA 內(nèi)存的存取不需要一個(gè)單獨(dú)的 ioremap 步驟. 但是, 來(lái)自內(nèi)核開發(fā)者的話, 是這些函數(shù)打算用來(lái)作為暫時(shí)的驅(qū)動(dòng)移植輔助, 并且它可能將來(lái)消失. 因此, 你應(yīng)當(dāng)避免使用它們.
9.5. 快速參考
本章介紹下列與硬件管理相關(guān)的符號(hào):
#include
void barrier(void)
這個(gè)"軟件"內(nèi)存屏蔽要求編譯器對(duì)待所有內(nèi)存是跨這個(gè)指令而非易失的.
#include
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
硬件內(nèi)存屏障. 它們請(qǐng)求 CPU(和編譯器)來(lái)檢查所有的跨這個(gè)指令的內(nèi)存讀, 寫, 或都有.
#include
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned doubleword, unsigned port);
用來(lái)讀和寫 I/O 端口的函數(shù). 它們還可以被用戶空間程序調(diào)用, 如果它們有正當(dāng)?shù)臋?quán)限來(lái)存取端口.
unsigned inb_p(unsigned port);
如果在一次 I/O 操作后需要一個(gè)小延時(shí), 你可以使用在前一項(xiàng)中介紹的這些函數(shù)的 6 個(gè)暫停對(duì)應(yīng)部分; 這些暫停函數(shù)有以 _p 結(jié)尾的名子.
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
這些"字串函數(shù)"被優(yōu)化為傳送數(shù)據(jù)從一個(gè)輸入端口到一個(gè)內(nèi)存區(qū), 或者其他的方式. 這些傳送通過(guò)讀或?qū)懙酵欢丝?count 次來(lái)完成.
#include
struct resource *request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
int check_region(unsigned long start, unsigned long len);
I/O 端口的資源分配器. 這個(gè)檢查函數(shù)成功返回 0 并且在錯(cuò)誤時(shí)小于 0.
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);
int check_mem_region(unsigned long start, unsigned long len);
為內(nèi)存區(qū)處理資源分配的函數(shù)
#include
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void *virt_addr);
ioremap 重映射一個(gè)物理地址范圍到處理器的虛擬地址空間, 使它對(duì)內(nèi)核可用. iounmap 釋放映射當(dāng)不再需要它時(shí).
#include
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
用來(lái)使用 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);
I/O 內(nèi)存原語(yǔ)的"重復(fù)"版本.
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
memset_io(address, value, count);
memcpy_fromio(dest, source, nbytes);
memcpy_toio(dest, source, nbytes);
舊的, 類型不安全的存取 I/O 內(nèi)存的函數(shù).
void *ioport_map(unsigned long port, unsigned int count);
void ioport_unmap(void *addr);
一個(gè)想對(duì)待 I/O 端口如同它們是 I/O 內(nèi)存的驅(qū)動(dòng)作者, 可以傳遞它們的端口給 ioport_map. 這個(gè)映射應(yīng)當(dāng)在不需要的時(shí)候恢復(fù)( 使用 ioport_unmap )
本文來(lái)自ChinaUnix博客,如果查看原文請(qǐng)點(diǎn):http://blog.chinaunix.net/u2/78225/showart_1270127.html |
|