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

linux設備驅動linux

  驅動程序英文全稱Device Driver,也稱做設備驅動程序。驅動程序是用於計算機和外部設備通訊的特殊程序,至關於軟件和硬件的接口,一般只有操做系統能使用驅動程序。程序員

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



設備驅動介紹
編程

  驅動程序是附加到操做系統的一段程序,一般用於硬件通訊。bash

  每種硬件都有本身的驅動程序,其中包含了硬件設備的信息。操做系統經過驅動程序提供的硬件信息與硬件設備通訊。因爲驅動設備的重要性,在安裝操做系統後須要安裝驅動程序,外部設備才能正常工做。數據結構

  Linux內核自帶了至關多的設備驅動程序,幾乎能夠驅動目前主流的各類硬件設備。多線程

  在同一臺計算機上,儘管設備是相同的,可是因爲操做系統不一樣,驅動程序是有很大差異的。可是,不管什麼系統驅動程序的功能都是類似的,能夠概括爲下面三點:併發

  • 初始化硬件設備。ide

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

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

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

  • 提供輔助功能。

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



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加載模塊。


  使用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 */
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內核源代碼

1
sudo apt-get install linux- 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 | grep ModuleHelloWorld



爲內核模塊添加參數

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

  經過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" );
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*類型的參數。從新編譯,帶參數加載模塊。

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



總結    

  驅動其實也沒有傳說中的難,關鍵是須要動手去實踐,相信本身,什麼均可以!



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

相關文章
相關標籤/搜索