李蘭蘭
南京航空航天大學金城學院 江蘇省 211156
任何一個計算機系統都是由計算機硬件子系統和計算機軟件子系統組成的,缺一不可,他們共同協作完成各種任務,但同時他們又具有一定的獨立性。硬件工程師往往不必關心軟件,而應用軟件工程師也不會過多關注硬件。應用軟件工程師需要看到一個沒有硬件的純粹的軟件世界,硬件對他們來說是透明的。而設備驅動程序就能達到這個目的,實現硬件對應用軟件工程師的隱形。
Linux系統下,設備可以分成三種基本類型:字符設備、塊設備和網絡設備。對于字符設備而言,往往是以字節流的形式進行訪問的設備,如鼠標、觸摸屏等,其對應的驅動類型是字符設備驅動程序。塊設備可以以任意順序進行訪問,以塊為單位進行操作,如硬盤,其驅動通過塊設備驅動程序來實現。Linux系統中,應用程序可以像操作字符設備一樣的讀寫塊設備,但內核為字符設備驅動和塊設備驅動提供了完全不同的接口。在Linux系統中,網絡設備處理的事務一般面向數據的接收和發送,不像字符設備和塊設備一樣對應于文件系統的節點,其對應的驅動為網絡設備驅動程序。內核和網絡設備驅動程序間的通信,完全不同于和字符及塊設備驅動程序之間的通信,內核為網絡設備驅動程序提供了一套和數據包傳輸相關的函數。
這種分類方法并不是非常嚴格的,對于某些復雜的設備,Linux系統還定義了其他的驅動體系結構。
Linux內核非常龐大,包含的組件也非常多。這些組件成為內核的一部分通常可以通過兩種途徑,一種是編譯時直接成為Linux內核的組件,另一種是編譯時不進入內核,但可以在需要時通過加載的方式成為內核的一部分,當不再需要該模塊時,通過卸載的方式從內核中移除。Linux中編寫的設備驅動程序就可以通過后面這種模塊化的方式進入到內核中。這種模塊化驅動程序編程方式是Linux系統的一個很好的特色,有助于縮短模塊的開發周期,不需要每次都經過冗長的關機/重啟過程。
加載模塊時可以使用insmod或modprobe命令將模塊加載到正在運行的內核中,通過rmmod程序把模塊從內核中移除。當通過insmod或modprobe命令加載驅動模塊時,模塊的加載函數會自動被內核執行,完成模塊的初始化工作,對字符設備驅動程序來說,一般會在加載函數完成字符設備的注冊。當通過rmmod命令卸載驅動模塊時,內核會自動運行模塊的卸載函數,對字符設備驅動程序來說,一般在卸載函數對字符設備進行注銷。
模塊的加載函數以“module_init(函數名)”的形式被指定,若初始化成功,應返回 0,而在初始化失敗時,應返回一個負值。模塊的卸載函數則以“module_exit(函數名)”形式指定,但卸載函數不返回任何值。
Linux內核中使用cdev結構體描述字符設備,其中cdev結構體的dev_t成員定義了32位的設備號,其中高12位用來表示主設備號,低 20位為次設備號。可以通過使用宏MAJOR(dev_t dev)和 MINOR(dev_t dev)來分別獲取主設備號和次設備號,而宏MKDEV(int major, int minor)則通過主設備號和次設備號來生成dev_t。通常,用主設備號來標識設備對應的驅動程序。
驅動程序在建立一個字符設備之前,首先要做的事情就是獲得一個或者多個設備號。該過程可以通過以下函數實現。

register_chrdev_region()函數用于已知設備的設備號的情況;而alloc_chrdev_region()用于設備號未知,內核為設備動態分配所需要的設備號,函數調用成功時,得到的設備號會放入第一個參數dev中。動態分配設備號的函數有個很大的優點,即能避免設備號沖突的發生。
在模塊卸載函數中,在對字符設備注銷后,調用unregister_chrdev_region()函數釋放開始分配的設備號。該函數原型為:

在申請完設備號之后,需要對字符設備進行注冊,而字符設備是用結構體cdev來表示的,cdev結構體的定義如下:

在對字符設備進行注冊時,先為字符設備分配 cdev結構,一般調用如下函數:

cdev_alloc函數會向系統動態申請一個cdev結構,接下來把該cdev結構嵌入到自己的設備特定結構中去,初始化已分配到的結構:

