Linux內核驅動模塊

Linux內核驅動模塊

Linux設備驅動會以內核模塊的形式出現,因此,學會編寫Linux內核模塊編程是學習Linux設備驅動的先決條件。
4.1~4.2節講解了Linux內核模塊的概念和結構,4.3~4.8節對Linux內核模塊的各個組成部分進行了展現,4.1~4.2與4.3~4.8節是整體與部分的關係。
4.9節說明了獨立存在的Linux內核模塊的Makefile文件編寫方法和模塊的編譯方法。
4.1 Linux內核模塊簡介Linux內核的整體結構已經非常龐大,而其包含的組件也非常多。我們怎樣把需要的部分都包含在內核中呢?
一種方法是把所有需要的功能都編譯到Linux內核。這會導致兩個問題,一是生成的內核會很大,二是如果我們要在現有的內核中新增或刪除功能,將不得不重新編譯內核。
有沒有一種機制使得編譯出的內核本身並不需要包含所有功能,而在這些功能需要被使用的時候,其對應的代碼被動態地加載到內核中呢?
答案是肯定的,Linux提供了這樣的一種機制,這種機制被稱爲模塊(Module)。模塊具有這樣的特點:
·  模塊本身不被編譯入內核映像,這控制了內核的大小。
·  模塊一旦被加載,它就和內核中的其它部分完全一樣。
爲了建立讀者對模塊的初步感性認識,我們先來看一個最簡單的內核模塊“Hello World”,如代碼清單4.1。
代碼清單4.1 一個最簡單的Linux內核模塊
1  #include <linux/init.h>
2  #include <linux/module.h>
3  MODULE_LICENSE("Dual BSD/GPL");
4  static int hello_init(void)
5  {
6    printk(KERN_INFO " Hello World enter\n");
7    return 0;
8  }
9  static void hello_exit(void)
10 {
11   printk(KERN_INFO " Hello World exit\n ");
12 }
13 module_init(hello_init);
14 module_exit(hello_exit);
15
16 MODULE_AUTHOR("Song Baohua");
17 MODULE_DESCRIPTION("A simple Hello World Module");
18 MODULE_ALIAS("a simplest module");
這個最簡單的內核模塊只包含內核模塊加載函數、卸載函數和對Dual BSD/GPL許可權限的聲明以及一些描述信息。編譯它會產生hello.ko目標文件,通過“insmod ./hello.ko”命令可以加載它,通過“rmmod hello”命令可以卸載它,加載時輸出“Hello World enter”,卸載時輸出“Hello World exit”。
內核模塊中用於輸出的函數是內核空間的printk()而非用戶空間的printf(),printk()的用法和printf()基本相似,但前者可定義輸出級別。printk()可作爲一種最基本的內核調試手段,在Linux驅動的調試章節中將詳細講解這個函數。
在Linux中,使用lsmod命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關係,例如:
[root@localhost driver_study]# lsmod
Module                  Size   Used by
hello                    1568    0
ohci1394                32716   0
ide_scsi                 16708   0
ide_cd                  39392   0
cdrom                  36960   1 ide_cd
lsmod命令實際上讀取並分析“/proc/modules”文件,與上述lsmod命令結果對應的“/proc/modules”文件如下:
[root@localhost driver_study]# cat /proc/modules
hello 1568 0 - Live 0xc8859000
ohci1394 32716 0 - Live 0xc88c8000
ieee1394 94420 1 ohci1394, Live 0xc8840000
ide_scsi 16708 0 - Live 0xc883a000
ide_cd 39392 0 - Live 0xc882f000
cdrom 36960 1 ide_cd, Live 0xc8876000
內核中已加載模塊的信息也存在於/sys/module目錄下,加載hello.ko後,內核中將包含/sys/module/hello目錄,該目錄下又包含一個refcnt文件和一個sections目錄,在/sys/module/hello目錄下運行tree –a得到如下目錄樹:
[root@localhost hello]# tree -a
.
|-- refcnt
`-- sections
    |-- .bss
    |-- .data
    |-- .gnu.linkonce.this_module
    |-- .rodata
    |-- .rodata.str1.1
    |-- .strtab
    |-- .symtab
    |-- .text
    `-- __versions
       modprobe命令比insmod命令要強大,它在加載某模塊時,會同時加載該模塊所依賴的其它模塊。使用modprobe命令加載的模塊若以“modprobe -r filename”的方式卸載將同時卸載其依賴的模塊。
       使用modinfo <模塊名>命令可以獲得模塊的信息,包括模塊作者、模塊的說明、模塊所支持的參數以及vermagic:
