官术网_书友最值得收藏!

第2篇 Linux設備驅動核心理論

Linux內核模塊

Linux文件系統與設備文件系統

字符設備驅動

Linux設備驅動中的并發控制

Linux設備驅動中的阻塞與非阻塞I/O

Linux設備驅動中的異步通知與異步I/O

中斷與時鐘

內存與I/O訪問

工程中的Linux設備驅動

第4章 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
    4 static int hello_init(void)
    5 {
    6    printk(KERN_INFO " Hello World enter\n");
    7    return 0;
    8 }
    9
    10 static void hello_exit(void)
    11 {
    12    printk(KERN_INFO " Hello World exit\n ");
    13 }
    14
    15 module_init(hello_init);
    16 module_exit(hello_exit);
    17
    18 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
    19 MODULE_LICENSE("Dual BSD/GPL");
    20 MODULE_DESCRIPTION("A simple Hello World Module");
    21 MODULE_ALIAS("a simplest module");

這個最簡單的內核模塊只包含內核模塊加載函數、卸載函數和對Dual BSD/GPL許可權限的聲明以及一些描述信息,位于本書配套光盤 VirtualBox 虛擬機映像的/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/hello 目錄。編譯它會產生 hello.ko 目標文件,通過“insmod ./hello.ko”命令可以加載它,通過“rmmod hello”命令可以卸載它,加載時輸出“Hello World enter”,卸載時輸出“Hello World exit”。

內核模塊中用于輸出的函數是內核空間的printk()而非用戶空間的printf(),printk()的用法和printf()基本相似,但前者可定義輸出級別。printk()可作為一種最基本的內核調試手段,在 Linux驅動的調試章節中將詳細講解這個函數。

在Linux中,使用lsmod命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關系,例如:

        Module                Size  Used by
        hello                 9 472  0
        nls_iso8859_1         12 032  1
        nls_cp437            13 696  1
        vfat                 18 816  1
        fat                 57 376  1 vfat
        ...

lsmod命令實際上讀取并分析“/proc/modules”文件,與上述lsmod命令結果對應的“/proc/modules”文件如下:

        lihacker@lihacker-laptop:~/$ cat /proc/modules
        hello 9472 0 - Live 0xf953b000
        nls_iso8859_1 12032 1 - Live 0xf950c000
        nls_cp437 13696 1 - Live 0xf9561000
        vfat 18816 1 - Live 0xf94f3000
        ...

內核中已加載模塊的信息也存在于/sys/module 目錄下,加載 hello.ko 后,內核中將包含/sys/module/hello目錄,該目錄下又包含一個refcnt文件和一個sections目錄,在/sys/module/hello目錄下運行“tree -a”得到如下目錄樹:

        lihacker@lihacker-laptop:/sys/module/hello$ tree -a
        .
        |-- holders
        |-- initstate
        |-- notes
        |   '-- .note.gnu.build-id
        |-- refcnt
        |-- sections
        |   |-- .bss
        |   |-- .data
        |   |-- .gnu.linkonce.this_module
        |   |-- .note.gnu.build-id
        |   |-- .rodata.str1.1
        |   |-- .strtab
        |   |-- .symtab
        |   '-- .text
        '-- srcversion
        3 directories, 12 files

modprobe命令比insmod命令要強大,它在加載某模塊時,會同時加載該模塊所依賴的其他模塊。使用modprobe命令加載的模塊若以“modprobe -r filename”的方式卸載將同時卸載其依賴的模塊。

使用modinfo <模塊名>命令可以獲得模塊的信息,包括模塊作者、模塊的說明、模塊所支持的參數以及vermagic:

        lihacker@lihacker-laptop: ~ /develop/svn/ldd6410-read-only/training/kernel/drivers/
    hello$ modinfo hello.ko
        filename:         hello.ko
        alias:            a simplest module
        description:      A simple Hello World Module
        license:          Dual BSD/GPL
        author:           Barry Song <21cnbao@gmail.com>
        srcversion:       3FE9B0FBAFDD565399B9C05
        depends:
        vermagic:         2.6.28-11-generic SMP mod_unload modversions 586

4.2 Linux內核模塊程序結構

一個Linux內核模塊主要由如下幾個部分組成。

(1)模塊加載函數(一般需要)。

當通過insmod或modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。

(2)模塊卸載函數(一般需要)。

當通過rmmod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。

(3)模塊許可證聲明(必須)。

許可證(LICENSE)聲明描述內核模塊的許可權限,如果不聲明LICENSE,模塊被加載時,將收到內核被污染 (kernel tainted)的警告。

在Linux2.6內核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。

大多數情況下,內核模塊應遵循 GPL 兼容許可權。Linux2.6 內核模塊最常見的是以MODULE_LICENSE( "Dual BSD/GPL" )語句聲明模塊采用BSD/GPL雙LICENSE。

(4)模塊參數(可選)。

模塊參數是模塊被加載的時候可以被傳遞給它的值,它本身對應模塊內部的全局變量。

(5)模塊導出符號(可選)。

內核模塊可以導出符號(symbol,對應于函數或變量),這樣其他模塊可以使用本模塊中的變量或函數。

(6)模塊作者等信息聲明(可選)。

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等方法把它們轉換成有意義的錯誤信息字符串。

在Linux2.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.8 模塊卸載函數

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 = " dissecting Linux Device Driver ";
        static int num = 4 000;
        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命令時,應使用逗號分隔輸入的數組元素。

