天天看點

【驅動】linux裝置驅動·入門

linux裝置驅動

  驅動程式英文全稱Device Driver,也稱作裝置驅動程式。驅動程式是用于計算機和外部裝置通信的特殊程式,相當于軟體和硬體的接口,通常隻有作業系統能使用驅動程式。

  在現代計算機體系結構中,作業系統并不直接于硬體打交道,而是通過驅動程式于硬體通信。

裝置驅動介紹

  驅動程式是附加到作業系統的一段程式,通常用于硬體通信。

  每種硬體都有自己的驅動程式,其中包含了硬體裝置的資訊。作業系統通過驅動程式提供的硬體資訊與硬體裝置通信。由于驅動裝置的重要性,在安裝作業系統後需要安裝驅動程式,外部裝置才能正常工作。

  Linux核心自帶了相當多的裝置驅動程式,幾乎可以驅動目前主流的各種硬體裝置。

  在同一台計算機上,盡管裝置是相同的,但是由于作業系統不同,驅動程式是有很大差别的。但是,無論什麼系統驅動程式的功能都是相似的,可以歸納為下面三點:

  • 初始化硬體裝置。

    這是驅動程式最基本的功能,初始化通過總線識别裝置,通路裝置寄存器,按照需求配置裝置地端口,設定中斷等。

  • 向作業系統提供統一的軟體接口。

    裝置驅動程式向作業系統提供了一類裝置通用的軟體接口,如硬碟裝置向作業系統提供了讀寫磁盤塊、尋址等接口,無論是哪種品牌的硬碟驅動向作業系統提供的接口都是一緻的。

  • 提供輔助功能。

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

Linux核心子產品

  Linux核心子產品是一種可以被核心動态加載和解除安裝的可執行程式。

  通過核心子產品可以擴充核心的功能,通常核心子產品被用于裝置驅動、檔案系統等。如果沒有核心子產品,需要向核心添加功能就需要修改代碼、重新編譯核心、安裝新核心等步驟,不僅繁瑣而且容易保出錯,不易于調試。

核心子產品簡介

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

  為了彌補整體式核心的缺點,Linux核心的開發者設計了核心子產品機制。

  從代碼的角度看,核心子產品是一組可以完成某種功能的函數集合。

  從執行的角度看,核心子產品可以看做是一個已經編譯但是沒有連接配接的程式。

  核心子產品是一個應用程式,但是與普通應用程式有所不同,差別在于:

  • 運作環境不同。

    核心子產品運作在核心空間,可以通路系統的幾乎所有的軟硬體資源;普通應用程式運作在使用者空間,可以通路的資源受到限制。這也是核心子產品與普通應用程式最主要的差別。由于核心子產品可以獲得與作業系統核心相同的權限,是以在程式設計的時候應該格外注意,可能在使用者空間看到的一點小錯誤在核心空間就會導緻系統崩潰。

  • 功能定位不同。

    普通應用程式為了完成某個特定的目标,功能定位明确;核心子產品是為其他的核心子產品以及應用程式服務的,通常提供的是通用的功能。

  • 函數調用方式不同。

    核心子產品隻能調用核心提供的函數,通路其他的函數會導緻運作異常;普通應用程式可能調用自身以外的函數,隻要能正确連接配接就有運作。

核心子產品的結構

  核心程式設計與使用者空間程式設計最大的差別就是程式的并發性。

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

  設計核心子產品的資料結構要十分小心。由于代碼的可重入特性,必須考慮到資料結構在多線程環境下不被其他線程破壞,對于共享資料更是應該采用加鎖的方法保護。驅動程式員的通常錯誤是假定某段代碼不會出現并發,導緻資料被破壞而很難于調試。

  linux核心子產品使用實體記憶體,這點與應用程式不同。應用程式使用虛拟記憶體,有一個巨大的位址空間,在應用程式中可以配置設定大塊的記憶體。核心子產品可以供使用的記憶體非常小,最小可能小到一個記憶體頁面(4096位元組)。在編寫核心子產品代碼的時候要注意記憶體的配置設定和使用。

  核心子產品至少支援加載和解除安裝兩種操作。是以,一個核心子產品至少包括加載和解除安裝兩個函數。在linux 2.6系列核心中,通過module_init()宏可以在加載核心子產品的時候調用核心子產品的初始化函數,module_exit()宏可以在解除安裝核心子產品的時候調用核心子產品的解除安裝函數。

  核心子產品的初始化和解除安裝函數是有固定格式的。

1

2

static

int

__init init_func(

void

);    

//初始化函數

static

void

__exit exit_func(

void

);    

//清除函數

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

    • static修飾符的作用是函數僅在目前檔案有效,外部不可見;
    • __init關鍵字告訴編譯器,該函數代碼在初始化完畢後被忽略;
    • __exit關鍵字告訴編譯器,該代碼僅在解除安裝子產品的時候被調用;

