## 9.2.?使用 I/O 端口
I/O 端口是驅動用來和很多設備通訊的方法, 至少部分時間. 這節涉及可用的各種函數來使用 I/O 端口; 我們也觸及一些可移植性問題.
### 9.2.1.?I/O 端口分配
如同你可能希望的, 你不應當離開并開始抨擊 I/O 端口而沒有首先確認你對這些端口有唯一的權限. 內核提供了一個注冊接口以允許你的驅動來聲明它需要的端口. 這個接口中的核心的函數是 request_region:
~~~
#include <linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
~~~
這個函數告訴內核, 你要使用 n 個端口, 從 first 開始. name 參數應當是你的設備的名子. 如果分配成功返回值是非 NULL. 如果你從 request_region 得到 NULL, 你將無法使用需要的端口.
所有的的端口分配顯示在 /proc/ioports 中. 如果你不能分配一個需要的端口組, 這是地方來看看誰先到那里了.
當你用完一組 I/O 端口(在模塊卸載時, 也許), 應當返回它們給系統, 使用:
~~~
void release_region(unsigned long start, unsigned long n);
~~~
還有一個函數以允許你的驅動來檢查是否一個給定的 I/O 端口組可用:
~~~
int check_region(unsigned long first, unsigned long n);
~~~
這里, 如果給定的端口不可用, 返回值是一個負錯誤碼. 這個函數是不推薦的, 因為它的返回值不保證是否一個分配會成功; 檢查和后來的分配不是一個原子的操作. 我們列在這里因為幾個驅動仍然在使用它, 但是你調用一直使用 request_region, 它進行要求的加鎖來保證分配以一個安全的原子的方式完成.
### 9.2.2.?操作 I/O 端口
在驅動硬件請求了在它的活動中需要使用的 I/O 端口范圍之后, 它必須讀且/或寫到這些端口. 為此, 大部分硬件區別8-位, 16-位, 和 32-位端口. 常常你無法混合它們, 象你正常使用系統內存存取一樣.[[33](#)]
一個 C 程序, 因此, 必須調用不同的函數來存取不同大小的端口. 如果在前一節中建議的, 只支持唯一內存映射 I/O 寄存器的計算機體系偽裝端口 I/O , 通過重新映射端口地址到內存地址, 并且內核向驅動隱藏了細節以便易于移植. Linux 內核頭文件(特別地, 體系依賴的頭文件 <asm/io.h>) 定義了下列內聯函數來存取 I/O 端口:
unsigned inb(unsigned port);void outb(unsigned char byte, unsigned port);
讀或寫字節端口( 8 位寬 ). port 參數定義為 unsigned long 在某些平臺以及 unsigned short 在其他的上. inb 的返回類型也是跨體系而不同的.
unsigned inw(unsigned port);void outw(unsigned short word, unsigned port);
這些函數存取 16-位 端口( 一個字寬 ); 在為 S390 平臺編譯時它們不可用, 它只支持字節 I/O.
unsigned inl(unsigned port);void outl(unsigned longword, unsigned port);
這些函數存取 32-位 端口. longword 聲明為或者 unsigned long 或者 unsigned int, 根據平臺. 如同字 I/O, "Long" I/O 在 S390 上不可用.
從現在開始, 當我們使用 unsigned 沒有進一步類型規定時, 我們指的是一個體系相關的定義, 它的確切特性是不相關的. 函數幾乎一直是可移植的, 因為編譯器自動轉換值在賦值時 -- 它們是 unsigned 有助于阻止編譯時的警告. 這樣的轉換不丟失信息, 只要程序員安排明智的值來避免溢出. 我們堅持這個"未完成的類型"傳統貫串本章.
注意, 沒有定義 64-位 端口 I/O 操作. 甚至在 64-位 體系中, 端口地址空間使用一個32-位(最大)的數據通路.
### 9.2.3.?從用戶空間的 I/O 存取
剛剛描述的這些函數主要打算被設備驅動使用, 但它們也可從用戶空間使用, 至少在 PC-類 的計算機. GNU C 庫在 <sys/io.h> 中定義它們. 下列條件應當應用來對于 inb 及其友在用戶空間代碼中使用:
-
程序必須使用 -O 選項編譯來強制擴展內聯函數.
-
ioperm 和 iopl 系統調用必須用來獲得權限來進行對端口的 I/O 操作. ioperm 為單獨端口獲取許可, 而 iopl 為整個 I/O 空間獲取許可. 這 2 個函數都是 x86 特有的.
-
程序必須作為 root 來調用 ioperm 或者 iopl.[[34](#)] 可選地, 一個它的祖先必須已贏得作為 root 運行的端口權限.
如果主機平臺沒有 ioperm 和 iopl 系統調用, 用戶空間仍然可以存取 I/O 端口, 通過使用 /dev/prot 設備文件. 注意, 但是, 這個文件的含義是非常平臺特定的, 并且對任何東西除了 PC 不可能有用.
例子源碼 misc-progs/inp.c 和 misc-progs/outp.c 是一個從命令行讀寫端口的小工具, 在用戶空間. 它們希望被安裝在多個名子下(例如, inb, inw, 和 inl 并且操作字節, 字, 或者長端口依賴于用戶調用哪個名子). 它們使用 ioperm 或者 iopl 在 x86下, 在其他平臺是 /dev/port.
程序可以做成 setuid root, 如果你想過危險生活并且在不要求明確的權限的情況下使用你的硬件. 但是, 請不要在產品系統上以 set-uid 安裝它們; 它們是設計上的安全漏洞.
### 9.2.4.?字串操作
除了單發地輸入和輸出操作, 一些處理器實現了特殊的指令來傳送一系列字節, 字, 或者 長字 到和自一個單個 I/O 端口或者同樣大小. 這是所謂的字串指令, 并且它們完成任務比一個 C 語言循環能做的更快. 下列宏定義實現字串處理的概念或者通過使用一個單個機器指令或者通過執行一個緊湊的循環, 如果目標處理器沒有進行字串 I/O 的指令. 當編譯為 S390 平臺時這些宏定義根本不定義. 這應當不是個移植性問題, 因為這個平臺通常不與其他平臺共享設備驅動, 因為它的外設總線是不同的.
字串函數的原型是:
void insb(unsigned port, void *addr, unsigned long count);void outsb(unsigned port, void *addr, unsigned long count);
讀或寫從內存地址 addr 開始的 count 字節. 數據讀自或者寫入單個 port 端口.
void insw(unsigned port, void *addr, unsigned long count);void outsw(unsigned port, void *addr, unsigned long count);
讀或寫 16-位 值到一個單個 16-位 端口.
void insl(unsigned port, void *addr, unsigned long count);void outsl(unsigned port, void *addr, unsigned long count);
讀或寫 32-位 值到一個單個 32-位 端口.
有件事要記住, 當使用字串函數時: 它們移動一個整齊的字節流到或自端口. 當端口和主系統有不同的字節對齊規則, 結果可能是令人驚訝的. 使用 inw 讀取一個端口交換這些字節, 如果需要, 來使讀取的值匹配主機字節序. 字串函數, 相反, 不進行這個交換.
### 9.2.5.?暫停 I/O
一些平臺 - 最有名的 i386 - 可能有問題當處理器試圖太快傳送數據到或自總線. 當處理器對于外設總線被過度鎖定時可能引起問題( 想一下 ISA )并且可能當設備單板太慢時表現出來. 解決方法是插入一個小的延時在每個 I/O 指令后面, 如果跟隨著另一個指令. 在 x86 上, 這個暫停是通過進行一個 outb 指令到端口 0x80 ( 正常地不是常常用到 )實現的, 或者通過忙等待. 細節見你的平臺的 asm 子目錄的 io.h 文件.
如果你的設備丟失一些數據, 或者如果你擔心它可能丟失一些, 你可以使用暫停函數代替正常的那些. 暫停函數正如前面列出的, 但是它們的名子以 _p 結尾; 它們稱為 inb_p, outb_p, 等等. 這些函數定義給大部分被支持的體系, 盡管它們常常擴展為與非暫停 I/O 同樣的代碼, 因為沒有必要額外暫停, 如果體系使用一個合理的現代外設總線.
### 9.2.6.?平臺依賴性
I/O 指令, 由于它們的特性, 是高度處理器依賴的. 因為它們使用處理器如何處理移進移出的細節, 是非常難以隱藏系統間的不同. 作為一個結果, 大部分的關于端口 I/O 的源碼是平臺依賴的.
你可以看到一個不兼容, 數據類型, 通過回看函數的列表, 這里參數是不同的類型, 基于平臺間的體系不同點. 例如, 一個端口是 unsigned int 在 x86 (這里處理器支持一個 64-KB I/O 空間), 但是在別的平臺是 unsiged long, 這里的端口只是同內存一樣的同一個地址空間中的特殊位置.
其他的平臺依賴性來自處理器中的基本的結構性不同, 并且, 因此, 無可避免地. 我們不會進入這個依賴性的細節, 因為我們假定你不會給一個特殊的系統編寫設備驅動而沒有理解底層的硬件. 相反, 這是一個內核支持的體系的能力的概括:
IA-32 (x86)x86_64
這個體系支持所有的本章描述的函數. 端口號是 unsigned short 類型.
IA-64 (Itanium)
支持所有函數; 端口是 unsigned long(以及內存映射的)). 字串函數用 C 實現.
Alpha
支持所有函數, 并且端口是內存映射的. 端口 I/O 的實現在不同 Alpha 平臺上是不同的, 根據它們使用的芯片組. 字串函數用 C 實現并且定義在 arch/alpha/lib/io.c 中定義. 端口是 unsigned long.
ARM
端口是內存映射的, 并且支持所有函數; 字串函數用 C 實現. 端口是 unsigned int 類型.
Cris
這個體系不支持 I/O 端口抽象, 甚至在一個模擬模式; 各種端口操作定義成什么不做.
M68kM68k
端口是內存映射的. 支持字串函數, 并且端口類型是 unsigned char.
MIPSMIPS64
MIPS 端口支持所有的函數. 字串操作使用緊湊匯編循環來實現, 因為處理器缺乏機器級別的字串 I/O. 端口是內存映射的; 它們是 unsigned long.
PA
支持所有函數; 端口是 int 在基于 PCI 的系統上以及 unsigned short 在 EISA 系統, 除了字串操作, 它們使用 unsigned long 端口號.
PowerPCPowerPC64
支持所有函數; 端口有 unsigned char * 類型在 32-位 系統上并且 unsigned long 在 64-位 系統上.
S390 類似于 M68k, 這個平臺的頭文件只支持字節寬的端口 I/O, 而沒有字串操作. 端口是 char 指針并且是內存映射的.
Super
端口是 unsigned int ( 內存映射的 ), 并且支持所有函數.
SPARC SPARC64
再一次, I/O 空間是內存映射的. 端口函數的版本定義來使用 unsigned long 端口.
好奇的讀者能夠從 io.h 文件中獲得更多信息, 這個文件有時定義幾個結構特定的函數, 加上我們在本章中描述的那些. 但是, 警告有些這些文件是相當難讀的.
有趣的是注意沒有 x86 家族之外的處理器具備一個不同的地址空間給端口, 盡管幾個被支持的家族配備有 ISA 和/或 PCI 插槽 ( 并且 2 種總線實現分開的 I/O 和地址空間 ).
更多地, 有些處理器(最有名的是早期的 Alphas)缺乏一次移動一個或 2 個字節的指令.[[35](#)] 因此, 它們的外設芯片組模擬 8-位 和 16-位 I/O 存取, 通過映射它們到內存地址空間的特殊的地址范圍. 因此, 操作同一個端口的一個 inb 和 一個 inw 指令, 通過 2 個操作不同地址的 32-位內存讀來實現. 幸運的是, 所有這些都對設備驅動編寫者隱藏了, 通過本節中描述的宏的內部, 但是我們覺得它是一個要注意的有趣的特性. 如果你想深入探究, 查找在 include/asm-alpha/core_lca.h 中的例子.
在每個平臺的程序員手冊中充分描述了I/O 操作如何在每個平臺上進行; 這些手冊常常在 WEB 上作為 PDF 下載.
[[33](#)] 有時 I/O 端口象內存一樣安排, 并且你可(例如)綁定 2 個 8-位 寫為一個單個 16-位 操作. 例如, 這應用于 PC 視頻板. 但是通常, 你不能指望這個特色.
[[34](#)] 技術上, 它必須有 CAP_SYS_RAWIO 能力, 但是在大部分當前系統中這是與作為 root 運行是同樣的.
[[35](#)] 單字節 I/O 不是一個人可能想象的那么重要, 因為它是一個稀少的操作. 為讀/寫一個單字節到任何地址空間, 你需要實現一個數據通道, 連接寄存器組的數據總線的低位到外部數據總線的任意字節位置. 這些數據通道需要額外的邏輯門在每個數據傳輸的通道上. 丟掉字節寬的載入和存儲能夠使整個系統性能受益.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考