第 1 章 Linux驅動開發概述
設備驅動程序是計算機硬件與應用程序的接口,是軟件系統與硬件系統溝通的橋梁。如果沒有設備驅動程序,那麼硬件設備就只是一堆廢鐵,沒有什麼功能。本章將對Linux驅動開發進行簡要的概述,使讀者理解一些常見的概念。
1.1 Linux設備驅動的基本概念
剛剛接觸Linux設備驅動的朋友,會對Linux設備驅動中的一些基本概念不太理解。這種不理解,會導致繼續學習的困難,所以本節集中講解一些Linux設備驅動的基本概念,為進一步學習打下良好的基礎。
1.1.1 設備驅動程序概述
設備驅動程序(Device Driver),簡稱驅動程序(Driver)。它是一個允許計算機軟件(Computer Software)與硬件(Hardware)交互的程序。這種程序建立了一個硬件與硬件,或硬件與軟件溝通的界面。CPU經由主板上的總線(Bus)或其他溝通子系統(Subsystem)與硬件形成連接,這樣的連接使得硬件設備(Device)之間的數據交換成為可能。
依據不同的計算機架構與操作系統平台差異,驅動程序可以是8位、16位、32位,64位。不同平台的操作系統需要不同的驅動程序。例如32位的Windows系統需要32位的驅動程序,64位的Windows系統,需要64位的驅動程序;在Windows 3.11的16位操作系統時代,大部分的驅動程序都是16位;到了32位的Windows XP,則大部分是使用32位驅動程序;至於64位的Linux或是Windows 7平台上,就必須使用64位驅動程序。
1.1.2 設備驅動程序的作用
設備驅動程序是一種可以使計算機與設備進行通信的特殊程序,可以說相當於硬件的接口。操作系統只有通過這個接口,才能控制硬件設備的工作。假如某設備的驅動程序未能正確安裝,便不能正常工作。正因為這個原因,驅動程序在系統中所占的地位十分重要。一般,當操作系統安裝完畢後,首要的便是安裝硬件設備的驅動程序。並且,當設備驅動程序有更新的時候,新的驅動程序比舊的驅動程序有更好的性能。這是因為新的驅動程序對內存、IO等進行了優化,使硬件能夠到達更好的性能。
但是,大多數情況下,並不需要安裝所有硬件設備的驅動程序。例如硬盤、顯示器、光驅、鍵盤和鼠標等就不需要安裝驅動程序,而顯卡、聲卡、掃描儀、攝像頭和Modem等就需要安裝驅動程序。不需要安裝驅動程序並不代表這些硬件不需要驅動程序,而是這些設備所需驅動已經內置在操作系統中。另外,不同版本的操作系統對硬件設備的支持也是不同的,一般情況下,版本越高所支持的硬件設備也越多。
設備驅動程序用來將硬件本身的功能告訴操作系統(通過提供接口的方式),完成硬件設備電子信號與操作系統及軟件的高級編程語言之間的互相翻譯。當操作系統需要使用某個硬件時,例如讓聲卡播放音樂,它會先發送相應指令到聲卡的某個I/O端口。聲卡驅動程序從該I/O端口接收到數據後,馬上將其翻譯成聲卡才能聽懂的電子信號命令,從而讓聲卡播放音樂。所以簡單地說,驅動程序是提供硬件到操作系統的一個接口,並且協調二者之間的關系。而因為驅動程序有如此重要的作用,所以人們都稱“驅動程序是硬件的靈魂”、“硬件的主宰”,同時驅動程序也被形象地稱為“硬件和系統之間的橋梁”。
1.1.3 設備驅動的分類
計算機系統的主要硬件由CPU、存儲器和外部設備組成。驅動程序的對象一般是存儲器和外部設備。隨著芯片制造工藝的提高,為了節約成本,通常將很多原屬於外部設備的控制器嵌入到CPU內部。例如Intel的酷睿i5 3450處理器,就內置有GPU單元,配合“需要搭配內建GPU的處理器”的主板,就能夠起到顯卡的作用。相比獨立顯卡,性價比上有很大的優勢。所以現在的驅動程序應該支持CPU中的嵌入控制器。Linux將這些設備分為3大類,分別是字符設備、塊設備和網絡設備。
1.
字符設備字符設備是指那些能一個字節一個字節讀取數據的設備,如LED燈、鍵盤和鼠標等。字符設備一般需要在驅動層實現open()、close()、read()、write()、ioctl()等函數。這些函數最終將被文件系統中的相關函數調用。內核為字符設備對應一個文件,如字符設備文件/dev/console。對字符設備的操作可以通過字符設備文件/dev/console來進行。這些字符設備文件與普通文件沒有太大的差別,差別之處是字符設備一般不支持尋址,但特殊情況下,有很多字符設備也是支持尋址的。尋址的意思是,對硬件中一塊寄存器進行隨機地訪問。不支持尋址就是只能對硬件中的寄存器進行順序的讀取,讀取數據後,由驅動程序自己分析需要哪一部分數據。
2.
塊設備塊設備與字符設備類似,一般是像磁盤一樣的設備。在塊設備中還可以容納文件系統,並存貯大量的信息,如U盤、SD卡。在Linux系統中,進行塊設備讀寫時,每次只能傳輸一個或者多個塊。Linux可以讓應用程序像訪問字符設備一樣訪問塊設備,一次只讀取一個字節。所以塊設備從本質上更像一個字符設備的擴展,塊設備能完成更多的工作,例如傳輸一塊數據。
綜合來說,塊設備比字符設備要求更復雜的數據結構來描述,其內部實現也是不一樣的。所以,在Linux內核中,與字符驅動程序相比,塊設備驅動程序具有完全不同的API接口。
3.
網絡設備計算機連接到互聯網上需要一個網絡設備,網絡設備主要負責主機之間的數據交換。與字符設備和塊設備完全不同,網絡設備主要是面向數據包的接收和發送而設計的。網絡設備在Linux操作系統中是一種非常特殊的設備,其沒有實現類似塊設備和字符設備的read()、write()和ioctl()等函數。網絡設備實現了一種套接字接口,任何網絡數據傳輸都可以通過套接字來完成。
1.2 Linux操作系統與驅動的關系
Linux操作系統與設備驅動之間的關系如圖1.1所示。用戶空間包括應用程序和系統調用兩層。應用程序一般依賴於函數庫,而函數庫是由系統調用來編寫的,所以應用程序間接地依賴於系統調用。
圖 1.1 設備驅動程序與操作系統的關系系統調用層是內核空間和用戶空間的接口層,就是操作系統提供給應用程序最底層的API。通過這個系統調用層,應用程序不需要直接訪問內核空間的程序,增加了內核的安全性。同時,應用程序也不能訪問硬件設備,只能通過系統調用層來訪問硬件設備。如果應用程序需要訪問硬件設備,那麼應用程序先訪問系統調用層,由系統調用層去訪問內核層的設備驅動程序。這樣的設計,保證了各個模塊的功能獨立性,也保證了系統的安全。
系統調用層依賴內核空間的各個模塊來實現。在Linux內核中,包含很多實現具體功能的模塊。這些模塊包括文件系統、網絡協議棧、設備驅動、內核調度、內存管理、進程管理等,都屬於系統內核空間,也就是這些模塊是在內核空間實現的。
最底層是硬件層,這一層是實際硬件設備的抽象。設備驅動程序的功能就是驅動這一層硬件。設備驅動程序可以工作在有操作系統的情況下,也可以工作在沒有操作系統的情況下。如果只需要實現一些簡單的控制設備操作,那麼可以不使用操作系統。如果嵌入式系統完成的功能比較復雜,則往往需要操作系統來幫忙。例如,單片機程序,就不需要操作系統,因為其功能簡單,內存、處理器能力弱,不能也沒有必要為其開發操作系統。
1.3 Linux驅動程序開發
Linux驅動程序的開發與應用程序的開發有很大的差別。這些差別導致了編寫Linux設備驅動程序與編寫應用程序有本質的區別,所以對於應用程序的設計技巧很難直接應用在驅動程序的開發上。最經典的例子是應用程序如果錯誤可以通過try catch等方式,避免程序的崩潰,驅動程序則沒有這麼好的處理方式。本節將對Linux驅動程序的開發進行簡要的講解。
1.3.1 用戶態和內核態
Linux操作系統分為用戶態和內核態。用戶態處理上層的軟件工作。內核態用來管理用戶態的程序,完成用戶態請求的工作。驅動程序與底層的硬件交互,所以工作在內核態。
簡單來說,內核態大部分時間在完成與硬件的交互,比如讀取內存,將硬盤上的數據讀取到內存中,調度內存中的程序到處理器中運行等。相對於內核態,用戶態則自由得多,其實,用戶態中的用戶可以狹隘的理解為應用程序開發者,他們很少與硬件直接打交道,他們經常的工作是編寫Java虛擬機下的應用程序,或者.NET框架下的應用程序。即使他們的編程水平很初級,經常出現程序異常,那麼充其量是將Java虛擬機搞崩潰,想把操作系統搞崩潰還是很難的。這一切都歸功於內核態對操作系統有很強大的保護能力。
另一方面,Linux操作系統分為兩個狀態的原因主要是,為應用程序提供一個統一的計算機硬件抽象。工作在用戶態的應用程序完全可以不考慮底層的硬件操作,這些操作由內核態程序來完成。這些內核態程序大部分是設備驅動程序。一個好的操作系統的驅動程序對用戶態應用程序應該是透明的,也就是說,應用程序可以在不了解硬件工作原理的情況下,很好地操作硬件設備,同時不會使硬件設備進入非法狀態。Linux操作系統很好的做到了這一點。在Linux編程中,程序員經常使用open()方法來讀取磁盤中的數據,在調用這個方法的時候,並不需要關心磁盤控制器是怎麼讀取數據,並將其傳到內存中的。這些工作都是驅動程序完成的,這就是驅動程序的透明性。
一個值得注意的問題是,工作在用戶態的應用程序不能因為一些錯誤而破壞內核態的程序。現代處理器已經充分考慮了這個問題。處理器提供了一些指令,分為特權指令和普通指令。特權指令只有在內核態下才能使用;普通指令既可以在內核態使用,也可以在用戶態使用。通過這種限制,用戶態程序就不能執行只有在內核態才能執行的程序了,從而起到保護的作用。
另一個值得注意的問題是,用戶態和內核態是可以互相轉換的。每當應用程序執行系統調用或者被硬件中斷掛起時,Linux操作系統都會從用戶態切換到內核態。當系統調用完成或者中斷處理完成後,操作系統會從內核態返回用戶態,繼續執行應用程序。
1.3.2 模塊機制
模塊是可以在運行時加入內核的代碼,這是Linux一個很好的特性。這個特性使內核可以很容易地擴大或縮小,一方面擴大內核可以增加內核的功能,另一方面縮小內核可以減小內核的大小。
Linux內核支持很多種模塊,驅動程序就是其中最重要的一種,甚至文件系統也可以寫成一個模塊,然後加入內核中。每一個模塊由編譯好的目標代碼組成,可以使用insmod(insert module的縮寫)命令將模塊加入正在運行的內核,也可以使用rmmod(remove module的縮寫)命令將一個未使用的模塊從內核中刪除。試圖刪除一個正在使用的模塊,將是不允許的。對Windows熟悉的朋友,可以將模塊理解為DLL文件。
模塊在內核啟動時裝載稱為靜態裝載,在內核已經運行時裝載稱為動態裝載。模塊可以擴充內核所期望的任何功能,但通常用於實現設備驅動程序。一個模塊的最基本框架代碼如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
int __init xxx_init(void)
{
/*這裡是模塊加載時的初始化工作*/
return 0;
}
void __exit xxx_exit(void)
{
/*這裡是模塊卸載時的銷毀工作*/
}
module_init(xxx_init); /*指定模塊的初始化函數的宏*/
module_exit(xxx_exit); /*指定模塊的卸載函數的宏*/
1.3.3 編寫設備驅動程序需要了解的知識
目前,Linux操作系統有七、八百萬行代碼,其中驅動程序代碼就有四分之三左右。所以對於驅動開發者來說,學習和編寫設備驅動程序都是一個漫長的過程。在這個過程中,讀者應該掌握如下一些知識:
1. 驅動開發人員應該有良好的C語言基礎,並能靈活地應用C語言的結構體、指針、宏等基本語言結構。另外,Linux系統使用的C編譯器是GNU C編譯器,所以對GNU C標准的C語言也應該有所了解。
2. 驅動開發人員應該有良好的硬件基礎。雖然不要求驅動開發人員具有設計電路的能力,但也應該對芯片手冊上描述的接口設備有清楚的認識。常用的設備有SRAM、Flash、UART、IIC和USB等。
3. 驅動開發人員應該對Linux內核源代碼有初步的了解。例如一些重要的數據結構和函數等。
4. 驅動開發人員應該有多任務程序設計的能力,同時驅動中也會使用大量的自旋鎖、互斥鎖和信號量等。
本書的大部分知識基本包括了這些方面,讀者在閱讀本書的過程中應該能快速掌握這些知識。
1.4 編寫設備驅動程序的注意事項
大部分程序員都比較熟悉應用程序的編寫,但是對於驅動程序的編寫可能不是很熟悉。關於應用程序的很多編程經驗不能直接應用到驅動程序的編寫中,下面給出編寫驅動程序的一些注意事項,希望引起讀者注意。
1.4.1 應用程序開發與驅動程序開發的差異
在Linux上的程序開發一般分為兩種,一種是內核及驅動程序開發,另一種是應用程序開發。這兩種開發種類對應Linux的兩種狀態,分別是內核態和用戶態。內核態用來管理用戶態的程序,完成用戶態請求的工作;用戶態處理上層的軟件工作。驅動程序與底層的硬件交互,所以工作在內核態。
大多數程序員致力於應用程序的開發,少數程序員則致力於內核及驅動程序的開發。相對於應用程序的開發,內核及驅動程序的開發有很大的不同。最重要的差異包括以下幾點:
內核及驅動程序開發時不能訪問C庫,因為C庫是使用內核中的系統調用來實現的,而且是在用戶空間實現的。驅動程序只能訪問有限的系統調用,或者匯編程序。
內核及驅動程序開發時必須使用GNU C,因為Linux操作系統從一開始就使用的是GNU C,雖然也可以使用其他的編譯工具,但是需要對以前的代碼做大量的修改。需要注意的是,32位的機型和64位的機型在編譯時,也有一定的差異。
內核支持異步中斷、搶占和SMP,因此內核及驅動程序開發時必須時刻注意同步和並發。
內核只有一個很小的定長堆棧。
內核及驅動程序開發時缺乏像用戶空間那樣的內存保護機制。稍不注意,就可能讀寫其他程序的內存。
內核及驅動程序開發時浮點數很難使用,應該使用整型數。
內核及驅動程序開發要考慮可移植性,因為對於不同的平台,驅動程序是不兼容的。
1.4.2 GUN C開發驅動程序
GUN C語言最早起源於一個GUN計劃,GUN的意思是GUN is not UNIX。GUN計劃開始於1984年,這個計劃的目的是開發一個類似UNIX並且軟件自由的完整操作系統。這個計劃一直在進行,到Linus開發Linux操作系統時,GNU計劃已經開發出來了很多高質量的自由軟件,其中就包括著名的GCC編譯器,GCC編譯器能夠編譯GUN C語言。Linus考慮到GUN計劃的自由和免費,所以選擇了GCC編譯器來編寫內核代碼,之後的很多開發者也使用這個編譯器,所以直到現在,驅動開發人員還使用GUN C語言來開發驅動程序。
1.4.3 不能使用C庫開發驅動程序
與用戶空間的應用程序不同,內核不能調用標准的C函數庫,主要的原因在於對於內核來說完整的C庫太大了。一個編譯的內核大小可以是1MB左右,而一個標准的C語言庫大小可能操作5MB。這對於存儲容量較小的嵌入式設備來說,是不實用的。缺少標准C語言庫,並不是說驅動程序就只能做很好的事情了。因為標准C語言庫是通過系統調用實現的,驅動也是通過系統調用等實現的,兩者都有相同的底層,所以驅動程序不需要調用C語言庫,也能實現很多的功能。
大部分常用的C庫函數在內核中都已經實現了。比如操作字符串的函數就位於內核文件lib/string.c中。只要包含<linux/string.h>,就可以使用它們;又如內存分配的函數也已經包含在include/linux/slab_def.h中實現了。
注意:內核程序中包含的頭文件是指內核代碼樹中的內核頭文件,不是指開發應用程序時的外部頭文件。在內核中實現的庫函數中的打印函數printk(),它是C庫函數printf()的內核版本。printk()函數和printf()函數有基本相同的用法和功能。
1.4.4 沒有內存保護機制
當一個用戶應用程序由於編程錯誤,試圖訪問一個非法的內存空間,那麼操作系統內核會結束這個進程,並返回錯誤碼。應用程序可以在操作系統內核的幫助下恢復過來,而且應用程序並不會對操作系統內核有太大的影響。但是如果操作系統內核訪問了一個非法的內存,那麼就有可能破壞內核的代碼或者數據。這將導致內核處於未知的狀態,內核會通過oops錯誤給用戶一些提示,但是這些提示都是不支持、難以分析的。
在內核編程中,不應該訪問非法內存,特別是空指針,否則內核會忽然死掉,沒有任何機會給用戶提示。對於不好的驅動程序,引起系統崩潰是很常見的事情,所以對於驅動開發人員來說,應該非常重視對內存的正確訪問。一個好的建議是,當申請內存後,應該對返回的地址進行檢測。
1.4.5 小內核棧
用戶空間的程序可以從棧上分配大量的空間存放變量,甚至用棧存放巨大的數據結構或者數組都沒問題。之所以能這樣做是因為應用程序是非常駐內存的,它們可以動態地申請和釋放所有可用的內存空間。內核要求使用固定常駐的內存空間,因此要求盡量少地占用常駐內存,而盡量多地留出內存提供給用戶程序使用。因此內核棧的長度是固定大小的,不可動態增長的,32位機的內核棧是8KB;64位機的內核棧是16KB。
由於內核棧比較小,所以編寫程序時,應該充分考慮小內核棧問題。盡量不要使用遞歸調用,在應用程序中,遞歸調用4000多次就有可能溢出,在內核中,遞歸調用的次數非常少,幾乎不能完成程序的功能。另外使用完內存空間後,應該盡快地釋放內存,以防止資源洩漏,引起內核崩潰。
1.4.6 重視可移植性
對於用戶空間的應用程序來說,可移植性一直是一個重要的問題。一般可移植性通過兩種方式來實現。一種方式是定義一套可移植的API,然後對這套API在這兩個需要移植的平台上分別實現。應用程序開發人員只要使用這套可移植的API,就可以寫出可移植的程序。在嵌入式領域,比較常見的API套件是QT。另一種方式是使用類似Java、Actionscript等可移植到很多操作系統上的語言。這些語言一般通過虛擬機執行,所以可以移植到很多平台上。
對於驅動程序來說,可移植性需要注意以下幾個問題:
考慮字節順序,一些設備使用大端字節序,一些設備使用小端字節序。Linux內核提供了大小端字節序轉換的函數。
[code]#define cpu_to_le16(v16) (v16)
#define cpu_to_le32(v32) (v32)
#define cpu_to_le64(v64) (v64)
#define le16_to_cpu(v16) (v16)
#define le32_to_cpu(v32) (v32)
#define le64_to_cpu(v64) (v64)
即使是同一種設備的驅動程序,如果使用的芯片不同,也應該寫不同的驅動程序,但是應該給用戶提供一個統一的編程接口。
盡量使用宏代替設備端口的物理地址,並且可以使用ifdefine宏確定版本等信息。
針對不同的處理器,應該使用相關處理器的函數。
1.5 Linux驅動的發展趨勢
隨著嵌入式技術的發展,使用Linux的嵌入式設備也越來越多,特別是現在的Android設備。同樣地,工業上對Linux驅動的開發也越來越重視。本節將對Linux驅動的發展做簡要的介紹。
1.5.1 Linux驅動的發展
Linux和嵌入式Linux軟件在過去幾年裡已經被越來越多的IT、半導體、嵌入式系統等公司所認可和接受,它已經成為一個可以替代微軟的Windows和眾多傳統的RTOS的重要的操作系統。Linux內核和基本組件及工具已經非常成熟。面向行業、應用和設備的嵌入式Linux工具軟件和嵌入式Linux操作系統平台,是未來發展的必然趨勢。符合標准,遵循開放是大勢所趨,人心所向,嵌入式Linux也不例外。
使嵌入式Linux不斷發展的一個核心問題,是提供大量的穩定和可靠的驅動程序。每天都有大量的芯片被生產出來,芯片的設計和原理不一樣,那麼驅動程序就不一樣。這樣,就需要大量的驅動程序開發人員開發驅動程序,可以說,Linux驅動程序的發展前景是很光明的。
1.5.2 驅動的應用
計算機系統已經融入到了各行各業、各個領域;計算機系統在電子產品中無處不在,從手機、游戲機、冰箱、電視、洗衣機等小型設備,到汽車、輪船、火車、飛機等大型設備都有它的身影。這些設備都需要驅動程序使之運行,可以說驅動程序的運用前景是非常廣泛的。每天都有很多驅動程序需要編寫,所以驅動程序開發人員的前途是無比光明的。
1.5.3 相關學習資源
學習Linux設備驅動程序,僅僅只學習理論是不夠的,還需要親自動手寫各種設備的驅動程序。編寫驅動程序不僅需要軟件知識,還需要硬件知識。在這裡,筆者推薦一些國內外優秀的驅動開發網站,希望對讀者的學習有所幫助。同時,筆者也計劃開通一個學習網站(http://www.zhengxiaoqiang.com),裡面將記錄一些工作心得和總結,希望對大家有所幫助。
1. Linux內核之旅網站:http://www.kerneltravel.net/;
2. 知名博客:http://www.lupaworld.com/26540;
3. Linux中國:http://www.linux-cn.com/;
4. 一個不錯的Linux中文社區:http://www.linux-cn.com/;
5. csdn內核驅動研究社區:http://topic.csdn.net/s/Linux_Dirver/0.html;
6. Linux伊甸園:http://bbs.linuxeden.com/index.php。
1.6 小結
本章首先對Linux設備驅動程序的基本概念進行了詳細的講述,並且講述了設備驅動程序的作用;接著講述了設備驅動程序的分類、特點及與操作系統之間的關系等;然後講述了驅動程序開發的一些重要知識和一些注意事項;最後講述了Linux驅動程序的發展趨勢。通過本章的學習,讀者可以對Linux設備驅動程序的開發有一個大概的了解。
隨著嵌入式設備的迅猛出現,有越來越多的驅動程序需要程序員去編寫,所以學習驅動程序的開發對個人的進步是非常有幫助的。本章作為驅動程序開發的入門,希望能夠引起讀者的學習興趣。