編寫設備驅動是一個具有挑戰性和冒險性的工作。當設備通過init_mo dule函數登記時,設備的資源應當被分 配。一個主要的設備資源是I/O端口。作為動態連接的驅動程序,開發者應當小心將未被使用的I/O端口分配 給這些設備。 首先驅動程序應偵測這些端口是否被使用或釋放。然後再為設備申請獲取端口。當驅動模塊被 從內核中移出時,端口應該被釋放。這篇文章討論了Linux設備驅動的安全端口分配的復雜性。
介紹
設備驅動開發者一個主要關心的問題是設備的資源分配。這些資源包括I/O端口,內存和中斷。這篇文章試圖解釋I/O子系統的基本原理和資源分配的重要性,主要是I/O端口的資源處理。同時還將闡明如何偵測,申請和釋放設備的端口地址。
基本的硬件元素,如端口,總線和設備控制器,構成了大量的不同的I/O設備。設備驅動向I/O子系統提供了 一個通用的設備存取界面,這非常類似於系統調用(systmem call)在應用程序和操作系統之間提供的標准界 面。現在有很多種類型的設備附屬在電腦上,舉例說來有: 存儲設備,如磁盤,磁帶,光驅和軟驅; 人機交 互設備,如鍵盤,鼠標和屏幕; 傳輸設備,如網卡和調制解調器。不論這些不同設備的數目巨大,我們只需要理解一些基本的概念,即設備如何加載以及軟件如何控制硬件。
基本概念
設備由兩部分組成,一個是被稱設備為控制器的電器部分,另一個是機械部分。控制器通過系統總線加載到 電腦上。典型的方式是,一組互不沖突的寄存器組被賦予到各個控制器。I/O端口包含4組寄存器,即狀態寄 存器,控制寄存器,數據輸入寄存器,數據輸出寄存器。狀態寄存器擁有可以被主機讀取的(狀態)位,用來 指示當前命令是否執行完畢,或者字節是否可以被讀出或寫入,以及任何錯誤提示。控制寄存器則被主機寫操作以啟動一條命令或者改變設備的(工作)模式。數據輸入寄存器用於獲取輸入而數據輸出寄存器則向主機發送結果。
所以,處理器和設備之間的基本界面是控制和狀態寄存器。當處理器執行程序並且遇到與設備相關的指令 時,它通過向相應的設備發送一條命令來執行該指令。控制器執行所要求的動作並設置狀態寄存器的特定位,然後進入等待。處理器有責任檢查設備的狀態直到發現操作完成。例如並口驅動程序(打印機使用的)一般會 輪詢打印機以知道打印機是否准備好。如果打印機沒有准備好,驅動程序會睡眠一段時間(處理器此時會做其他有用的工作),該過程將重復直到打印機准備好。這種輪詢的機制能夠改進系統的性能。另外一種方式則是 系統進行不必要的"死等"(unnecessarily waiting)而不做任何有用的工作。
寄存器擁有在I/O空間明確定義的地址范圍。通常這些地址在啟動時被分配,使用一組在配置文件中定義的參數。各個設備的地址范圍可能被預分配,如果設備是靜態加載的。這意味內核包含了已存在設備的驅動 程序,以分配的I/O端口能被存放在Proc目錄下。你可以在系統使用這些設備時,通過運行“cat /proc/ioports” 命令同步的檢查其所使用的地址范圍。第一列輸出顯示了端口的范圍而第二列則是擁用這些端口的設備。一 些操作系統具備在運行時動態加載設備驅動模塊的特性。所以任何新的設備都能通過動態加載模塊在系統運行時加載到系統中,並且能夠被控制和訪問。
設備驅動的概念是非常抽象的並且處於一台計算上所運行軟件的最低層。由於直接到設備的硬件特性的限 制。每個設備驅動都只管理一種單一類型的設備。這些類型可能是字符型,快設備型或網絡型。如果一個應用程序向設備提出(操作)要求。內核會聯系到對應的設備驅動,設備驅動接著向特定的設備發出命令。設備驅 動是一個函數集合:包含了許多調用入口,類似於open,close,read,write,ioctl,llseek 等。當你插入你的模塊時,init_module ( ) 函數會被調用,而模塊被移出時,cleanup_module ( ) 函數會被調用。設備是在 設備驅動的init_module ( ) 例程中被登記的。
當設備在 init_module ( ) 中登記時,設備的資源如I/O端口,內存和中斷號也在這個函數被分配,這也 是驅動程序能夠正確操作設備的需要。如果你分配了任何錯誤的內存地址,系統會顯示錯誤信息segmentation fault。 而對於I/O端口,系統不會給出任何類似wrong I/O port的信息,但是指派任何現有設備已使用的端 口將會造成系統崩潰。當你移出模塊時,設備應當被注銷,更確切的說,主(設備)號和資源將在cleanup_module ( ) 函 數中被釋放。
設備驅動最頻繁的工作時讀寫IO端口。所以你的驅動應當是確信完美的,被設備使用的端口地址是獨占的。任何其他設備都不會使用這段地址范圍。為了確認這點,首先驅動應當查明這段地址是否在使用,當驅動發現 這段地址未被使用時,可以申請內核為設備分配這段地址。
安全端口分配
現在我們來看看如何通過系統函數來完成資源分配和資源釋放。下面的實例 是在linux 2。4內核上進行實驗的,以下的所有實現僅適用於Linux操作系統和某些擴展的Unix變種。
首先偵測可用的端口(地址)范圍,通過下面的函數:
int check_region (unsigned long start, unsigned long len);
函數返回0表示端口地址可用,返回小於零或負的錯誤編碼( -EBUSY or -EINVAL) 表示已在使用中。函數接受2個參數: start 是 連續區域(或I/O端口范圍)的起始值,而len是區域內的端口數目。
當端口可用時,應該將它分配給設備,通過request_region 函數。
struct resource *request_region (unsigned long start, unsigned long len, char *name);
頭兩個參數和我們前面看到的一樣,字符指針變量name是要分配端口地址的設備名稱。函數返回指向resource結構的指針。Resource結構用來描述資源的范圍,定義於。結構的格式定義如下:
struct resource { const char *name; unsigned long start, end; unsigned long flags; struct resource *parent, *sibiling, *child; };
當模塊從內核移出時,端口應當被釋放以便為其它設備使用,為此我們在 cleanup_module ( )中 使用release_region ( ) 函數。 函數的語法如下:
void release_region ( unsigned long start, unsigned long len);
兩個參數的解釋和前面一致。 以上的3個函數實際上是宏定義,定義於。
設備端口分配的驅動代碼例子
下面的程序說明了動態加載設備的端口分配與回收:
#include #include struct file_operations fops; unsigned long start, len; int init_module (void) { int status; start = 0xff90; len = 0x90; register_chrdev(254,"your_device",&fops); status = check_region (start, len); if (status == 0) { printk ("The ports are available in that range。\n"); request_region(start,len,"your_device"); } else { printk ("The ports are already in use。 Try other range。\n"); return (status); } return 0; } void cleanup_module (void) { release_region(start, len); printk (" ports are freed successfully\n"); unregister_chrdev(254,"your_device");} printk (" your device is unregistered\n"); }
為了避免混淆,例子代碼中去掉了錯誤檢查和和主(設備)號的動態分配。 當端口分配成功時,我們可以在proc目錄中檢查:
$cat /proc/ioports
驅動程序的內核I/O端口函數選擇
Linux支持不同位寬的端口函數,用於I/O端口的讀寫。端口可以是8位,16位或32位。Linux的內 核頭文件定義了訪問I/O端口的內聯(inline)函數,用於讀取(inx)或寫入(outx) 8位,16位以及32位端口。這些函數是:
__u8 inb (unsigned int port); void outb (__u8 data, unsigned int port);
__u16 inw (unsigned int port); void outw(__u16 data, unsigned int port);
__u32 inl (unsigned int prot); void outl (__u32 data, unsigned int port);
這些函數的串版本(string versions)能讓你在單位時間內更有效的傳輸一個以上的數據,通過以下函數:
void insb(unsigned int port, void *addr, unsigned long count); void outsb(unsigned int port, void *addr, unsigned long count);
addr 是被傳入或傳出的內存單元地址,count是被傳輸單元的數量。 Data 則是被讀取或 寫入到"port"端口的數據:
void insw(unsigned int port, void *addr, unsigned long count); void outsw(unsigned int port, void *addr, unsigned long count);
向16位端口讀寫16位數據:
void insl(unsigned int port, void *addr, unsigned long count); void outsl(unsigned int port, void *addr, unsigned long count);
向32位端口讀寫32位數據。