第22章Linux I/O連線埠編程
本章介紹有關Linux I/O連線埠編程的內容,如在C語言下使用I / O連線埠、硬體中斷與I M A存取
等方面的內容。
22.1 如何在C 語言下使用I/O連線埠
22.1.1 一般的方法
用來存取I/O 連線埠的子過程都放在檔案/usr/include/asm/io.h 里(或放在核心原始碼程式的
linux/include/asm-i386/io.h 檔案里)。這些子過程是以嵌入宏的方式寫成的,所以使用時只要以
#include 的方式引用就夠了,不需要附加任何函式館。
因為g c c以及e g c s的限制,你在編譯任何使用到這些子過程的原始碼時必須打開最最佳化選
項(gcc -O1或較高層次的),或者在做#include 這個動作前使用#define extern 將
extern 定義成空白。
為了除錯的目的,你編譯時可以使用gcc -g -O (至少現在的gcc 版本是這樣),但是最最佳化
之後有時可能會讓調試器的行為變得有點奇怪。如果這個狀況對你而言是個困擾,你可以將所
有使用到I / O連線埠的子過程集中放在一個檔案里,並只在編譯該檔案時打開最最佳化選項。
在你存取任何I/O 連線埠之前,你必須讓程式有如此做的許可權。要完成這個目的,你可以在
程式一開始的地方(但是,要在任何I/O 連線埠存取動作之前) 調用i o p e r m ( )這個函式(該函式在
檔案u n i s t d . h中,並且被定義在核心中)。使用語法是i o p e r m ( f r o m,n u m,t u r n o n ),其中
from 是第一個允許存取的I/O 連線埠地址,n u m是接著連續存取I/O 連線埠地址的數目。例如,
i o p e r m ( 0 x 3 0 0,5,1 )的意思就是說允許存取連線埠0x300 到0x304 ( 一共五個連線埠地址)。
而最後一個參數是一個布爾代數值,用來指定是否給予程式存取I/O 連線埠的許可權(true (1))
或者除去存取的許可權(false (0))。你可以多次調用函式i o p e r m ( )以便使用多個不連續的連線埠地
址。
你的程式必須擁有root 許可權才能調用函式ioperm() ; 所以你如果不是以r o o t身份執行該程
序,就得將該程式設定成r o o t。當你調用過函式ioperm() 打開I/O 連線埠的存取許可權後你便可以
拿掉root 的許可權。在你的程式結束之後並不特別要求你以i o p e r m ( . . .,0) 這個方式拿掉I/O 端
口的存取許可權; 因為當你的程式執行完畢之後,這個動作會自動完成。
調用函式setuid() 將目前執行程式的有效用戶識別碼(ID) 設定成非r o o t的用戶,並不影響
其先前以ioperm() 的方式所取得的I/O 連線埠存取許可權,但是調用函式fork() 的方式卻會有所影
響(雖然父進程保有存取許可權,但是子進程卻無法取得存取許可權)。
函式ioperm() 只能讓你取得連線埠地址0x000 到0 x 3 ff 的存取許可權; 至於較高地址的連線埠,
你得使用函式iopl() (該函式讓你一次可以存取所有的連線埠地址)。將許可權等級參數值設為3 (例
如,i o p l ( 3 ) ),以便你的程式能夠存取所有的I/O 連線埠(因此要小心,如果存取到錯誤的連線埠地址
將對你的計算機造成各種不可預期的損害。同樣地,調用函式iopl() 你得擁有root 的許可權。
接著,我們來實際地存取I/O 連線埠。要從某個連線埠地址輸入一個位元組( 8位)的信息,你得調
用函式inb(port) ,該函式會傳回所取得的一個位元組的信息。要輸出一個位元組的信息,你得調
用函式o u t b ( v a l u e,port) (請記住參數的次序)。要從某兩個連線埠地址x 和x+1 (兩個位元組組成
一個字,故使用組合語言指令i n w )輸入一個字(16 個bit) 的信息,你得調用函式inw(x); 要輸
出一個字的信息到兩個連線埠地址,你得調用函式o u t w ( v a l u e,x )。如果你不確定使用哪個連線埠
指令(位元組或字),你大概須要inb() 與outb() 這兩個連線埠指令,因為大多數的設備都是採用字
節大小的連線埠存取方式來設計的。注意所有的連線埠存取指令都至少需要大約1 μ s 的時間。
如果你使用的是i n b p ( )、o u t b p ( )、i n w p ( )以及outw_p() 等宏指令,在你對連線埠地址存
取動作之後只需很短的(大約為1 μ s ) 延遲時間就可以完成;你也可以讓延遲時間變成大約4 μ s ,方
法是在使用#include 之前使用#define REALLY S L O W I O。這些宏指令通常(除非
你使用的是#define SLOW_IO_BY_JUMPING,這個方法可能不太準確) 會利用輸出信息到端
口地址0 x 8 0以便達到延遲時間的目的,所以你得先以函式ioperm() 取得連線埠地址0x80 的使用
許可權(輸出信息到連線埠地址0x80 不應該會對系統的其他部分造成影響)。至於其他通用的延遲
時間的方法,請參考下面的內容。
22.1.2 另一個替代方法: /dev/port
另一個存取I/O 連線埠的方法是以函式open() 打開檔案/ d e v / p o r t (一個字元設備,主設備編
號為1,次設備編號為4 ), 以便執行讀與( /或)寫的動作(注意標準輸出入函式f*() 有內部的緩
沖,所以要避免使用)。接著使用lseek() 函式以便在該字元設備檔案中找到某個位元組信息的正
確位置(檔案位置0 = 連線埠地址0 x 0 0,檔案位置1 = 連線埠地址0 x 0 1,以此類推),然後你可以使
用read() 或write() 函式對某個連線埠地址做讀或寫一個位元組的動作。
這個方法就是在你的程式里使用read/write 函式來存取/ d e v / p o r t字元設備檔案。這個方法
的執行速度或許比前面所講的一般方法還慢,但是不需要編譯器的最最佳化函式,也不需要使用
函式ioperm() 。如果你允許非root 用戶或群組存取/dev/port 字元設備,操作時就不需擁有
root 許可權。但是,對於系統安全而言,這樣做非常糟糕,因為它可能傷害到你的系統,或許,
會有人因此而取得root 的許可權,利用/dev/port 字元設備檔案直接在硬碟、網路卡等設備上進
行存取操作。
22.2 硬體中斷與DMA 存取
你的程式如果在用戶模式下執行,不可以直接使用硬體中斷(IRQ) 或D M A。你必須編寫
一個核心驅動程式。也就是說,你在用戶模式中所寫的程式無法控制硬體中斷的產生。
22.3 高精確的時間
22.3.1 延遲時間
在用戶模式中執行的進程不能精確地控制時間,因為Linux 是個多用戶的操作環境,在執
行中的進程隨時會因為各種原因被暫停大約1 0 m s到數秒(在系統負荷非常高的時候)。然而,
對於大多數使用I/O 連線埠的應用程式而言,這個延遲時間實際上算不了什麼。要縮短延遲時間,
你得使用函式nice 將你在執行中的進程設定成高優先權(請參考n i c e ( 2 )使用說明檔案),或使用
即時調度法(real-time scheduling) (請看下面介紹)。
如果你想獲得比在一般用戶模式中執行的進程還要精確的時間,有一些方法可以讓你在用
戶模式中做到“即時調度”的支持。Linux 2.x 版本的核心中有軟體方式的即時調度支持。
第22章計Linux I/O 連線埠編程計計241
下載
242計計第四篇Linux 系統高級篇程
下載
1. 睡眠: sleep() 與u s l e e p ( )
現在,讓我們開始進行較簡單的時間函式調用。想要延遲數秒的時間,最佳的方法大概
是使用函式sleep() 。想要延遲至少數十毫秒的時間(10 ms 似乎已是最短的延遲時間了),函
數usleep() 應該可以使用。這些函式把CPU 的使用權讓給其他想要執行的進程,所以沒有浪
費掉C P U的時間。
如果讓出CPU 的使用權因而使得時間延遲了大約5 0 m s (這取決於處理器與機器的速度,以
及系統的負荷),那就浪費掉CPU 太多的時間,因為Linux 的調度器(scheduler) (單就x86 結構
而言) 在將控制權發還給你的進程之前通常至少要花費1 0~3 0 m s的時間。因此,短時間的延遲,
使用函式usleep(3) 所得到的延遲結果通常會大於你在參數所指定的值,大約至少有10 ms。
2. nanosleep()
在Linux 2.0.x 一系列的核心發行版本中,有一個新的系統調用, nanosleep() (請參考
nanosleep(2) 的說明檔案),它讓你能夠休息或延遲一個短的時間(數微秒或更多)。
3. 使用I/O 連線埠延遲時間
另一個延遲數微秒的方法是使用I/O 連線埠。就是從連線埠地址0x80 輸入或輸出任何位元組的
信息(請參考前面) 等待的時間應該幾乎只要1 μ s 這要看你的處理器的類型與速度。如果要延遲
數微秒的時間,你可以將這個動作多做幾次。在任何標準的機器上輸出信息到該連線埠地址,
應該不會有不良的後果(而且有些核心的設備驅動程式也在使用它)。{in|out}[bw]_p() 等函式就
是使用這個方法來產生時間延遲的。
實際上,一個使用到連線埠地址範圍為0~0 x 3 ff 的I/O 連線埠指令,幾乎只要1 μ s 的時間,所
以如果你要如此做,例如,直接使用並行連線埠,只要加上幾個inb() 函式從該連線埠地址範圍讀
入位元組的信息即可。
4. 使用組合語言來延遲時間
如果你知道執進程式所在機器的處理器類型與時鐘速度,你可以執行某些組合語言指令以
獲得較短的延遲時間(但是記住,你在執行中的進程隨時會被暫停, 所以,有時延遲的時間會
比實際長)。如下面列表所示,內部處理器的速度決定了所要使用的時鐘周期數;例如,一個
50 MHz 的處理器( 4 8 6 D X - 5 0或4 8 6 D X 2 - 5 0 ),一個時鐘周期要花費1/50 000 000 s (=20ns)。
指令i386 時鐘周期數i486 時鐘周期數
n o p 3 1
XCHG %ax,% a x 3 3
or %ax ,%ax 2 1
mov %ax,% a x 2 1
add %ax,0 2 1
上面的列表中,指令nop 與xchg 應該不會有不良的後果。指令最後可能會改變標誌暫存
器的內容,但是,這沒關係,因為gcc 會處理。指令nop 是個好的選擇。
想要在你的程式中使用這些指令,你得使用a s m ( ' i n s t r u c t i o n ' )。指令的語法就如同上面列
表的用法; 如果你想要在單一的asm() 敘述中使用多個指令,可以使用分號將它們隔開。例如,
asm('nop ; nop ; nop ; nop') 會執行4個nop 指令,在i486 或Pentium 處理器中會延遲4個時鐘周
期( i386 會延遲12 個時鐘周期)。
gcc 會將asm() 翻譯成單行組合語言程式碼,所以不會有調用函式的負荷。在Intel x86 結
構中不可能有比1個時鐘周期還短的時間延遲。
5. 在Pentium 處理器上使用函式r d t s c
對於Pentium 處理器而言,你可以使用下面的C 語言程式計算自從上次重新開機到現在經
第22章計Linux I/O 連線埠編程計計243
下載
過了多少個時鐘周期:
extern __inline__ unsigned long long int rdtsc()
{
unsigned long long int x;
__asm__ volatile ("。位元組0 x 0 f,0x31" : "=A" (x));
return x;
}
你可以查詢並參考此值以便延遲你想要的時鐘周期數。
22.3.2 時間的量測
想要時間精確到1 s, 使用函式time() 或許是最簡單的方法。想要時間更精確,函式
gettimeofday() 大約可以精確到微秒(但是如前所述會受到CPU 調度的影響)。至於Pentium 處
理器,使用上面的程式片斷就可以精確到一個時鐘周期。
如果要執行中的進程在一段時間到了之後能夠被通知(get a signal) ,可以使用函式
setitimer() 或alarm() 。
22.4 使用其他程式語言
上面的說明集中在C 程式語言。它應該可以直接套用在C++ 及Objective C語言之上。至
於組合語言部分,雖然你必須先在C 語言中調用函式ioperm() 或i o p l ( ),但是,隨後可以直接
使用I/O 連線埠讀寫指令。
至於其他程式語言,除非你可以在該程式語言中插入單行組合語言或C語言的程式碼,或
者使用上面所說的系統調用,否則,倒不如編寫一個內含有存取I / O連線埠或延遲時間所必須使
用的函式的C原始程式,編譯之後再與你的程式連線。要不然就使用前面所說的/dev/port 字元
設備檔案。
22.5 一些有用的I/O 連線埠
如果你要按照其原始的設計目的來使用這些或其他常用的I/O 連線埠(例如,控制一般的列印
機或數據機),你應該使用現成的設備驅動程式(它通常含在核心中),而不會如本文所說的去
編寫I/O 連線埠程式。本節主要是提供給那些想要將液晶顯示器( L C D )、步進電動機或其他商業
電子產品連線到P C標準I/O 連線埠的人。
22.5.1 並行連線埠
並行連線埠的基本連線埠地址(以下稱為B A S E )對於/dev/lp0 是0x3bc ,對於/ d e v / l p 1是0x378 ,
對於/dev/lp2 是0x278 。
除了下面即將描述的標準只輸出模式,大多數的並行連線埠都有擴充的雙向模式。因為在用
戶模式中的程式無法使用IRQ 或D M A,想要使用E C P / E P P模式,或許得編寫一個核心的設備
驅動程式。
連線埠地址BASE+0 (信息連線埠) 用來控制信息連線埠的信號電平(D0 到D7 分別代表著bit 0
到7,電平狀態: 0 = 低電平(0 V),1 = 高電平(5 V))。一個寫入信息到該連線埠的動作,會將信
息信號電平拴在連線埠的端腳上。一個將該連線埠的信息讀出的動作會將上一次以標準只輸出模式
或擴充的寫入模式所拴住的信息信號電平讀回,或者以擴充讀出模式從另外一個設備將端腳上
的信息信號電平讀回。
244計計第四篇Linux 系統高級篇程
下載
連線埠地址BASE+1 (狀態連線埠) 是個唯讀入的連線埠,會將下面的輸入信號電平讀回:
bits 0 和1 保留不用。
bit 2 IRQ 的狀態。
bit 3 ERROR (1=高電平)。
bit 4 SLCT (1=高電平)。
bit 5 PE (1=高電平)。
bit 6 ACK (1=高電平)。
bit 7 -BUSY (0=高電平)。
連線埠地址BASE+2 (控制連線埠) 是個只寫入的連線埠(一個將該連線埠的信息讀出的動作僅會將
上一次寫入的信息信號電平讀回),用來控制下面的狀態信號:
bit 0 -strobe (0=高電平)。
bit 1 AUTO_FD_XT (1=高電平)。
bit 2 -INIT (0=高電平)。
bit 3 SLCT_IN (1=高電平)。
bit 4 當被設定為1 時允許並行連線埠產生IRQ 信號(發生在A C K端腳的電平由低變高的瞬
間)。
bit 5 用來控制擴充模式時連線埠的輸出入方向(0 = 寫,1 = 讀),這是個只寫的連線埠(一個將
該連線埠的信息讀出的動作對此bit 一點用處也沒有)。
bits 6和7 保留不用。
連線埠的端腳排列方式(該連線埠是一個25 只腳D 字形外殼的母連線器,i =輸入,o =輸出)如
下:
1 i o S T R O B E,2 i o D 0,3 i o D 1,4 i o D 2,5 i o D 3,6 i o D 4,7 i o D 5,8io D 6,9 i o
D 7,1 0 i A C K,11 i B U S Y,1 2 i P E,1 3 i S L C T,1 4 o A U TO F D X T,1 5 i E R R O R,1 6 o
I N I T,17o S L C T I N,1 8 25 Ground
22.5.2 遊戲連線埠
遊戲連線埠的連線埠地址範圍為0 x 2 0 0 - 0 x 2 0 7。
連線埠的端腳排列方式(該連線埠是一個15 只腳D 字形外殼的母連線器)如下:
1、8、9、15: +5 V (電源)。
4、5、12: 接地。
2、7、1 0、14: BA1、B A 2、B B 1和BB2 等數位輸入。
3、6、11、13: AX、AY、B X和BY 等“類比”輸入。
+5 V 的端腳似乎通常會直接連線到主機板的電源線上,所以它應該提供相當的電力,這
還要看所使用主機板、電源以及遊戲連線埠的類型。
數位輸入用於操縱口的按鈕可以讓你連線兩個操縱口的四個按鈕(操縱口A和操縱口B,各
有兩個按鈕) 到遊戲連線埠也就是數位輸入的四個端腳。它們應該是一般T T L電壓電平的輸入,
你可以直接從狀態連線埠(參考下面說明) 讀出它們的電平狀態。一個實際的操縱口在按鈕被按
下時會傳回低電平(0 V) 狀態,否則傳回高電平( 5 V經由1 k W的電阻連線到電源端腳) 狀態。
所謂的類比輸入實際是量測到的阻抗值。遊戲連線埠有四個單晶體多諧振盪器(一個558 晶
片) 連線到四個類比輸入端腳。每個類比輸入端腳與多諧振盪器的輸出之間連線著一個2.2k W
的電阻,而且多諧振盪器的輸出與地之間連線著一個0.01 μF 的時間電容。一個實際的操縱口
的每個坐標(X 和Y) 上會有一個可變電阻,連線在+5 V與每個相對的類比輸入端腳之間(端腳
AX 或AY 是給操縱口A用的,而端腳BX 或B Y是給操縱口B用的)。
操作的時候,多諧振盪器將其輸出設定為高電平(5 V),並且等到時間電容上的電壓達到
3.3 V 之後將相對的輸出設定為低電平。因此操縱口中多諧振盪器輸出的高電平時間周期與可
變電阻的電阻值成正比(也就是,操縱口在相對坐標的位置),如下所示:
R = (t - 24 .2) / 0.0 11
其中,R是可變電阻的阻值( W ),而t是高電平時間周期的長度( s )。
因此,要讀出類比輸入端腳的數值,首先得啟動多諧振盪器(以連線埠寫入的方式,請看下
面),然後查詢四個坐標的信號狀態(以持續的連線埠讀出方式),一直到信號狀態由高電平變成低
電平,計算其高電平時間周期的長度。這個持續查詢的動作花費相當多的CPU 時間,而且在
一個非即時的多用戶環境,所得的結果不是非常準確的。因為,你無法以固定的時間來查詢信
號的狀態(除非你使用核心層次的驅動程式而且你得在查詢的時候抑制掉中斷的產生,但是這
樣做會浪費更多的CPU 時間)。如果你知道信號的狀態會花費一段不短的時間(數十毫秒)而成
為低電平,可以在查詢之前調用函式usleep() 將C P U的時間讓給其他想要執行的進程。
遊戲連線埠中唯一需要你來存取的連線埠地址是0x201 (其他的連線埠地址不是動作一樣就是沒
用)。任何對這個連線埠地址所做的寫入動作(不論你寫入什麼) 都會啟動多諧振盪器。對這個端
口地址做讀出動作會取回輸入信號的狀態:
bit 0: AX (1=高電平,多諧振盪器的輸出狀態)
bit 1: AY (1=高電平,多諧振盪器的輸出狀態)
bit 2: BX (1=高電平,多諧振盪器的輸出狀態)
bit 3: BY (1=高電平,多諧振盪器的輸出狀態)
bit 4: BA1 (數位輸入,1 =高電平)
bit 5: BA2 (數位輸入,1 =高電平)
bit 6: BB1 (數位輸入,1 =高電平)
bit 7: BB2 (數位輸入,1 =高電平)
22.5.3 串列連線埠
如果你的設備支持R S - 2 3 2之類的接口,你應該可以使用串列連線埠。L i n u x所提供的串列端
口驅動程式應該能夠套用在任何地方(你應該不需要直接編寫串列連線埠程式,或是核心的驅動
程式)。它具有相當的通用性,所以如果使用非標準的速率以及其他等等,應該不是問題。
第22章計Linux I/O 連線埠編程計計245
下載