現在我們定義一個包含兩個參數的模塊(如代碼清單4.4,位于虛擬機/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/param目錄),并觀察模塊加載時被傳遞參數和不傳遞參數時的輸出。

代碼清單4.4帶參數的內核模塊

        1  #include <linux/init.h>
        2  #include <linux/module.h>
        3  MODULE_LICENSE("Dual BSD/GPL");
        4
        5  static char *book_name = "dissecting Linux Device Driver";
        6  static int num = 4 000;
        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("Barry Song <21cnbao@gmail.com>");
        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:4 000

當用戶運行“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:5 000

2.4 導出符號

Linux2.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 模塊的使用計數

Linux2.4內核中,模塊自身通過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理自己被使用的計數。

Linux2.6內核提供了模塊計數管理接口try_module_get(&module)和module_put (&module),從而取代Linux2.4內核中的模塊使用計數管理宏。模塊的使用計數一般不必由模塊自身管理,而且模塊計數管理還考慮了SMP與PREEMPT機制的影響。

        int try_module_get(struct module *module);

該函數用于增加模塊使用計數;若返回為 0,表示調用失敗,希望使用的模塊沒有被加載或正在被卸載中。

        void module_put(struct module *module);

該函數用于減少模塊使用計數。

try_module_get ()與module_put()的引入與使用與Linux2.6內核下的設備模型密切相關。Linux2.6內核為不同類型的設備定義了struct module *owner域,用來指向管理此設備的模塊。當開始使用某個設備時,內核使用try_module_get(dev->owner)去增加管理此設備的owner模塊的使用計數;當不再使用此設備時,內核使用 module_put(dev->owner)減少對管理此設備的owner 模塊的使用計數。這樣,當設備在使用時,管理此設備的模塊將不能被卸載。只有當設備不再被使用時,模塊才允許被卸載。

在Linux2.6內核下,對于設備驅動工程師而言,很少需要親自調用 try_module_get()與module_put(),因為此時開發人員所寫的驅動通常為支持某具體設備的owner模塊,對此設備owner模塊的計數管理由內核里更底層的代碼如總線驅動或是此類設備共用的核心模塊來實現,從而簡化了設備驅動開發。

4.9 模塊的編譯

我們可以為代碼清單4.1的模板編寫一個簡單的Makefile:

        KVERS = $(shell uname -r)
        # Kernel modules
        obj-m += hello.o
        # Specify flags for the module compilation.
        #EXTRA_CFLAGS=-g -O0
        build: kernel_modules
        kernel_modules:
              make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
        clean:
              make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

該Makefile文件應該與源代碼hello.c位于同一目錄,開啟其中的EXTRA_CFLAGS=-g -O0可以得到包含調試信息的hello.ko模塊。運行make命令得到的模塊可直接在PC上運行。

如果一個模塊包括多個.c文件(如file1.c、file2.c),則應該以如下方式編寫Makefile:

        obj-m := modulename.o
        modulename-objs := file1.o file2.o

4.10 使用模塊繞開GPL

對于企業自己編寫的驅動等內核代碼,如果不編譯為模塊則無法繞開GPL,編譯為模塊后企業在產品中使用模塊,則公司對外不再需要提供對應的源代碼,為了使公司產品所使用的Linux操作系統支持模塊,需要完成如下工作。

● 在內核編譯時應該選上“可以加載模塊”,嵌入式產品一般不需要動態卸載模塊,所以“可以卸載模塊”不用選。

● 將我們編譯的內核模塊.ko文件應該放置在目標文件系統的相關目錄中。

● 產品的文件系統中應該包含了支持新內核的insmod、lsmod、rmmod 等工具,由于嵌入式產品中一般不需要建立模塊間依賴關系,所以 modprobe 可以不要,一般也不需要卸載模塊,所以rmmod也可以不要。

● 在使用中用戶可使用insmod命令手動加載模塊,如insmod xxx.ko。

● 但是一般而言,產品在啟動過程中應該加載模塊,在嵌入式產品Linux的啟動過程中,加載企業自己的模塊的最簡單的方法是修改啟動過程的rc腳本,增加insmod /.../xxx.ko這樣的命令。用busybox做出的文件系統,通常修改etc/init.d/rcS文件。

4.11 總結

本章主要講解了Linux內核模塊的概念和基本的編程方法。內核模塊由加載/卸載函數、功能函數以及一系列聲明組成,它可以被傳入參數,也可以導出符號供其他模塊使用。

由于Linux設備驅動以內核模塊的形式而存在,因此,掌握這一章的內容是編寫任何類型設備驅動的必須。在具體的設備驅動開發中,將驅動編譯為模塊也有很強的工程意義,因為如果將正在開發中的驅動直接編譯入內核,而開發過程中會不斷修改驅動的代碼,則需要不斷地編譯內核并重啟Linux,但是如果編譯為模塊,則只需要rmmod并insmod即可,開發效率大為提高。

主站蜘蛛池模板: 天祝| 耒阳市| 临清市| 武冈市| 中西区| 宜州市| 邵武市| 青海省| 保康县| 阿克陶县| 泰州市| 巴东县| 马尔康县| 北辰区| 临朐县| 嘉鱼县| 贡嘎县| 怀来县| 南靖县| 汤阴县| 巴楚县| 六盘水市| 黔东| 沽源县| 台安县| 扎兰屯市| 敖汉旗| 灌南县| 尼勒克县| 大竹县| 宕昌县| 德昌县| 大英县| 汝阳县| 衡水市| 成都市| 扶风县| 南漳县| 安泽县| 雷山县| 资溪县|