Next Previous Contents

2. 如何在 C 語言下使用 I/O 埠

2.1 正規的方法

用來存取 I/O 埠的常式 (Routine) 都放在檔案 /usr/include/asm/io.h 裡 (或放在核心原始碼程式集的 linux/include/asm-i386/io.h 檔案裡). 這些常式是以單行巨集 (inline macros) 的方式寫成的, 所以使用時只要以 #include <asm/io.h> 的方式引用就夠了; 不需要附加任何函式館 (libraries).

譯注: 常式(Routine) 通常是指系統呼叫(System Call)與函式(Function)的總稱.

因為 gcc (至少出現在 2.7.2.3 和以前的版本) 以及 egcs (所有的版本) 的限制, 你在編譯任何使用到這些常式的原始碼時 必須 打開最佳化選項 (gcc -O1 或較高層次的), 或者是在做 #include <asm/io.h> 這個動作前使用 #define extern 將 extern 定義成空白.

為了除錯的目的, 你編譯時可以使用 gcc -g -O (至少現在的 gcc 版本是這樣), 但是最佳化之後有時可能會讓除錯器 (debugger) 的行為變的有點奇怪. 如果這個狀況對你而言是個困擾, 你可以將所有使用到 I/O 埠的常式集中放在一個檔案裡並只在編譯該檔案時纔打開最佳化選項.

在你存取任何 I/O 埠之前, 你必須讓你的程式有如此做的權限. 要達成這個目的 你可以在你的程式一開始的地方 (但是要在任何 I/O 埠存取動作之前) 呼叫 ioperm() 這個函式 (該函式被宣告於檔案 unistd.h , 並且被定義在 核心中). 使用語法是 ioperm(from, num, turn_on), 其中 from 是第一個允許存取的 I/O 埠位址, num 是接著連續存取 I/O 埠位址的數目. 例如, ioperm(0x300, 5, 1) 的意思就是說允許存取埠 0x300 到 0x304 (一共五個埠位址). 而最後一個參數是一個布林代數值用來指定是否 給予程式存取 I/O 埠的權限 (true (1)) 或是除去存取的權限 (false (0)). 你 可以多次呼叫函式 ioperm() 以便使用多個不連續的埠位址. 至於語法的細節請 參考 ioperm(2) 的使用說明文件.

你的程式必須擁有 root 的權限纔能呼叫函式 ioperm() ; 所以你如果不是以 root 的身份執行該程式, 就是得將該程式 setuid 成 root. 當你呼叫過函式 ioperm() 打開 I/O 埠的存取權限後你便可以拿掉 root 的權限. 在你的程式結束之後並不特別 要求你以 ioperm(..., 0) 這個方式拿掉 I/O 埠的存取權限; 因為當你的程式 執行完畢之後這個動作會自動完成.

呼叫函式 setuid() 將目前執行程式的有效使用者識別碼 (ID) 設定成非 root 的使用者並不影響其先前以 ioperm() 的方式所取得的 I/O 埠存取權限, 但是呼叫函式 fork() 的方式卻會有所影響 (雖然父行程 (parent process) 保有存取權限, 但是子行程 (child process) 卻無法取得存取權限).

函式 ioperm() 只能讓你取得埠位址 0x000 到 0x3ff 的存取權限; 至於 較高位址的埠, 你得使用函式 iopl() (該函式讓你一次可以存取所有的埠位址). 將權限等級參數值設為 3 (例如, iopl(3)) 以便你的程式能夠存取 所有的 I/O 埠 (因此要小心 --- 如果存取到錯誤的埠位址將對你的電腦造成各種不可預期的損害. 同樣地, 呼叫函式 iopl() 你得擁有 root 的權限.至於語法的細節請參考 iopl(2) 的使用說明文件.

接著, 我們來實際地存取 I/O 埠... 要從某個埠位址輸入一個 byte (8 個 bits) 的資料, 你得呼叫函式 inb(port) , 該函式會傳回所取得的一個 byte 的資料. 要輸出一個 byte 的資料, 你得呼叫函式 outb(value, port) (請記住參數的次序). 要從某二個埠位址 xx+1 (二個 byte 組成一個 word, 故使用組合語言 指令 inw) 輸入一個 word (16 個 bits) 的資料, 你得呼叫函式 inw(x) ; 要輸出一個 word 的資料到二個埠位址, 你得呼叫函式 outw(value, x) . 如果你不確定使用那個埠指令 (byte 或 word), 你大概須要 inb()outb() 這二個埠指令 --- 因為大多數的裝置都是採用 byte 大小的埠存取方式來設計的. 注意所有的埠存取指令都至少需要大約一微秒的時間來執行.

如果你使用的是 inb_p(), outb_p(), inw_p(), 以及 outw_p() 等巨集指令, 在你對埠位作址存取動作之後只需很短的(大約一微秒)延遲時間就可以完成; 你也可以讓延遲時間變成大約四微秒方法是在使用 #include <asm/io.h> 之前使用 #define REALLY_SLOW_IO. 這些巨集指令通常 (除非你使用的是 #define SLOW_IO_BY_JUMPING, 這個方法可能較不準確) 會利用輸出資料到埠位址 0x80 以便達到延遲時間的目的, 所以你得先以函式 ioperm() 取得埠位址 0x80 的使用權限 (輸出資料到埠位址 0x80 不應該會對系統的其他其他部分造成影響). 至於 其他通用的延遲時間的方法, 請繼續讀下去.

ioperm(2), iopl(2) 等函式, 和上面所述及的巨集指令的使用說明會收錄在 最近出版的 Linux 使用說明文件集中.

2.2 另一個替代的方法: /dev/port

另一個存取 I/O 埠的方法是以函式 open() 開啟檔案 /dev/port (一個字元裝置,主要裝置編號為 1, 次要裝置編號為 4) 以便執行讀且/或寫的動作 (注意標準輸出入 (stdio) 函式 f*() 有內部的緩衝 (buffering), 所以要避免使用). 接著使用 lseek() 函式以便在該字元裝置檔案中找到某個 byte 資料的正確位置 (檔案位置 0 = 埠位址 0x00, 檔案位置 1 = 埠位址 0x01, 以此類推), 然後你可以使用 read()write() 函式對某個埠位址做讀或寫一個 byte 或 word 資料的動作.

這個替代的方法就是在你的程式裡使用 read/write 函式來存取 /dev/port 字元裝置檔案. 這個方法的執行速度或許比前面所講的一般方法還慢, 但是不需要編譯器 的最佳化功能也不需要使用函式 ioperm() . 如果你允許非 root 使用者或群組存取 /dev/port 字元設裝置案, 操作時就不需擁有 root 權限 -- 但是對於系統安全而言 是個非常糟糕的事情, 因為他可能傷害到你的系統, 或許會有人因而取的 root 的權限, 利用 /dev/port 字元裝置檔案直接存取硬碟, 網路卡, 等設備.


Next Previous Contents