9.2. 使用 I/O 端口

2018-02-24 15:50 更新

9.2.?使用 I/O 端口

I/O 端口是驅(qū)動用來和很多設(shè)備通訊的方法, 至少部分時間. 這節(jié)涉及可用的各種函數(shù)來使用 I/O 端口; 我們也觸及一些可移植性問題.

9.2.1.?I/O 端口分配

如同你可能希望的, 你不應(yīng)當(dāng)離開并開始抨擊 I/O 端口而沒有首先確認(rèn)你對這些端口有唯一的權(quán)限. 內(nèi)核提供了一個注冊接口以允許你的驅(qū)動來聲明它需要的端口. 這個接口中的核心的函數(shù)是 request_region:


#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

這個函數(shù)告訴內(nèi)核, 你要使用 n 個端口, 從 first 開始. name 參數(shù)應(yīng)當(dāng)是你的設(shè)備的名子. 如果分配成功返回值是非 NULL. 如果你從 request_region 得到 NULL, 你將無法使用需要的端口.

所有的的端口分配顯示在 /proc/ioports 中. 如果你不能分配一個需要的端口組, 這是地方來看看誰先到那里了.

當(dāng)你用完一組 I/O 端口(在模塊卸載時, 也許), 應(yīng)當(dāng)返回它們給系統(tǒng), 使用:


void release_region(unsigned long start, unsigned long n); 

還有一個函數(shù)以允許你的驅(qū)動來檢查是否一個給定的 I/O 端口組可用:


int check_region(unsigned long first, unsigned long n); 

這里, 如果給定的端口不可用, 返回值是一個負(fù)錯誤碼. 這個函數(shù)是不推薦的, 因為它的返回值不保證是否一個分配會成功; 檢查和后來的分配不是一個原子的操作. 我們列在這里因為幾個驅(qū)動仍然在使用它, 但是你調(diào)用一直使用 request_region, 它進(jìn)行要求的加鎖來保證分配以一個安全的原子的方式完成.

9.2.2.?操作 I/O 端口

在驅(qū)動硬件請求了在它的活動中需要使用的 I/O 端口范圍之后, 它必須讀且/或?qū)懙竭@些端口. 為此, 大部分硬件區(qū)別8-位, 16-位, 和 32-位端口. 常常你無法混合它們, 象你正常使用系統(tǒng)內(nèi)存存取一樣.[33]

一個 C 程序, 因此, 必須調(diào)用不同的函數(shù)來存取不同大小的端口. 如果在前一節(jié)中建議的, 只支持唯一內(nèi)存映射 I/O 寄存器的計算機(jī)體系偽裝端口 I/O , 通過重新映射端口地址到內(nèi)存地址, 并且內(nèi)核向驅(qū)動隱藏了細(xì)節(jié)以便易于移植. Linux 內(nèi)核頭文件(特別地, 體系依賴的頭文件 <asm/io.h>) 定義了下列內(nèi)聯(lián)函數(shù)來存取 I/O 端口:

unsigned inb(unsigned port);void outb(unsigned char byte, unsigned port);
讀或?qū)懽止?jié)端口( 8 位寬 ). port 參數(shù)定義為 unsigned long 在某些平臺以及 unsigned short 在其他的上. inb 的返回類型也是跨體系而不同的.

unsigned inw(unsigned port);void outw(unsigned short word, unsigned port);
這些函數(shù)存取 16-位 端口( 一個字寬 ); 在為 S390 平臺編譯時它們不可用, 它只支持字節(jié) I/O.

unsigned inl(unsigned port);void outl(unsigned longword, unsigned port);
這些函數(shù)存取 32-位 端口. longword 聲明為或者 unsigned long 或者 unsigned int, 根據(jù)平臺. 如同字 I/O, "Long" I/O 在 S390 上不可用.