[root@localhost driver_study]# modinfo hello.ko
filename:       hello.ko
license:        Dual BSD/GPL
author:         Song Baohua
description:    A simple Hello World Module
alias:          a simplest module
vermagic:       2.6.15.5 686 gcc-3.2
depends:  
4.2 Linux內核模塊程序結構一個Linux內核模塊主要由如下幾個部分組成:
       ·  模塊加載函數(一般需要)
       當通過insmod或modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。
·  模塊卸載函數(一般需要)
當通過rmmod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。
·  模塊許可證聲明(必須)
許可證(LICENSE)聲明描述內核模塊的許可權限,如果不聲明LICENSE,模塊被加載時,將收到內核被污染 (kernel tainted)的警告。
在Linux 2.6內核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。
大多數情況下,內核模塊應遵循GPL兼容許可權。Linux 2.6內核模塊最常見的是以MODULE_LICENSE( "Dual BSD/GPL" )語句聲明模塊採用BSD/GPL雙LICENSE。
·  模塊參數(可選)
模塊參數是模塊被加載的時候可以被傳遞給它的值,它本身對應模塊內部的全局變量。
·  模塊導出符號(可選)
內核模塊可以導出符號(symbol,對應於函數或變量),這樣其它模塊可以使用本模塊中的變量或函數。
·  模塊作者等信息聲明(可選)
4.3模塊加載函數Linux內核模塊加載函數宜被以__init標識聲明,典型的模塊加載函數的形式如代碼清單4.2所示。
代碼清單4.2 內核模塊加載函數
1    static int __init initialization_function(void)
2    {    
3    /* 初始化代碼 */
4    }
5    module_init(initialization_function);
模塊加載函數必須以“module_init(函數名)”的形式被指定。它返回整型值,若初始化成功,應返回0。而在初始化失敗時,應該返回錯誤編碼。在Linux內核裏,錯誤編碼是一個負值,在<linux/errno.h>中定義,包含-ENODEV、-ENOMEM之類的符號值。總是返回相應的錯誤編碼是種非常好的習慣,因爲只有這樣,用戶程序纔可以利用perror等方法把它們轉換成有意義的錯誤信息字符串。
在Linux 2.6內核中,可以使用request_module(const char *fmt, …)函數加載內核模塊,驅動開發人員可以通過調用
request_module(module_name);

request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));
這種靈活的方式加載其它內核模塊。
在Linux中,所有標識爲__init的函數在連接的時候都放在.init.text這個區段內,此外,所有的__init函數在區段.initcall.init中還保存了一份函數指針,在初始化時內核會通過這些函數指針調用這些__init函數,並在初始化完成後,釋放init區段(包括.init.text,.initcall.init等)。
4.4模塊卸載函數       Linux內核模塊加載函數宜被以__exit標識聲明,典型的模塊卸載函數的形式如代碼清單4.3所示。
代碼清單4.3 內核模塊卸載函數
1     static void __exit cleanup_function(void)
2     {
3     /* 釋放代碼 */
4     }
5     module_exit(cleanup_function);
模塊卸載函數在模塊卸載的時候執行,不返回任何值,必須以“module_exit(函數名)”的形式來指定。
通常來說,模塊卸載函數要完成與模塊加載函數相反的功能,例如:
·  若模塊加載函數註冊了XXX,則模塊卸載函數應該註銷XXX。
·  若模塊加載函數動態申請了內存,則模塊卸載函數應釋放該內存
·  若模塊加載函數申請了硬件資源(中斷、DMA通道、I/O端口和I/O內存等)的佔用,則模塊卸載函數應釋放這些硬件資源。
·  若模塊加載函數開啓了硬件,則卸載函數中一般要關閉之。
和__init一樣,__exit也可以使對應函數在運行完成後自動回收內存。實際上,__init和__exit都是宏,其定義分別爲:
#define __init        __attribute__ ((__section__ (".init.text")))

