Linux內核模塊編程與內核模塊LICENSE -《具體解釋(第3版)》預讀

Linux內核模塊簡單介紹

Linux內核的整體結構已經很龐大,而其包括的組件或許多。咱們如何把需要的部分都包括在內核中呢?
一種方法是把所有需要的功能都編譯到Linux內核。這會致使兩個問題。一是生成的內核會很大,二是假設咱們要在現有的內核中新增或刪除功能,將不得不又一次編譯內核。


有沒有一種機制使得編譯出的內核自己並不需要包括所有功能,而在這些功能需要被使用的時候,其相應的代碼被動態地載入到內核中呢?
Linux提供了這樣的一種機制,這樣的機制被稱爲模塊(Module)。模塊具備這樣的特色。
linux

  1. 模塊自己不被編譯入內核映像,從而控制了內核的大小。

  2. 模塊一旦被載入,它就和內核中的其它部分全然同樣。
爲了使讀者創建對模塊的初步感性認識,咱們先來看一個最簡單的內核模塊「Hello World」,如代碼清單4.1所看到的。


代碼清單4.1  一個最簡單的Linux內核模塊
shell

01 /*
02  * a simple kernel module: hello
03  *
04  * Copyright (C) 2014 Barry Song  (baohua@kernel.org)
05  *
06  * Licensed under GPLv2 or later.
07  */
08
09 #include <linux/init.h>
10 #include <linux/module.h>
11
12 static int __init hello_init(void)
13 {
14     printk(KERN_INFO "Hello World enter\n");
15     return 0;
16 }
17 module_init(hello_init);
18
19 static void __exit hello_exit(void)
20 {
21     printk(KERN_INFO "Hello World exit\n ");
22 }
23 module_exit(hello_exit);
24
25 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
26 MODULE_LICENSE("GPL v2");
27 MODULE_DESCRIPTION("A simple Hello World Module");
28 MODULE_ALIAS("a simplest module");

這個最簡單的內核模塊僅僅包括內核模塊載入函數、卸載函數和對GPL v2許可權限的聲明以及一些描寫敘述信息。位於本書配套源碼的/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」文件例如如下:
$ cat /proc/modules
hello 12393 0 - Live 0xe67a2000 (OF)
nls_utf8 12493 1 - Live 0xe678e000
isofs 39596 1 - Live 0xe677f000
vboxsf 42561 2 - Live 0xe6767000 (OF)
...
內核中已載入模塊的信息也存在於/sys/module文件夾下。載入hello.ko後,內核中將包括/sys/module/hello文件夾。該文件夾下又包括一個refcnt文件和一個sections文件夾,在/sys/module/hello文件夾下執行「tree –a」獲得例如如下文件夾樹:
root@barry-VirtualBox:/sys/module/hello# tree -a
.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│   └── .note.gnu.build-id
├── refcnt
├── sections
│   ├── .exit.text
│   ├── .gnu.linkonce.this_module
│   ├── .init.text
│   ├── .note.gnu.build-id
│   ├── .rodata.str1.1
│   ├── .strtab
│   └── .symtab
├── srcversion
├── taint
└── uevent

3 directories, 15 files

modprobe命令比insmod命令要強大。它在載入某模塊時,會同一時候載入該模塊所依賴的其它模塊。

使用modprobe命令載入的模塊若以「modprobe -r filename」的方式卸載將同一時候卸載其依賴的模塊。模塊之間的依賴關係上存放在根文件系統的/lib/modules/<kernel-version>/modules.dep文件裏,其實是在整體編譯內核的時候由depmod工具生成的,它的格式很easy:
app

kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

使用modinfo <模塊名>命令可以得到模塊的信息,包括模塊做者、模塊的說明、模塊所支持的參數以及vermagic:
# modinfo hello.ko
filename:       hello.ko
alias:          a simplest module
description:    A simple Hello World Module
license:        GPL v2
author:         Barry Song <21cnbao@gmail.com>
srcversion:     081230411494509792BD4A3
depends:        
vermagic:       3.8.0-39-generic SMP mod_unload modversions 686

Linux內核模塊程序結構

一個Linux內核模塊主要由例如如下幾個部分組成。
(1)模塊載入函數
當經過insmod或modprobe命令載入內核模塊時。模塊的載入函數會本身主動被內核執行,完畢本模塊的相關初始化工做。
(2)模塊卸載函數
當經過rmmod命令卸載某模塊時,模塊的卸載函數會本身主動被內核執行,完畢與模塊卸載函數相反的功能。
(3)模塊許可證聲明
許可證(LICENSE)聲明描寫敘述內核模塊的許可權限,假設不聲明LICENSE,模塊被載入時,將收到內核被污染 (kernel tainted)的警告。
在Linux內核模塊領域。可接受的LICENSE包括「GPL」、「GPL v2」、「GPL and additional rights」、「Dual BSD/GPL」、「Dual MPL/GPL」和「Proprietary」(關於模塊可否夠採用非GPL許可權如「Proprietary」,這個在學術界和法律界都有爭議)。
大多數狀況下,內核模塊應遵循GPL兼允許可權。

