【驅動】linux設備驅動·入門

linux設備驅動

  驅動程序英文全稱Device Driver,也稱作設備驅動程序。驅動程序是用於計算機和外部設備通信的特殊程序,相當於軟件和硬件的接口,通常只有操作系統能使用驅動程序。

  在現代計算機體系結構中,操作系統並不直接於硬件打交道,而是通過驅動程序於硬件通信。



設備驅動介紹

  驅動程序是附加到操作系統的一段程序,通常用於硬件通信。

  每種硬件都有自己的驅動程序,其中包含了硬件設備的信息。操作系統通過驅動程序提供的硬件信息與硬件設備通信。由於驅動設備的重要性,在安裝操作系統後需要安裝驅動程序,外部設備才能正常工作。

  Linux內核自帶了相當多的設備驅動程序,幾乎可以驅動目前主流的各種硬件設備。

  在同一臺計算機上,儘管設備是相同的,但是由於操作系統不同,驅動程序是有很大差別的。但是,無論什麼系統驅動程序的功能都是相似的,可以歸納爲下面三點:

  • 初始化硬件設備。

    這是驅動程序最基本的功能,初始化通過總線識別設備,訪問設備寄存器,按照需求配置設備地端口,設置中斷等。

  • 向操作系統提供統一的軟件接口。

    設備驅動程序向操作系統提供了一類設備通用的軟件接口,如硬盤設備向操作系統提供了讀寫磁盤塊、尋址等接口,無論是哪種品牌的硬盤驅動向操作系統提供的接口都是一致的。

  • 提供輔助功能。

    現代計算機的處理能力越來越強,操作系統有一類虛擬設備驅動,可以模擬真實設備的操作,如虛擬打印機驅動向操作系統提供了打印機的接口,在系統沒有打印機制情況下仍然可以執行打印操作。



Linux內核模塊

  Linux內核模塊是一種可以被內核動態加載和卸載的可執行程序。

  通過內核模塊可以擴展內核的功能,通常內核模塊被用於設備驅動、文件系統等。如果沒有內核模塊,需要向內核添加功能就需要修改代碼、重新編譯內核、安裝新內核等步驟,不僅繁瑣而且容易保出錯,不易於調試。



內核模塊簡介

  Linux內核是一個整體結構,可以把內核想象成一個巨大的程序,各種功能結合在一起。當修改和添加新功能的時候,需要重新生成內核,效率較低。

  爲了彌補整體式內核的缺點,Linux內核的開發者設計了內核模塊機制。

  從代碼的角度看,內核模塊是一組可以完成某種功能的函數集合。

  從執行的角度看,內核模塊可以看做是一個已經編譯但是沒有連接的程序。

  內核模塊是一個應用程序,但是與普通應用程序有所不同,區別在於:

  • 運行環境不同。

    內核模塊運行在內核空間,可以訪問系統的幾乎所有的軟硬件資源;普通應用程序運行在用戶空間,可以訪問的資源受到限制。這也是內核模塊與普通應用程序最主要的區別。由於內核模塊可以獲得與操作系統內核相同的權限,因此在編程的時候應該格外注意,可能在用戶空間看到的一點小錯誤在內核空間就會導致系統崩潰。

  • 功能定位不同。

    普通應用程序爲了完成某個特定的目標,功能定位明確;內核模塊是爲其他的內核模塊以及應用程序服務的,通常提供的是通用的功能。

  • 函數調用方式不同。

    內核模塊只能調用內核提供的函數,訪問其他的函數會導致運行異常;普通應用程序可能調用自身以外的函數,只要能正確連接就有運行。




內核模塊的結構

  內核編程與用戶空間編程最大的區別就是程序的併發性

  在用戶空間,除多線程應用程序外,大部分應用程序的運行是順序執行的,在程序執行過程中不必擔心被其他程序改變執行的環境。而內核的程序執行環境要複雜的多,即時最簡單的內核模塊也要考慮到併發執行的問題。

  設計內核模塊的數據結構要十分小心。由於代碼的可重入特性,必須考慮到數據結構在多線程環境下不被其他線程破壞,對於共享數據更是應該採用加鎖的方法保護。驅動程序員的通常錯誤是假定某段代碼不會出現併發,導致數據被破壞而很難於調試。

  linux內核模塊使用物理內存,這點與應用程序不同。應用程序使用虛擬內存,有一個巨大的地址空間,在應用程序中可以分配大塊的內存。內核模塊可以供使用的內存非常小,最小可能小到一個內存頁面(4096字節)。在編寫內核模塊代碼的時候要注意內存的分配和使用。

  內核模塊至少支持加載和卸載兩種操作。因此,一個內核模塊至少包括加載和卸載兩個函數。在linux 2.6系列內核中,通過module_init()宏可以在加載內核模塊的時候調用內核模塊的初始化函數,module_exit()宏可以在卸載內核模塊的時候調用內核模塊的卸載函數。

  內核模塊的初始化和卸載函數是有固定格式的。

1
2
staticint__init init_func(void);    //初始化函數
staticvoid__exit exit_func(void);    //清除函數

  這兩個函數的名稱可以由用戶自己定義,但是必須使用規定的返回值和參數格式。

    • static修飾符的作用是函數僅在當前文件有效,外部不可見;

    • __init關鍵字告訴編譯器,該函數代碼在初始化完畢後被忽略;

    • __exit關鍵字告訴編譯器,該代碼僅在卸載模塊的時候被調用;