接下來對cdev結構體中的某些字段進行初始化:

在設置好cdev結構體后,通過下面的函數調用把該結構告訴內核:

參數dev為已設置好的cdev結構體,num為該設備對應的第一個設備號,count應該為和該設備關聯的設備號的數量,count的值常常會被設置成1。 cdev_add函數如果返回一個負的錯誤碼,則設備不會被添加到系統中去,只有當成功返回時,該字符設備才真正被添加到內核中去,它的操作也才能被內核調用。
當需要從系統中刪除一個字符設備,則需調用cdev_del函數,該函數原型如下:

一旦調用了該函數,會刪除cdev結構,注銷字符設備,此時就不能再訪問cdev結構。
file_operations結構體中的成員函數是字符設備驅動程序設計的主體內容,這些函數會在應用程序進行 open()、write()、read()、close()等系統調用時最終被調用。File_operations結構體比較龐大,其中比較主要的成員如下:

該成員指向擁有該結構體的模塊的指針,該字段可以避免內核正在操作該模塊時卸載該模塊。大多情形下,該成員都會被初始化為THIS_MODULE。

用來從設備讀取數據。函數返回非負值表示成功讀取的字節數,出錯時返回一個負值。

用來向設備發送數據。函數返回的非負值表示成功寫入的字節數,

用來修改文件當前讀寫位置,并將新位置作為返回值返回,在出錯時,該函數返回一個負值。

用來提供一種執行設備特定命令的方法。當調用成功時,返回給調用程序一個非負值。內核本身常常會提供一些操作設備的命令,而不需要調用設備驅動中的ioctl函數。如果設備不提供ioctl函數,則對于任何內核未預先定義的請求,ioctl系統調用將返回一個負的錯誤碼。
字符設備驅動中需要對file_operations結構進行初始化,如下:

其中read和write函數分別進行拷貝數據到應用程序空間和從應用程序空間拷貝數據的操作,ssize_t (*read) (struct file *filp, char __user*buff, size_t count, loff_t *offp);

對于這兩個函數,參數filp是文件指針;參數count是請求傳輸的數據長度;參數buff指向用戶空間緩沖區,這個緩沖區或者保存要寫入的數據,或者是一個存放新讀入數據的空緩沖區;參數offp指明用戶在文件中進行存取操作的位置。對于表示用戶空間的指針的buff參數,內核代碼不能直接引用其中的內容,而驅動程序又必須訪問用戶空間的緩沖區以便完成自己的工作,這種訪問應始終通過內核提供的專用函數完成。Read函數的任務是從設備拷貝數據到用戶空間,通過使用copy_to_user函數來實現;write函數的任務則是從用戶空間拷貝數據到設備上,這可通過使用copy_from_user函數來實現。實現內核空間和用戶空間之間拷貝數據的這兩個函數的原型是:


對編寫好的字符設備驅動程序進行編譯,將會得到mydev.ko文件,通過命令”insmod mydev.ko”加載驅動模塊,運行”lsmod”命令,發現 mydev模塊已被加載。當執行”cat/proc/devices”命令,發現多出了主設備號為***的“mydev“字符設備驅動。
接下來通過命令“mknod /dev/mydev c *** 0“創建”/dev/mydev”設備節點,最后進行驗證。通過命令“echo “Is the driver OK” > /dev/mydev”和 命令“cat /dev/mydev”分別驗證設備的寫和讀,結果證明字符“Is the driver OK”被成功寫入到mydev字符設備中去。
本文介紹的字符設備驅動程序的設計是針對Linux內核2.6而言的,相比較塊設備和網絡設備驅動程序而言,字符設備驅動程序還是相對較簡單的一類驅動程序。對于實際的物理設備,當把該設備注冊為字符設備時,除了實現字符設備驅動的部分,往往還需根據設備本身的特點,實現設備功能相關的代碼。
[1]Cobet,Rubini,Kroah-Hartman. Linux設備驅動程序.第三版.魏永明等譯.北京:中國電力出版社.2005.
[2]宋寶華編著.Linux設備驅動開發詳解.北京:人民郵電出版社.2008.
[3] NeilMatthew RichardStones著.Linux程序設計[M].楊曉云,楊濤譯等譯.北京:機械工業出版社.2002.
[4]Mait Weish,Matthias Kalle Dalheimer,Lar Kaufman著.Linux權威指南(第3版)[M].北京:中國電力出版社.2000.