#ifdef MODULE
#define __exit        __attribute__ ((__section__(".exit.text")))
#else
#define __exit        __attribute_used__ __attribute__ ((__section__(".exit.text")))
#endif
數據也可以被定義爲__initdata和__exitdata,這兩個宏分別爲:
#define __initdata   __attribute__ ((__section__ (".init.data")))

#define __exitdata  __attribute__ ((__section__(".exit.data")))
4.5模塊參數我們可以用“module_param(參數名,參數類型,參數讀/寫權限)”爲模塊定義一個參數,例如下列代碼定義了1個整型參數和1個字符指針參數:
static char *book_name = "深入淺出Linux設備驅動";
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
在裝載內核模塊時,用戶可以向模塊傳遞參數,形式爲“insmode(或modprobe)模塊名 參數名=參數值”,如果不傳遞,參數將使用模塊內定義的缺省值。
參數類型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指針)、bool 或invbool(布爾的反),在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較,判斷是否一致。
模塊被加載後,在/sys/module/目錄下將出現以此模塊名命名的目錄。當“參數讀/寫權限”爲0時,表示此參數不存在sysfs文件系統下對應的文件節點,如果此模塊存在“參數讀/寫權限”不爲0的命令行參數,在此模塊的目錄下還將出現parameters目錄,包含一系列以參數名命名的文件節點,這些文件的權限值就是傳入module_param()的“參數讀/寫權限”,而文件的內容爲參數的值。
除此之外,模塊也可以擁有參數數組,形式爲“module_param_array(數組名,數組類型,數組長,參數讀/寫權限)”。從2.6.0至2.6.10 版本,須將數組長變量名賦給“數組長”,從2.6.10 版本開始,須將數組長變量的指針賦給“數組長”,當不需要保存實際輸入的數組元素個數時,可以設置“數組長”爲NULL。
運行insmod或modprobe命令時,應使用逗號分隔輸入的數組元素。
現在我們定義一個包含2個參數的模塊(如代碼清單4.4),並觀察模塊加載時被傳遞參數和不傳遞參數時的輸出。
代碼清單4.4 帶參數的內核模塊
1  #include <linux/init.h>   
2  #include <linux/module.h>
3  MODULE_LICENSE("Dual BSD/GPL");                               

5  static char *book_name = "dissecting Linux Device Driver";    
6  static int num = 4000;   
7       
8  static int book_init(void)          
9  {                               
10    printk(KERN_INFO " book name:%s\n",book_name);                       
11    printk(KERN_INFO " book num:%d\n",num);                              
12    return 0;                               
13 }                               
14 static void book_exit(void)                               
15 {                               
16   printk(KERN_INFO " Book module exit\n ");                           
17 }                               
18 module_init(book_init);                               
19 module_exit(book_exit);                               
20 module_param(num, int, S_IRUGO);                               
21 module_param(book_name, charp, S_IRUGO);
22                                
23 MODULE_AUTHOR("Song Baohua, [email protected]");
24 MODULE_DESCRIPTION("A simple Module for testing module params");
25 MODULE_VERSION("V1.0");
對上述模塊運行“insmod book.ko”命令加載,相應輸出都爲模塊內的默認值,通過察看“/var/log/messages”日誌文件可以看到內核的輸出:
[root@localhost driver_study]# tail -n 2 /var/log/messages
Jul  2 01:03:10 localhost kernel:  <6> book name:dissecting Linux Device Driver
Jul  2 01:03:10 localhost kernel:  book num:4000
當用戶運行“insmod book.ko book_name=’GoodBook’ num=5000”命令時,輸出的是用戶傳遞的參數:
[root@localhost driver_study]# tail -n 2 /var/log/messages
Jul  2 01:06:21 localhost kernel:  <6> book name:GoodBook
Jul  2 01:06:21 localhost kernel:  book num:5000
4.6導出符號Linux 2.6的“/proc/kallsyms”文件對應着內核符號表,它記錄了符號以及符號所在的內存地址。
模塊可以使用如下宏導出符號到內核符號表:
EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);
       導出的符號將可以被其它模塊使用,使用前聲明一下即可。EXPORT_SYMBOL_GPL()只適用於包含GPL許可權的模塊。代碼清單4.5給出了一個導出整數加、減運算函數符號的內核模塊的例子(這些導出符號毫無實際意義,僅僅只是爲了演示)。