內核模塊的加載

  linux內核提供了一個kmod的模塊用來管理內核模塊。

  kmod模塊與用戶態的kmodule模塊通信,獲取內核模塊的信息。

  通過insmod命令和modprobe命令都可以加載一個內核模塊。

    • insmod命令加載內核模塊的時候不檢查內核模塊的符號是否已經在內核中定義。

    • modprobe不僅檢查內核模塊符號表,而且還會檢查模塊的依賴關係。

  另外,linux內核可以在需要加載某個模塊的時候,通過kmod機制通知用戶態的modprobe加載模塊。


  使用insmod加載內核模塊的時候,首先使用特權級系統調用查找內核輸出的符號。通常,內核輸出符號被保存在內核模塊列表第一個模塊結構裏。insmod命令把內核模塊加載到虛擬內存,利用內核輸出符號表來修改被加載模塊中沒有解析的內核函數的資源地址。

  修改完內核模塊中的函數和資源地址後,insmod使用特權指令申請存放內核模塊的空間。因爲內核模塊是工作在內核態的,訪問用戶態的資源需要做地址轉換。申請好空間後,insmod把內核模塊複製到新空間,然後把模塊加入到內核模塊列表的尾部,並且設置模塊標誌爲UNINTIALIZED,表示模塊還沒有被引用。insmod使用特權指令告訴內核新增加的模塊初始化和清除函數的地址,供內核調用。



內核模塊的卸載

  卸載的過程相對於加載要簡單,主要問題是對模塊引用計數的判斷。

  一個內核模塊被其他模塊引用的時候,自身的引用計數器會增加1.當卸載模塊的時候,需要判斷模塊引用計數器值是否爲0,如果爲0才能卸載模塊,否則只能把模塊計數減1.

  超級用戶使用rmmod命令可以卸載指定的模塊。

  此外,內核kmod機制會定期檢查每個模塊的引用計數器,如果某個模塊的引用計數器值爲0,kmod會卸載該模塊。



編寫一個基本的內核模塊

  還是以最經典的"Hello World !"爲例子吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 內核模塊: ModuleHelloWorld.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");      
MODULE_AUTHOR("Mystety");        
/* init function */
staticint__init hello_init(void)      
{
printk(KERN_ALERT "(init)Hello,World!\n");
return0;
}
/* exit function */
staticvoid__exit hello_exit(void)      
{
printk(KERN_ALERT "(exit)Bye-bye,Mystery!\n");
}
module_init(hello_init);                
module_exit(hello_exit);



編譯內核模塊

  編譯內核模塊需要建立一個Makefile,主要目的是使用內核頭文件,因爲內核模塊對內核版本有很強的依賴關係。

  ❶我用的系統是Ubuntu的,首先在系統命令行shell下安裝當前版本的linux內核源代碼

1
sudoapt-get installlinux-source

  編譯內核模塊不需要重新編譯內核代碼,但前提是需要使用當前內核版本相同的代碼。

  ❷安裝內核代碼完畢後,在ModuleHelloWorld.c同一目錄下編寫Makefile

1
2
3
4
5
6
7
8
ifneq ($(KERNELRELEASE),)
obj-m := ModuleHelloWorld.o
else
KERNELDIR := /lib/modules/$(shell uname-r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

  程序第1行檢查是否定義了KERNELRELEASE環境變量,如果定義則表示該模塊是內核代碼的一部分,直接把模塊名稱添加到 obj-m環境變量即可;

  如果未定義環境變量,表示在內核代碼以外編譯,通過設置KERNELDIR和PWD環境變量,然後通過內核腳本編譯當前文件,生成內核模塊文件。

  ❸Makefile建立完畢後,在shell下輸入"make"回車編譯內核模塊。


  ❹編譯結束後,生成ModuleHelloWorld.ko內核模塊,通過modprobe或者insmod加載內核模塊。

  在加載過程中可以看到hello_init()函數的輸出信息。

  ❺加載內核模塊成功後,可以使用rmmod命令卸載內核模塊。

  卸載模塊的時候,內核會調用內核的卸載函數,輸出hello_exit()函數的內容。

  模塊卸載以後,使用lsmod命令查看模塊列表,如果沒有任何輸出,表示HelloWorld內核模塊已經被成功卸載。

1
lsmod | grepModuleHelloWorld



爲內核模塊添加參數

  驅動程序常需要在加載的時候提供一個或者多個參數,內模塊提供了設置參數的能力。

  通過module_param()宏可以爲內核模塊設置一個參數。

  定義如下:module_param(參數名稱,類型,屬性)

  其中,參數名稱是加載內核模塊時使用的參數名稱,在內核模塊中需要有一個同名的變量與之對應;類型是參數的類型,內核支持C語言常用的基本類型屬性是參數的訪問權限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <linux/init.h>                                              
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mystety");
staticintinitValue = 0;   //模塊參數 initValue = <int value>
staticchar*initName = NULL;   //模塊參數 initName = <char*>
module_param(initValue, int, S_IRUGO);
module_param(initName, charp, S_IRUGO);
/* init function */
staticint__init hello_init(void)
{
printk(KERN_ALERT"initValue = %d initName = %s \n",initValue,initName); //打印參數值
printk(KERN_ALERT "(init)Hello,World!\n");
return0;
}
/* exit function */
staticvoid__exit hello_exit(void)
{
printk(KERN_ALERT "(exit)Bye-bye,Mystery!\n");
}
module_init(hello_init);                                          
module_exit(hello_exit);

  在原來的代碼中,增加了兩個變量initValue和initName,分別是int類型和char*類型;然後在第8行設置initValue爲int類型的參數,第9行設置initName爲char*類型的參數。重新編譯,帶參數加載模塊。

  從輸出結果可以看出,內核模塊的參數被正確傳遞到了程序中。



總結    

  驅動其實也沒有傳說中的難,關鍵是需要動手去實踐,相信自己,什麼都可以!



本文出自 “成鵬致遠” 博客,請務必保留此出處http://infohacker.blog.51cto.com/6751239/1218461


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章