一吁断、Linux內(nèi)核模塊簡介
1.1 Linux內(nèi)核模塊介紹
Linux內(nèi)核的整體結(jié)構(gòu)已經(jīng)非常龐大孵户,而其包含的組件也非常多宵距。我們怎樣把需要的部分都包含在內(nèi)核中呢?
一種方法是把所有需要的功能都編譯到Linux內(nèi)核略荡。這會導致兩個問題庵佣,一是生成的內(nèi)核會很大,二是如果我們要在現(xiàn)有的內(nèi)核中新增或刪除功能汛兜,將不得不重新編譯內(nèi)核巴粪。
有沒有一種機制使得編譯出的內(nèi)核本身并不需要包含所有功能,而在這些功能需要被使用的時候序无,其對應(yīng)的代碼被動態(tài)地加載到內(nèi)核中呢验毡?
Linux提供了這樣一種機制衡创,這種機制被稱為模塊(Module)帝嗡。模塊具有這樣的特點:
- ++模塊本身不被編譯如內(nèi)核映像,從而控制了內(nèi)核的大小++璃氢;
- ++模塊一旦被加載哟玷,它就和內(nèi)核中的其他部分完全一樣++;
模塊化的最大的好處是可以動態(tài)擴展應(yīng)用程序的功能而無需重新編譯鏈接生成一個新的應(yīng)用程序映像(或內(nèi)核)。這對應(yīng)Windows系統(tǒng)上為動態(tài)鏈接庫DDL(Dynamic Link Library)巢寡,對應(yīng)到Linux系統(tǒng)中為驅(qū)動模塊和應(yīng)用程序的共享庫so(shared object)喉脖。
模塊的機制使驅(qū)動程序與Linux內(nèi)核結(jié)合有兩種方式:在編譯內(nèi)核時,靜態(tài)地鏈接進內(nèi)核抑月;++在系統(tǒng)運行時树叽,以模塊加載的方式加載進內(nèi)核++。
1.2 Linux內(nèi)核模塊的構(gòu)成
一個Linux內(nèi)核模塊主要由如下幾個部分組成:
(1)模塊加載函數(shù)(一般需要)
當通過insmod或modprobe命令加載內(nèi)核模塊時谦絮,模塊的加載函數(shù)會被內(nèi)核執(zhí)行题诵,完成本模塊的相關(guān)初始化工作。
(2)模塊卸載函數(shù)(一般需要)
當通過rmmod命令卸載某模塊時层皱,模塊的卸載函數(shù)會自動被內(nèi)核執(zhí)行性锭,完成與加載函數(shù)相反的功能。
(3)模塊許可證聲明(必須)
許可證(LICENSE)聲明描述內(nèi)核模塊的許可權(quán)限叫胖,如果不聲明LICENSE草冈,模塊被加載時,將收到內(nèi)核被污染(kernel tainted)的警告瓮增。
在Linux 2.6內(nèi)核中怎棱,可接受的LICENSE包括“GPL”、“GPL v2”绷跑、“GPL and additional rights”蹄殃、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”你踩。大多數(shù)情況下诅岩,內(nèi)核模塊應(yīng)遵循GPL兼容許可權(quán),Linux 2.6內(nèi)核模塊最常見的是以MODULE_LICENSE(“Dual BSD/GPL”);語句聲明模塊采用BSD/GPL雙LICENSE带膜。
(4)模塊參數(shù)(可選)
模塊參數(shù)是模塊被加載的時候可以被傳遞給它的值吩谦,它本身對應(yīng)模塊內(nèi)部的全局變量。
(5)模塊導出符號(可選)
內(nèi)核模塊可以導出符號(symbol膝藕,對應(yīng)于函數(shù)或變量)式廷,這樣其他模塊就可以使用本模塊中的變量或函數(shù)。
(6)模塊作者等信息聲明(可選)
1.3 一個簡單的Linux內(nèi)核模塊
一個基本的內(nèi)核模塊只需包含頭文件芭挽、模塊加載函數(shù)滑废、模塊卸載函數(shù)以及許可協(xié)議和一些描述信息。如代碼清單1.1所示袜爪。
代碼清單1.1 一個最簡單的Linux內(nèi)核模塊
#include <linux/init.h> /* 頭文件 */
#include <linux/module.h>
static int hello_init(void)
{
printk(KERN_INFO”Hello world enter.\n”);
return 0;
}
static void hello_exit(void)
{
printk(KERN_INFO”Hello world exit.\n”);
}
module_init(hello_init);
module_exit(hello_exit);
/* 模塊說明信息 */
MODULE_LICENSE(“Dual BSD/GPL”);
MODULE_AUTHOR(“Aaron konishi:zh5202@163.com”);
MODULE_ALIAS(“A simplest module”);
MODULE_DESCRIPTION(“A simple Hello world module”);
編譯它會產(chǎn)生一個hello.ko目標文件蠕趁,通過“insmod ./hello.ko”命令可以加載它,通過“rmmod hello”命令可以卸載它辛馆,加載時輸出“Hello world enter.”俺陋,卸載時輸出“Hello world exit”。
注釋:內(nèi)核模塊中的輸出函數(shù)是內(nèi)核空間的printk()而非用戶空間的printf(),printk()用法與printf()基本相似腊状,但前者可以定義輸出級別诱咏。printk()可作為一種最基本的內(nèi)核調(diào)試手段,在Linux驅(qū)動的調(diào)試章節(jié)中將詳細講解這個函數(shù)缴挖。
內(nèi)核中已加載模塊的信息存在于/sys/module目錄下袋狞,加載hello.ko后,內(nèi)核中將包含/sys/module/hello目錄映屋,該目錄下又包含一個refcnt文件和一個sections目錄硕并,在/sys/module/hello目錄下運行“tree -a”得到如下目錄樹:
# 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
1.4 Linux模塊操作常用命令
在編寫、調(diào)試和使用Linux內(nèi)核模塊的過程中秧荆,常常會使用到lsmod倔毙、modprobe、rmmod和modinfo命令乙濒。
++lsmod命令可以獲得系統(tǒng)中加載了的所有模塊以及模塊間的依賴關(guān)系++陕赃,例如:
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命令結(jié)果對應(yīng)的“/proc/modules”文件如下:
# 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
…
++modprobe命令比insmod命令要強大颁股,它在加載某模塊時么库,會同時加載該模塊所依賴的其他模塊。使用modprobe命令加載的模塊若以“modprobe -r filename”的方式卸載將同時卸載其依賴的模塊甘有。++
++使用modinfo<模塊名>命令可以獲得模塊的信息诉儒,包括模塊作者、模塊說明亏掀、模塊所支持的參數(shù)以及vermagic++忱反。
# modinfo hello.ko
filename: hello.ko
alias: A simplest module
description: A simple Hello world module
license: Dual BSD/GPL
author: Aaron konishi:zh5202@163.com
srcversion: 3FE9B0FBAFDD565399B9C05
depends:
vermagic: 2.6.15-11-generic SMP mod_unload modversions 586
二、Linux內(nèi)核模塊詳解
2.1 模塊加載函數(shù)
++Linux內(nèi)核模塊加載函數(shù)一般以__init標識聲明++滤愕,典型的模塊加載函數(shù)的形式如代碼清單1.2所示温算。
代碼清單1.2 內(nèi)核模塊加載函數(shù)
static int __init initialization_function(void)
{
/* 初始化代碼 */
}
module_init(initialization_function);
==模塊加載函數(shù)必須以“module_init(函數(shù)名)”的形式被指定==。它返回整型值间影,若初始化成功則返回0注竿;初始化失敗則返回錯誤編碼。在Linux內(nèi)核里魂贬,錯誤編碼是一個負值巩割,在<linux/errno.h>中定義,包含-ENODEV付燥、-ENOMEM之類的符號值宣谈。總是返回相應(yīng)的錯誤編碼是種非常好的習慣机蔗,因為只有這樣蒲祈,用戶程序才可以利用perror等方法把它們轉(zhuǎn)換成有意義的錯誤信息字符串。
在Linux 2.6內(nèi)核中萝嘁,可以++使用request_module(const char *fmt,…)函數(shù)加載內(nèi)核模塊++梆掸,驅(qū)動開發(fā)人員可以通過調(diào)用:
request_module(module_name);
或:
request_module(“char-major-%d-%d”, MAJOR(dev), MINOR(dev));
這種靈活的方式加載其他內(nèi)核模塊。
在Linux中牙言,所有標識為__init的函數(shù)在鏈接的時候都放在.init.text這個區(qū)段內(nèi)酸钦,此外,所有的__init函數(shù)在區(qū)段.initcall.init中還保存了一份函數(shù)指針咱枉,在初始化時內(nèi)核會通過這些函數(shù)指針調(diào)用這些__init函數(shù)卑硫,并在初始化完成后,釋放init區(qū)段(包括.init.text蚕断、.initcall.init等)欢伏。
2.2 模塊卸載函數(shù)
++Linux內(nèi)核模塊卸載函數(shù)一般以__exit標識聲明++,典型的模塊卸載函數(shù)的形式如代碼清單1.3所示亿乳。
代碼清單1.3 內(nèi)核模塊卸載函數(shù)
static void __exit cleanup_function(void)
{
/* 釋放代碼 */
}
module_exit(cleanup_function);
模塊卸載函數(shù)在模塊卸載的時候執(zhí)行硝拧,不返回任何值,++必須以“module_exit(函數(shù)名)”的形式來指定++葛假。
通常來說障陶,模塊卸載函數(shù)要完成與模塊加載函數(shù)相反的功能,如下所示:
- 若模塊加載函數(shù)注冊了xxx聊训,則模塊卸載函數(shù)應(yīng)該注銷xxx抱究;
- 若模塊加載函數(shù)動態(tài)申請了內(nèi)存,則模塊卸載函數(shù)應(yīng)釋放該內(nèi)存带斑;
- 若模塊加載函數(shù)開啟了硬件鼓寺,則模塊卸載函數(shù)中一般要關(guān)閉該硬件;
- 若模塊加載函數(shù)申請了硬件資源(中斷勋磕、DMA通道侄刽、I/O端口和I/O內(nèi)存等)的占用,則模塊卸載函數(shù)應(yīng)釋放這些硬件資源朋凉;
和__init一樣州丹,__exit也可以使對應(yīng)的函數(shù)在運行完成后自動回收內(nèi)存。實際上杂彭,__init和__exit都是宏墓毒,其定義分別為:
#define __init __attribute__ ((__section__(“.init.text”)))
#define MODULE
#define __exit __attribute__ ((__section__(“.exit.text”)))
#else
#define __exit __attribute_used__attribute__ ((__section__(“.exit.text”)))
#endif
數(shù)據(jù)也可以被定義為__initdata和__exitdata,這兩個宏分別為:
#define __initdata __attribute__ ((__section__(“.init.data”)))
#define __exitdata __attribute__ ((__section__(“.exit.data”)))
2.3 模塊參數(shù)
我們可以++用“module_param(參數(shù)名,參數(shù)類型,參數(shù)讀/寫權(quán)限)”為模塊定義一個參數(shù)++亲怠,例如下列代碼定義了1個整型參數(shù)和1個字符指針參數(shù):
static char *book_name = “dissecting Linux Device Driver”;
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
++在裝載內(nèi)核模塊時所计,用戶可以向模塊傳遞參數(shù),形式為“insmod(或modprobe) 模塊名 參數(shù)名=參數(shù)值”团秽,如果不傳遞主胧,參數(shù)將使用模塊內(nèi)定義的缺省值++叭首。
參數(shù)類型可以是byte、short踪栋、ushort焙格、int、uint夷都、long眷唉、ulong、charp(字符指針)囤官、bool或invbool(布爾的反)冬阳,在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較,判斷是否一致党饮。
模塊被加載后肝陪,在/sys/module/目錄下將出現(xiàn)以此模塊名命名的目錄。當“參數(shù)讀/寫權(quán)限”為0時刑顺,表示此參數(shù)不存在sysfs文件系統(tǒng)下對應(yīng)的文件節(jié)點见坑,如果此模塊存在“參數(shù)讀/寫權(quán)限”不為0的時,在此模塊的目錄下將會出現(xiàn)parameters目錄捏检,包含一系列以參數(shù)名命名的文件節(jié)點荞驴,這些文件的權(quán)限值就是傳入module_param()的“參數(shù)讀/寫權(quán)限”,而文件的內(nèi)容為參數(shù)的值贯城。
除此之外熊楼,++模塊也可以擁有參數(shù)數(shù)組,形式為“module_param_array(數(shù)組名, 數(shù)組類型, 數(shù)組長度, 參數(shù)讀/寫權(quán)限)”++能犯。從2.6.0~2.6.10版本鲫骗,需將數(shù)組長變量名賦給“數(shù)組長”,從2.6.10版本開始踩晶,需將數(shù)組長變量的指針賦給“數(shù)組長”执泰,當不需要保存實際輸入的數(shù)組元素個數(shù)時,可以設(shè)置“數(shù)組長”為NULL渡蜻。
運行insmod或modprobe命令時术吝,應(yīng)使用逗號分隔輸入的數(shù)組元素。
現(xiàn)在我們定義一個包含兩個參數(shù)的模塊(如代碼清單1.4)茸苇,并觀察模塊加載時被傳遞參數(shù)和不傳遞參數(shù)時的輸出排苍。
代碼清單1.4 帶參數(shù)的內(nèi)核模塊
#include <linux/init.h>
#include <linux/module.h>
static char *book_name = “dissecting Linux Device Driver”;
static int num = 4000;
static int book_init(void)
{
printk(KERN_INFO”book name:%s\n”, book_name);
printk(KERN_INFO”book num:%d\n”, num);
return 0;
}
static void book_exit(void)
{
printk(KERN_INFO”book module exit\n”);
}
module_init(book_init);
module_exit(book_exit);
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
MODULE_AUTHOR(“Aaron Konishi<zh5202@163.com>”);
MODULE_DESCRIPTION(“A simple Module for testing module params”);
MODULE_VERSION(“V1.0”);
MODULE_LICENSE(“Dual BSD/GPL”);
對上述模塊運行“insmod book.ko”命令加載,相應(yīng)輸出都為模塊內(nèi)的默認值学密,通過查看“/var/log/messages”日志文件可以看到內(nèi)核的輸出:
# 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: <6> book num:4000
當用戶運行“insmod book.ko book_name=’GoodBook’ num=5000”命令時淘衙,輸出的是用戶傳遞的參數(shù):
# tail -n 2 /var/log/messages
Jul 2 01:03:10 localhost kernel: <6> book name:GoodBook
Jul 2 01:03:10 localhost kernel: <6> book num:5000
2.4 模塊符號導出
Linux 2.6的“/proc/kallsyms”文件對應(yīng)著內(nèi)核符號表,它記錄了符號以及符號所在的內(nèi)存地址腻暮。模塊可以使用如下宏導出符號到內(nèi)核符號表:
EXPORT_SYMBOL (符號名);
EXPORT_SYMBOL_GPL(符號名);
導出的符號將可以被其他模塊使用彤守,使用前聲明一下即可毯侦。EXPORT_SYMBOL_GPL()只適用于包含GPL許可權(quán)的模塊。代碼清單1.5給出了一個導出整數(shù)加具垫、減運算函數(shù)符號的內(nèi)核模塊的例子(這里僅僅是為了演示)侈离。
代碼清單1.5 內(nèi)核模塊中的符號導出
#include <linux/init.h>
#include <linux/module.h>
int add_integar(int a,int b);
{
return a+b;
}
int sub_integar(int a,int b)
{
return a-b;
}
EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(add_integar);
MODULE_LICENSE(“Dual BSD/GPL”);
加載該模塊之后,從“/proc/kallsyms”文件中找出add_integar做修、sub_integar相關(guān)信息:
# cat /proc/kallsyms | grep integar
…..
c886f00 T add_integar [export]
c886f0b T sub_integar [export]
……
2.5 模塊的聲明與描述
在Linux內(nèi)核模塊中霍狰,我們可以用MODULE_AUTHOR抡草、MODULE_DESCRIPTION饰及、MODULE_VERSION、MODULE_DEVICE_TABLE康震、MODULE_ALIAS分別聲明模塊的作者燎含、描述、版本腿短、設(shè)備表和別名屏箍,例如:
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
對于USB、PCI等設(shè)備驅(qū)動橘忱,通常會建立一個MODULE_DEVICE_TABLE赴魁,表明該驅(qū)動模塊所支持的設(shè)備,如代碼清單1.6所示钝诚。
/* 對應(yīng)此驅(qū)動的設(shè)備表 */
static struct usb_device_id skel_table[] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID,
USB_SKEL_PRODUCT_ID) }
{ } /* 表結(jié)束 */
};
MODULE_DEVICE_TABLE(usb, skel_table);
此時颖御,并不需要讀者理解MODULE_DEVICE_TABLE的作用,后續(xù)相關(guān)章節(jié)會有詳細介紹凝颇。
2.6 模塊的使用計數(shù)
Linux 2.4內(nèi)核中潘拱,模塊自身通過MOD_INC_COUNT、MOD_DEC_USE_COUNT宏來管理自己被使用的計數(shù)拧略。
Linux 2.6內(nèi)核提供了模塊計數(shù)管理接口try_module_get(&module)和module_put(&module)芦岂,從而取代Linux 2.4內(nèi)核中的模塊使用計數(shù)管理宏。模塊的使用計數(shù)一般不必由模塊自身管理垫蛆,而且模塊計數(shù)管理還考慮了SMP與PREEMPT機制的映像禽最。
int try_module_get(struct module *module);
該函數(shù)用于增加模塊使用計數(shù),若返回0袱饭,表示調(diào)用失敗弛随,希望使用的模塊沒有被加載或正在被卸載中。
void module_put(struct module *module);
該函數(shù)用于減少模塊使用計數(shù)宁赤。
try_module_get()與module_put()的引入與使用與Linux 2.6內(nèi)核下的設(shè)備模型密切相關(guān)舀透。Linux 2.6內(nèi)核為不同類型的設(shè)備定義了struct module *owner域,用來指向管理此設(shè)備的模塊决左。當開始使用某個設(shè)備時愕够,內(nèi)核使用try_module_get(dev->owner)去增加管理此設(shè)備的owner模塊的使用計數(shù)走贪;當不再使用此設(shè)備時,內(nèi)核使用module_put(dev->owner)減少對管理此設(shè)備的owner模塊的使用計數(shù)惑芭。這樣坠狡,當設(shè)備在使用時,管理此設(shè)備的模塊將不能被卸載遂跟。只有當設(shè)備不再被使用時逃沿,模塊才允許被卸載。
在Linux 2.6內(nèi)核下幻锁,對于設(shè)備驅(qū)動工程師而言凯亮,很少需要親自調(diào)用try_module_get()與module_put(),因為此時開發(fā)人員所寫的驅(qū)動通常與支持某具體設(shè)備的owner模塊哄尔,對此設(shè)備owner模塊的計數(shù)管理由內(nèi)核里更底層的代碼如總線驅(qū)動或是此類設(shè)備共用的核心模塊來實現(xiàn)假消,從而簡化了設(shè)備驅(qū)動的開發(fā)。
2.7 模塊的編譯
我們可以為代碼清單1.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文件應(yīng)該與源代碼hello.c位于同一目錄岭接,開啟其中的EXTRA_CFLAGS=-g -O0可以得到包含調(diào)試信息的hello.ko模塊富拗。運行make命令得到的模塊可直接在PC上運行。
如果一個模塊包括多個.c文件(如file1.c鸣戴、file2.c)啃沪,則應(yīng)該以如下方式編寫Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o
2.8 使模塊繞開GPL
對于企業(yè)自己編寫的驅(qū)動等內(nèi)核代碼,如果不編譯為模塊則無法繞開GPL窄锅,編譯為模塊后企業(yè)在產(chǎn)品中使用模塊创千,則公司對外不再需要提供對應(yīng)的源代碼,為了使公司產(chǎn)品所使用的Linux操作系統(tǒng)支持模塊酬滤,需要完成如下工作:
- 在內(nèi)核編譯時應(yīng)該選上“可以加載模塊”签餐,嵌入式產(chǎn)品一般不需要動態(tài)卸載模塊,所以“可以卸載模塊”不用選盯串;
- 將我們編譯的內(nèi)核模塊.ko文件放在目標文件系統(tǒng)的相關(guān)目錄中氯檐;
產(chǎn)品的文件系統(tǒng)中應(yīng)該包含了支持新內(nèi)核的insmod、lsmod体捏、rmmod等工具冠摄,由于嵌入式產(chǎn)品中一般不需要建立模塊間依賴關(guān)系,所以modprobe可以不要几缭,一般也不需要卸載模塊河泳,所以rmmod也可以不要; - 在使用中用戶可使用insmod命令手動加載模塊年栓,如insmod xxx.ko拆挥;
- 但是一般而言,產(chǎn)品在啟動過程中應(yīng)該加載模塊某抓,在嵌入式產(chǎn)品Linux的啟動過程中纸兔,加載企業(yè)自己的模塊的最簡單的方法是修改啟動過程的rc腳本惰瓜,增加insmod /…/xxx.ko這樣的命令。用busybox做出的文件系統(tǒng)汉矿,通常修改/etc/init.d/rcS文件崎坊。