從現(xiàn)在開始, 當(dāng)我們使用 unsigned 沒有進(jìn)一步類型規(guī)定時, 我們指的是一個體系相關(guān)的定義, 它的確切特性是不相關(guān)的. 函數(shù)幾乎一直是可移植的, 因為編譯器自動轉(zhuǎn)換值在賦值時 -- 它們是 unsigned 有助于阻止編譯時的警告. 這樣的轉(zhuǎn)換不丟失信息, 只要程序員安排明智的值來避免溢出. 我們堅持這個"未完成的類型"傳統(tǒng)貫串本章.

注意, 沒有定義 64-位 端口 I/O 操作. 甚至在 64-位 體系中, 端口地址空間使用一個32-位(最大)的數(shù)據(jù)通路.

9.2.3.?從用戶空間的 I/O 存取

剛剛描述的這些函數(shù)主要打算被設(shè)備驅(qū)動使用, 但它們也可從用戶空間使用, 至少在 PC-類 的計算機(jī). GNU C 庫在 <sys/io.h> 中定義它們. 下列條件應(yīng)當(dāng)應(yīng)用來對于 inb 及其友在用戶空間代碼中使用:

  • 程序必須使用 -O 選項編譯來強(qiáng)制擴(kuò)展內(nèi)聯(lián)函數(shù).

  • ioperm 和 iopl 系統(tǒng)調(diào)用必須用來獲得權(quán)限來進(jìn)行對端口的 I/O 操作. ioperm 為單獨(dú)端口獲取許可, 而 iopl 為整個 I/O 空間獲取許可. 這 2 個函數(shù)都是 x86 特有的.

  • 程序必須作為 root 來調(diào)用 ioperm 或者 iopl.[34] 可選地, 一個它的祖先必須已贏得作為 root 運(yùn)行的端口權(quán)限.

如果主機(jī)平臺沒有 ioperm 和 iopl 系統(tǒng)調(diào)用, 用戶空間仍然可以存取 I/O 端口, 通過使用 /dev/prot 設(shè)備文件. 注意, 但是, 這個文件的含義是非常平臺特定的, 并且對任何東西除了 PC 不可能有用.

例子源碼 misc-progs/inp.c 和 misc-progs/outp.c 是一個從命令行讀寫端口的小工具, 在用戶空間. 它們希望被安裝在多個名子下(例如, inb, inw, 和 inl 并且操作字節(jié), 字, 或者長端口依賴于用戶調(diào)用哪個名子). 它們使用 ioperm 或者 iopl 在 x86下, 在其他平臺是 /dev/port.

程序可以做成 setuid root, 如果你想過危險生活并且在不要求明確的權(quán)限的情況下使用你的硬件. 但是, 請不要在產(chǎn)品系統(tǒng)上以 set-uid 安裝它們; 它們是設(shè)計上的安全漏洞.

9.2.4.?字串操作

除了單發(fā)地輸入和輸出操作, 一些處理器實現(xiàn)了特殊的指令來傳送一系列字節(jié), 字, 或者 長字 到和自一個單個 I/O 端口或者同樣大小. 這是所謂的字串指令, 并且它們完成任務(wù)比一個 C 語言循環(huán)能做的更快. 下列宏定義實現(xiàn)字串處理的概念或者通過使用一個單個機(jī)器指令或者通過執(zhí)行一個緊湊的循環(huán), 如果目標(biāo)處理器沒有進(jìn)行字串 I/O 的指令. 當(dāng)編譯為 S390 平臺時這些宏定義根本不定義. 這應(yīng)當(dāng)不是個移植性問題, 因為這個平臺通常不與其他平臺共享設(shè)備驅(qū)動, 因為它的外設(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ù)讀自或者寫入單個 port 端口.

void insw(unsigned port, void addr, unsigned long count);void outsw(unsigned port, void addr, unsigned long count);
讀或?qū)?16-位 值到一個單個 16-位 端口.

void insl(unsigned port, void addr, unsigned long count);void outsl(unsigned port, void addr, unsigned long count);
讀或?qū)?32-位 值到一個單個 32-位 端口.

