Linux内核模块简介
Linux内核的整体结构已经非常庞大,而其包含的组件也非常多。我们怎样把需要的部分都包含在内核中呢?
一种方法是把所有需要的功能都编译到Linux内核。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。
有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中呢?
Linux提供了这样的一种机制,这种机制被称为模块(Module)。模块具有这样的特点。
- 模块本身不被编译入内核映像,从而控制了内核的大小。
- 模块一旦被加载,它就和内核中的其他部分完全一样。
为了使读者建立对模块的初步感性认识,我们先来看一个最简单的内核模块“Hello World”,如代码清单4.1所示。
代码清单4.1 一个最简单的Linux内核模块
01 /* 02 * a simple kernel module: hello 03 * 04 * Copyright (C) 2014 Barry Song ([email protected]) 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 <[email protected]>"); 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”得到如下目录树:
[email protected]:/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工具生成的,它的格式非常简单:
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 <[email protected]> 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)模块作者等信息声明(可选)。
模块加载函数
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);
这种灵活的方式加载其他内核模块。
在Linux中,所有标识为_ _init的函数如果直接编译进入内核,成为内核镜像的一部分,在连接的时候都放在.init.text这个区段内。
#define _ _init _ _attribute_ _ ((_ _section_ _ (".init.text")))
所有的_ _init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些_ _init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等)的内存。
除了函数以外,数据也可以被定义为_ _initdata,对于只是初始化阶段需要的数据,内核在初始化完后,也可以释放它们占用的内存。例如,下面的代码中将hello_data定义为__initdata。
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来形容。
模块参数
我们可以用“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 <[email protected]>"); 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模块的参数:
[email protected]:/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许可权。
Linux内核模块编程与内核模块LICENSE -《详解(第3版)》预读,布布扣,bubuko.com