代碼清單4.5 內核模塊中的符號導出
1  #include <linux/init.h>                               
2  #include <linux/module.h>                               
3  MODULE_LICENSE("Dual BSD/GPL");                               
4                                 
5  int add_integar(int a,int b)                               
6  {                               
7  return a+b;                            
8  }
9                                
10 int sub_integar(int a,int b)                               
11 {                               
12   return a-b;                            
13 }                           
14
15 EXPORT_SYMBOL(add_integar);
16 EXPORT_SYMBOL(sub_integar);
       從“/proc/kallsyms”文件中找出add_integar、sub_integar相關信息:
[root@localhost driver_study]# cat /proc/kallsyms | grep integar
c886f050 r __kcrctab_add_integar        [export]
c886f058 r __kstrtab_add_integar        [export]
c886f070 r __ksymtab_add_integar        [export]
c886f054 r __kcrctab_sub_integar        [export]
c886f064 r __kstrtab_sub_integar        [export]
c886f078 r __ksymtab_sub_integar        [export]
c886f000 T add_integar  [export]
c886f00b T sub_integar  [export]
13db98c9 a __crc_sub_integar    [export]
e1626dee a __crc_add_integar    [export]
4.7模塊聲明與描述在Linux內核模塊中,我們可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分別聲明模塊的作者、描述、版本、設備表和別名,例如:
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
對於USB、PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE,如代碼清單4.6。
代碼清單4.6 驅動所支持的設備列表
1 /* 對應此驅動的設備表 */
2 static struct usb_device_id skel_table [] = {
3 { USB_DEVICE(USB_SKEL_VENDOR_ID,
4    USB_SKEL_PRODUCT_ID) },
5   { } /* 表結束 */
6 };
7
8 MODULE_DEVICE_TABLE (usb, skel_table);
此時,並不需要讀者理解MODULE_DEVICE_TABLE的作用,後續相關章節會有詳細介紹。
4.8模塊的使用計數2.4內核中,模塊自身通過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理自己被使用的計數。
Linux 2.6內核提供了模塊計數管理接口try_module_get(&module)和module_put (&module),從而取代2.4中的模塊使用計數管理宏。模塊的使用計數一般不必由模塊自身管理,而且模塊計數管理還考慮了SMP與PREEMPT機制的影響。
int try_module_get(struct module *module);
該函數用於增加模塊使用計數;若返回爲0,表示調用失敗,希望使用的模塊沒有被加載或正在被卸載中。
void module_put(struct module *module);
該函數用於減少模塊使用計數。
try_module_get ()與module_put()的引入與使用與2.6內核下的設備模型密切相關。Linux 2.6內核爲不同類型的設備定義了struct module *owner域,用來指向管理此設備的模塊。當開始使用某個設備時,內核使用try_module_get(dev->owner)去增加管理此設備的owner模塊的使用計數;當不再使用此設備時,內核使用module_put(dev->owner)減少對管理此設備的owner模塊的使用計數。這樣,當設備在使用時,管理此設備的模塊將不能被卸載。只有當設備不再被使用時,模塊才允許被卸載。
在Linux 2.6內核下,對於設備驅動工程師而言,很少需要親自調用try_module_get()與module_put(),因爲此時開發人員所寫的驅動通常爲支持某具體設備的owner模塊,對此設備owner模塊的計數管理由內核裏更底層的代碼如總線驅動或是此類設備共用的核心模塊來實現,從而簡化了設備驅動開發。
4.9模塊的編譯       我們可以爲代碼清單4.1的模板編寫一個簡單的Makefile:
obj-m := hello.o
並使用如下命令編譯Hello World模塊:
make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules
       如果當前處於模塊所在的目錄,則以下命令與上述命令同等:
         make –C /usr/src/linux-2.6.15.5 M=$(pwd) modules
       其中-C後指定的是Linux內核源代碼的目錄,而M=後指定的是hello.c和Makefile所在的目錄,編譯結果如下:
[root@localhost driver_study]# make -C /usr/src/linux-2.6.15.5/ M=/driver_study/ modules
make: Entering directory `/usr/src/linux-2.6.15.5'
  CC

  /driver_study/hello.o

/driver_study/hello.c:18:35: warning: no newline at end of file
  Building modules, stage 2.
  MODPOST
  CC      /driver_study/hello.mod.o
  LD

  /driver_study/hello.ko

make: Leaving directory `/usr/src/linux-2.6.15.5'
從中可以看出,編譯過程中,經歷了這樣的步驟:先進入Linux內核所在的目錄,並編譯出hello.o文件,運行MODPOST會生成臨時的hello.mod.c文件,而後根據此文件編譯出hello.mod.o,之後連接hello.o和hello.mod.o文件得到模塊目標文件hello.ko,最後離開Linux內核所在的目錄。
       中間生成的hello.mod.c文件的源代碼如代碼清單4.7所示。
代碼清單4.7 模塊編譯時生成的.mod.c文件
1    #include <linux/module.h>
2    #include <linux/vermagic.h>
3    #include <linux/compiler.h>
4   
5    MODULE_INFO(vermagic, VERMAGIC_STRING);
6   
7    struct module __this_module
8    __attribute__((section(".gnu.linkonce.this_module"))) = {
9     .name = KBUILD_MODNAME,
10    .init = init_module,
11    #ifdef CONFIG_MODULE_UNLOAD
12    .exit = cleanup_module,
13    #endif
14    };
15   
16    static const char __module_depends[]
17    __attribute_used__
18    __attribute__((section(".modinfo"))) =
19    "depends=";
hello.mod.o產生了ELF(Linux所採用的可執行/可連接的文件格式)的2個節,即modinfo和.gun.linkonce.this_module。
如果一個模塊包括多個.c文件(如file1.c、file2.c),則應該以如下方式編寫Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o
4.10使用模塊繞開GPL       對於企業自己編寫的驅動等內核代碼,如果不編譯爲模塊則無法繞開GPL,編譯爲模塊後企業在產品中使用模塊,則公司對外不再需要提供對應的源代碼,爲了使公司產品所使用的Linux操作系統支持模塊,需要完成如下工作:
· 在內核編譯時應該選上“可以加載模塊”,嵌入式產品一般不需要動態卸載模塊,所以“可以卸載模塊”不用選,當然選了也沒關係,如圖4.1。

圖4.1 內核中支持模塊的編譯選項

如果有項目被選擇“M”,則編譯時除了make bzImage或zImage以外,也要make modules。
· 將我們編譯的內核模塊.ko文件應該放置在目標文件系統的相關目錄中。
· 產品的文件系統中應該包含了支持新內核的insmod、lsmod、rmmod等工具,由於嵌入式產品中一般不需要建立模塊間依賴關係,所以modprobe可以不要,一般也不需要卸載模塊,所以rmmod也可以不要。
· 在使用中用戶可使用insmod命令手動加載模塊,如insmod xxx.ko。
       · 但是一般而言,產品在啓動過程中應該加載模塊,在嵌入式產品Linux的啓動過程中,加載企業自己的模塊的最簡單的方法是修改啓動過程的rc腳本,增加insmod /.../xxx.ko這樣的命令。
如某設備正在使用的Linux系統中的rc腳本是這樣的:
mount /proc
mount /var
mount /dev/pts
mkdir /var/log
mkdir /var/run
mkdir /var/ftp
mkdir -p /var/spool/cron
mkdir /var/config
...
insmod /usr/lib/company_driver.ko 2> /dev/null
/usr/bin/userprocess
/var/config/rc
總結       本章主要講解了Linux內核模塊的概念和基本的編程方法。內核模塊由加載/卸載函數、功能函數以及一系列聲明組成,它可以被傳入參數,也可以導出符號供其它模塊使用。
由於Linux設備驅動以內核模塊的形式而存在,因此,掌握這一章的內容是編寫任何類型設備驅動的必須。在具體的設備驅動開發中,將驅動編譯爲模塊也有很強的工程意義,因爲如果將正在開發中的驅動直接編譯入內核,而開發過程中會不斷修改驅動的代碼,則需要不斷的編譯內核並重啓Linux,但是如果編譯爲模塊,則只需要rmmod並insmod即可,開發效率爲大爲提高
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章