有件事要記住, 當(dāng)使用字串函數(shù)時: 它們移動一個整齊的字節(jié)流到或自端口. 當(dāng)端口和主系統(tǒng)有不同的字節(jié)對齊規(guī)則, 結(jié)果可能是令人驚訝的. 使用 inw 讀取一個端口交換這些字節(jié), 如果需要, 來使讀取的值匹配主機(jī)字節(jié)序. 字串函數(shù), 相反, 不進(jìn)行這個交換.

9.2.5.?暫停 I/O

一些平臺 - 最有名的 i386 - 可能有問題當(dāng)處理器試圖太快傳送數(shù)據(jù)到或自總線. 當(dāng)處理器對于外設(shè)總線被過度鎖定時可能引起問題( 想一下 ISA )并且可能當(dāng)設(shè)備單板太慢時表現(xiàn)出來. 解決方法是插入一個小的延時在每個 I/O 指令后面, 如果跟隨著另一個指令. 在 x86 上, 這個暫停是通過進(jìn)行一個 outb 指令到端口 0x80 ( 正常地不是常常用到 )實現(xiàn)的, 或者通過忙等待. 細(xì)節(jié)見你的平臺的 asm 子目錄的 io.h 文件.

如果你的設(shè)備丟失一些數(shù)據(jù), 或者如果你擔(dān)心它可能丟失一些, 你可以使用暫停函數(shù)代替正常的那些. 暫停函數(shù)正如前面列出的, 但是它們的名子以 _p 結(jié)尾; 它們稱為 inb_p, outb_p, 等等. 這些函數(shù)定義給大部分被支持的體系, 盡管它們常常擴(kuò)展為與非暫停 I/O 同樣的代碼, 因為沒有必要額外暫停, 如果體系使用一個合理的現(xiàn)代外設(shè)總線.

9.2.6.?平臺依賴性

I/O 指令, 由于它們的特性, 是高度處理器依賴的. 因為它們使用處理器如何處理移進(jìn)移出的細(xì)節(jié), 是非常難以隱藏系統(tǒng)間的不同. 作為一個結(jié)果, 大部分的關(guān)于端口 I/O 的源碼是平臺依賴的.

你可以看到一個不兼容, 數(shù)據(jù)類型, 通過回看函數(shù)的列表, 這里參數(shù)是不同的類型, 基于平臺間的體系不同點(diǎn). 例如, 一個端口是 unsigned int 在 x86 (這里處理器支持一個 64-KB I/O 空間), 但是在別的平臺是 unsiged long, 這里的端口只是同內(nèi)存一樣的同一個地址空間中的特殊位置.

其他的平臺依賴性來自處理器中的基本的結(jié)構(gòu)性不同, 并且, 因此, 無可避免地. 我們不會進(jìn)入這個依賴性的細(xì)節(jié), 因為我們假定你不會給一個特殊的系統(tǒng)編寫設(shè)備驅(qū)動而沒有理解底層的硬件. 相反, 這是一個內(nèi)核支持的體系的能力的概括:

IA-32 (x86)x86_64
這個體系支持所有的本章描述的函數(shù). 端口號是 unsigned short 類型.

IA-64 (Itanium)
支持所有函數(shù); 端口是 unsigned long(以及內(nèi)存映射的)). 字串函數(shù)用 C 實現(xiàn).

Alpha
支持所有函數(shù), 并且端口是內(nèi)存映射的. 端口 I/O 的實現(xiàn)在不同 Alpha 平臺上是不同的, 根據(jù)它們使用的芯片組. 字串函數(shù)用 C 實現(xiàn)并且定義在 arch/alpha/lib/io.c 中定義. 端口是 unsigned long.

ARM
端口是內(nèi)存映射的, 并且支持所有函數(shù); 字串函數(shù)用 C 實現(xiàn). 端口是 unsigned int 類型.