Linux內核模塊最多見的是以MODULE_LICENSE( "GPL v2" )語句聲明模塊採用GPL v2。
(4)模塊參數(可選)。
模塊參數是模塊被載入的時候可以被傳遞給它的值,它自己相應模塊內部的全局變量。
(5)模塊導出符號(可選)。
內核模塊可以導出符號(symbol。相應於函數或變量),這樣其它模塊可以使用本模塊中的變量或函數。
(6)模塊做者等信息聲明(可選)。

ide

 模塊載入函數

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內核裏,錯誤編碼是一個接近於0的負值,在<linux/errno.h>中定義。包括-ENODEV、-ENOMEM之類的符號值。工具

老是返回相應的錯誤編碼是種很好的習慣。因爲僅僅有這樣,用戶程序才幹夠利用perror等方法把它們轉換成有意義的錯誤信息字符串。
在Linux內核中,可以使用request_module(const char *fmt, …)函數載入內核模塊,驅動開發者可以經過調用
request_module(module_name);
這樣的靈活的方式載入其它內核模塊。post


在Linux中,所有標識爲_ _init的函數假設直接編譯進入內核,成爲內核鏡像的一部分,在鏈接的時候都放在.init.text這個區段內。
#define _ _init        _ _attribute_ _ ((_ _section_ _ (".init.text")))
所有的_ _init函數在區段.initcall.init中還保存了一份函數指針,在初始化時內核會經過這些函數指針調用這些_ _init函數,並在初始化完畢後,釋放init區段(包括.init.text、.initcall.init等)的內存。
除了函數之外,數據也可以被定義爲_ _initdata,對於僅僅是初始化階段需要的數據,內核在初始化完後,也可以釋放它們佔用的內存。好比,如下的代碼中將hello_data定義爲__initdata。
ui

static int hello_data __initdata = 1;

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, world %d\n", hello_data);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);

模塊卸載函數


Linux內核模塊載入函數通常以_ _exit標識聲明,典型的模塊卸載函數的形式如代碼清單4.3所看到的。
代碼清單4.3  內核模塊卸載函數
1    static void _ _exit cleanup_function(void)
2    {
3          /* 釋放代碼 */
4    }
5    module_exit(cleanup_function);

模塊卸載函數在模塊卸載的時候執行,不返回不論什麼值,必須以「module_exit(函數名)」的形式來指定。一般來講,模塊卸載函數要完畢與模塊載入函數相反的功能。


咱們用__exit來修飾模塊卸載函數,可以告訴內核假設相關的模塊被直接編譯進內核(即built-in),則cleanup_function() 函數會被省略直接不鏈接進最後的鏡像。既然模塊被built-in了。就不可能卸載它了。卸載函數也就沒有存在的必要了。除了函數之外,僅僅是退出階段採用的數據也可以用__exitdata來形容。

this

模塊參數


咱們可以用「module_param(參數名,參數類型,參數讀/寫權限)」爲模塊定義一個參數。比例如如下列代碼定義了1個整型參數和1個字符指針參數:
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

在裝載內核模塊時,用戶可以向模塊傳遞參數。形式爲「insmode(或modprobe)模塊名 參數名=參數值」,假設不傳遞,參數將使用模塊內定義的缺省值。假設模塊被built-in。就沒法insmod了,但是bootloader可以經過在bootargs裏設置「模塊名.參數名=值」的形式給該built-in的模塊傳遞參數。
參數類型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指針)、bool或invbool(布爾的反),在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較。推斷是否一致。
除此以外,模塊也可以擁有參數數組,形式爲「module_param_array(數組名,數組類型,數組長,參數讀/寫權限)」。
模塊被載入後,在/sys/module/文件夾下將出現以此模塊名命名的文件夾。當「參數讀/寫權限」爲0時。表示此參數不存在sysfs文件系統下相應的文件節點。假設此模塊存在「參數讀/寫權限」不爲0的命令行參數,在此模塊的文件夾下還將出現parameters文件夾。包括一系列以參數名命名的文件節點,這些文件的權限值就是傳入module_param()的「參數讀/寫權限」,而文件的內容爲參數的值。
執行insmod或modprobe命令時,應使用逗號分隔輸入的數組元素。
現在咱們定義一個包括兩個參數的模塊(如代碼清單4.4。位於本書源碼/kernel/drivers/param文件夾),並觀察模塊載入時被傳遞參數和不傳遞參數時的輸出。


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

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 static char *book_name = "dissecting Linux Device Driver";
05 module_param(book_name, charp, S_IRUGO);
06
07 static int book_num = 4000;
08 module_param(book_num, int, S_IRUGO);
09
10 static int __init book_init(void)
11 {
12     printk(KERN_INFO "book name:%s\n", book_name);
13     printk(KERN_INFO "book num:%d\n", book_num);
14     return 0;
15 }
16 module_init(book_init);
17
18 static void __exit book_exit(void)
19 {
20     printk(KERN_INFO "book module exit\n ");
21 }
22 module_exit(book_exit);
23
24 MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
25 MODULE_LICENSE("GPL v2");
26 MODULE_DESCRIPTION("A simple Module for testing module params");
27 MODULE_VERSION("V1.0");