核心子產品的加載

  linux核心提供了一個kmod的子產品用來管理核心子產品。

  kmod子產品與使用者态的kmodule子產品通信,擷取核心子產品的資訊。

  通過insmod指令和modprobe指令都可以加載一個核心子產品。

    • insmod指令加載核心子產品的時候不檢查核心子產品的符号是否已經在核心中定義。
    • modprobe不僅檢查核心子產品符号表,而且還會檢查子產品的依賴關系。

  另外,linux核心可以在需要加載某個子產品的時候,通過kmod機制通知使用者态的modprobe加載子產品。

【驅動】linux裝置驅動·入門

  使用insmod加載核心子產品的時候,首先使用特權級系統調用查找核心輸出的符号。通常,核心輸出符号被儲存在核心子產品清單第一個子產品結構裡。insmod指令把核心子產品加載到虛拟記憶體,利用核心輸出符号表來修改被加載子產品中沒有解析的核心函數的資源位址。

  修改完核心子產品中的函數和資源位址後,insmod使用特權指令申請存放核心子產品的空間。因為核心子產品是工作在核心态的,通路使用者态的資源需要做位址轉換。申請好空間後,insmod把核心子產品複制到新空間,然後把子產品加入到核心子產品清單的尾部,并且設定子產品标志為UNINTIALIZED,表示子產品還沒有被引用。insmod使用特權指令告訴核心新增加的子產品初始化和清除函數的位址,供核心調用。

核心子產品的解除安裝

  解除安裝的過程相對于加載要簡單,主要問題是對子產品引用計數的判斷。

  一個核心子產品被其他子產品引用的時候,自身的引用計數器會增加1.當解除安裝子產品的時候,需要判斷子產品引用計數器值是否為0,如果為0才能解除安裝子產品,否則隻能把子產品計數減1.

  超級使用者使用rmmod指令可以解除安裝指定的子產品。

  此外,核心kmod機制會定期檢查每個子產品的引用計數器,如果某個子產品的引用計數器值為0,kmod會解除安裝該子產品。

【驅動】linux裝置驅動·入門

編寫一個基本的核心子產品

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

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 */

static

int

__init hello_init(

void

)      

{

printk(KERN_ALERT

"(init)Hello,World!\n"

);

return

0;

}

/* exit function */

static

void

__exit hello_exit(

void

)      

{

printk(KERN_ALERT

"(exit)Bye-bye,Mystery!\n"

);

}

module_init(hello_init);                

module_exit(hello_exit);

編譯核心子產品

  編譯核心子產品需要建立一個Makefile,主要目的是使用核心頭檔案,因為核心子產品對核心版本有很強的依賴關系。

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

sudo

apt-get

install

linux-

source

  編譯核心子產品不需要重新編譯核心代碼,但前提是需要使用目前核心版本相同的代碼。

【驅動】linux裝置驅動·入門

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

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"回車編譯核心子產品。

【驅動】linux裝置驅動·入門

  ❹編譯結束後,生成ModuleHelloWorld.ko核心子產品,通過modprobe或者insmod加載核心子產品。

【驅動】linux裝置驅動·入門

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

  ❺加載核心子產品成功後,可以使用rmmod指令解除安裝核心子產品。

【驅動】linux裝置驅動·入門

  解除安裝子產品的時候,核心會調用核心的解除安裝函數,輸出hello_exit()函數的内容。

  子產品解除安裝以後,使用lsmod指令檢視子產品清單,如果沒有任何輸出,表示HelloWorld核心子產品已經被成功解除安裝。

lsmod |

grep

ModuleHelloWorld

為核心子產品添加參數

  驅動程式常需要在加載的時候提供一個或者多個參數,内子產品提供了設定參數的能力。

  通過module_param()宏可以為核心子產品設定一個參數。

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

  其中,參數名稱是加載核心子產品時使用的參數名稱,在核心子產品中需要有一個同名的變量與之對應;類型是參數的類型,核心支援C語言常用的基本類型;屬性是參數的通路權限。

21

22

23

24

#include <linux/init.h>                                              

#include <linux/module.h>

#include <linux/kernel.h>

MODULE_LICENSE(

"GPL"

);

MODULE_AUTHOR(

"Mystety"

);

static

int

initValue = 0;  

//子產品參數 initValue = <int value>

static

char

*initName = NULL;  

//子產品參數 initName = <char*>

module_param(initValue,

int

, S_IRUGO);

module_param(initName, charp, S_IRUGO);

/* init function */

static

int

__init hello_init(

void

)

{

printk(KERN_ALERT

"initValue = %d initName = %s \n"

,initValue,initName);

//列印參數值

printk(KERN_ALERT

"(init)Hello,World!\n"

);

return

0;

}

/* exit function */

static

void

__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*類型的參數。重新編譯,帶參數加載子產品。

【驅動】linux裝置驅動·入門

  從輸出結果可以看出,核心子產品的參數被正确傳遞到了程式中。

總結    

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

繼續閱讀