Cris
這個體系不支持 I/O 端口抽象, 甚至在一個模擬模式; 各種端口操作定義成什么不做.

M68kM68k
端口是內(nèi)存映射的. 支持字串函數(shù), 并且端口類型是 unsigned char.

MIPSMIPS64
MIPS 端口支持所有的函數(shù). 字串操作使用緊湊匯編循環(huán)來實現(xiàn), 因為處理器缺乏機(jī)器級別的字串 I/O. 端口是內(nèi)存映射的; 它們是 unsigned long.

PA
支持所有函數(shù); 端口是 int 在基于 PCI 的系統(tǒng)上以及 unsigned short 在 EISA 系統(tǒng), 除了字串操作, 它們使用 unsigned long 端口號.

PowerPCPowerPC64
支持所有函數(shù); 端口有 unsigned char * 類型在 32-位 系統(tǒng)上并且 unsigned long 在 64-位 系統(tǒng)上.

S390 類似于 M68k, 這個平臺的頭文件只支持字節(jié)寬的端口 I/O, 而沒有字串操作. 端口是 char 指針并且是內(nèi)存映射的.

Super
端口是 unsigned int ( 內(nèi)存映射的 ), 并且支持所有函數(shù).

SPARC SPARC64
再一次, I/O 空間是內(nèi)存映射的. 端口函數(shù)的版本定義來使用 unsigned long 端口.

好奇的讀者能夠從 io.h 文件中獲得更多信息, 這個文件有時定義幾個結(jié)構(gòu)特定的函數(shù), 加上我們在本章中描述的那些. 但是, 警告有些這些文件是相當(dāng)難讀的.

有趣的是注意沒有 x86 家族之外的處理器具備一個不同的地址空間給端口, 盡管幾個被支持的家族配備有 ISA 和/或 PCI 插槽 ( 并且 2 種總線實現(xiàn)分開的 I/O 和地址空間 ).

更多地, 有些處理器(最有名的是早期的 Alphas)缺乏一次移動一個或 2 個字節(jié)的指令.[35] 因此, 它們的外設(shè)芯片組模擬 8-位 和 16-位 I/O 存取, 通過映射它們到內(nèi)存地址空間的特殊的地址范圍. 因此, 操作同一個端口的一個 inb 和 一個 inw 指令, 通過 2 個操作不同地址的 32-位內(nèi)存讀來實現(xiàn). 幸運(yùn)的是, 所有這些都對設(shè)備驅(qū)動編寫者隱藏了, 通過本節(jié)中描述的宏的內(nèi)部, 但是我們覺得它是一個要注意的有趣的特性. 如果你想深入探究, 查找在 include/asm-alpha/core_lca.h 中的例子.

在每個平臺的程序員手冊中充分描述了I/O 操作如何在每個平臺上進(jìn)行; 這些手冊常常在 WEB 上作為 PDF 下載.

[33] 有時 I/O 端口象內(nèi)存一樣安排, 并且你可(例如)綁定 2 個 8-位 寫為一個單個 16-位 操作. 例如, 這應(yīng)用于 PC 視頻板. 但是通常, 你不能指望這個特色.

[34] 技術(shù)上, 它必須有 CAP_SYS_RAWIO 能力, 但是在大部分當(dāng)前系統(tǒng)中這是與作為 root 運(yùn)行是同樣的.

[35] 單字節(jié) I/O 不是一個人可能想象的那么重要, 因為它是一個稀少的操作. 為讀/寫一個單字節(jié)到任何地址空間, 你需要實現(xiàn)一個數(shù)據(jù)通道, 連接寄存器組的數(shù)據(jù)總線的低位到外部數(shù)據(jù)總線的任意字節(jié)位置. 這些數(shù)據(jù)通道需要額外的邏輯門在每個數(shù)據(jù)傳輸?shù)耐ǖ郎? 丟掉字節(jié)寬的載入和存儲能夠使整個系統(tǒng)性能受益.

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號