對上述模塊執行「insmod book.ko」命令載入,相應輸出都爲模塊內的默認值。經過查看「/var/log/messages」日誌文件可以看到內核的輸出:
# 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' book_num=5000」命令時。輸出的是用戶傳遞的參數:
# 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
Jul  2 01:06:21 localhost kernel:  book num:5000
另外,在/sys文件夾下,也可以看到book模塊的參數:
barry@barry-VirtualBox:/sys/module/book/parameters$ tree
.
├── book_name
└── book_num

導出符號

Linux的「/proc/kallsyms」文件相應着內核符號表,它記錄了符號以及符號所在的內存地址。
模塊可以使用例如如下宏導出符號到內核符號表:
EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);
    導出的符號將可以被其它模塊使用。使用前聲明一下就能夠。EXPORT_SYMBOL_GPL()僅僅適用於包括GPL許可權的模塊。代碼清單4.5給出了一個導出整數加、減運算函數符號的內核模塊的樣例。
代碼清單4.5  內核模塊中的符號導出
01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 int add_integar(int a, int b)
05 {
06     return a + b;
07 }
08 EXPORT_SYMBOL_GPL(add_integar);
09
10 int sub_integar(int a, int b)
11 {
12     return a - b;
13 }
14 EXPORT_SYMBOL_GPL(sub_integar);
15
16 MODULE_LICENSE("GPL v2");

從「/proc/kallsyms」文件裏找出add_integar、sub_integar相關信息:
# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar    [export_symb]
e679403c r __kstrtab_sub_integar    [export_symb]
e6794038 r __kcrctab_sub_integar    [export_symb]
e6794024 r __ksymtab_add_integar    [export_symb]
e6794048 r __kstrtab_add_integar    [export_symb]
e6794034 r __kcrctab_add_integar    [export_symb]
e6793000 t add_integar    [export_symb]
e6793010 t sub_integar    [export_symb]

模塊聲明與描寫敘述


在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的做用,興許相關章節會有具體介紹。

 模塊的使用計數


Linux 2.4內核中。模塊自身經過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理本身被使用的計數。
Linux 2.6之後的內核提供了模塊計數管理接口try_module_get(&module)和module_put (&module),從而代替Linux 2.4內核中的模塊使用計數管理宏。

模塊的使用計數通常沒必要由模塊自身管理。而且模塊計數管理還考慮了SMP與PREEMPT機制的影響。



int try_module_get(struct module *module);

該函數用於添加模塊使用計數。若返回爲0。表示調用失敗,但願使用的模塊沒有被載入或正在被卸載中。

void module_put(struct module *module);

該函數用於下降模塊使用計數。
try_module_get ()與module_put()的引入與使用與Linux 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.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    

模塊與GPL

Linux內核有2種方法導出符號給模塊使用,一種方法是EXPORT_SYMBOL()。第二種EXPORT_SYMBOL_GPL()。這一點和模塊A導出符號給模塊B用是一致的。
內核的Documentation/DocBook/kernel-hacking.tmpl明白代表「the symbols exported by EXPORT_SYMBOL_GPL()can only be seen by modules with a MODULE_LICENSE() that specifies a GPL compatible license.」因而可知內核用EXPORT_SYMBOL_GPL()導出的符號是不可以被非GPL模塊引用的。
因爲至關多的內核符號都是以EXPORT_SYMBOL_GPL()導出的,因此歷史上之前有一些公司的作法是把內核的EXPORT_SYMBOL_GPL()直接改成EXPORT_SYMBOL()。而後將改動後的內核以GPL形式公佈。這樣改動內核以後,模塊再也不使用內核的EXPORT_SYMBOL_GPL()符號,所以模塊再也不需要GPL。對此Linus的回覆是:「I think both them said that anybody who were to change a xyz_GPL to the non-GPL one in order to use it with a non-GPL module would almost immediately fall under the "willful infringement" thing, and that it would make it MUCH easier to get triple damages and/or injunctions, since they clearly knew about it」。

所以,這樣的作法可能構成「蓄意侵權(willful infringement)」。
    第二種作法是寫一個wrapper內核模塊(這個模塊遵循GPL)。把EXPORT_SYMBOL_GPL()導出的符號封裝一次再次以EXPORT_SYMBOL()形式導出,而其它的模塊不直接調用內核而是調用wrapper函數。如圖4.1所看到的。這樣的作法也具備爭議。
 圖4.1將EXPORT_SYMBOL_GPL又一次以EXPORT_SYMBOL導出通常以爲。保守的作法是Linux內核不能使用非GPL許可權。

相關文章
相關標籤/搜索