Linux内核模块(.ko文件)
Linux内核模块简介
模块的基本概念
Linux内核非常庞大,包含很多组件。我们如何把需要的部分包含在内核中?
两种办法:
1)把所有需要的功能都编译进内核中。
会导致2个问题:生成的内核会非常大;为现有内核添加、删除功能,将不得不重新编译。
2)不包含所有功能,需要的时候,动态地加载代码到内核。
这种机制称为模块(Module)。
模块特点:
- 模块本身不被编译入内核映像,从而控制内核的大小。
- 模块一旦被加载,就和内核中其他部分完全一样。
Linux中,一个.ko文件就是一个模块文件,可以通过insmod/rmmod/lsmod命令,动态地加载/卸载/查看模块。
一个最简单的Linux内核模块
hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void) /* 内核模块加载函数 */
{
printk(KERN_INFO "Hello World enter\n");
return 0;
}
module_init(hello_init); /* 告诉内核hello_init是模块驱动程序的入口函数 */
static void __exit hello_exit(void) /* 内核模块卸载函数 */
{
printk(KERN_INFO "Hello World exit\n");
}
module_exit(hello_exit); /* 告诉内核hello_init是模块驱动程序的出口函数 */
MODULE_AUTHOR("Barray Song <21cnbao@gmail.com>");
MODULE_LICENSE("GPL v2"); /* 必须, 声明遵循GPL协议 */
MODULE_DESCRITION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");
包含内核模块加载函数、卸载函数,对GPL v2许可权限的声明,一些描述信息。
利用内核编译该文件,生成hello.ko模块文件夹。可通过"insmod ./hello.ko"加载驱动,"rmmod hello"卸载驱动。加载时,输出"Hello World enter",卸载时输出"Hello World exit"。
printk与printf
printk是内核模块输出函数,运行在内核空间;printf是glibc库函数,运行在用户空间。
printk可定义输出级别,printf不支持输出级别定义。
lsmod命令
lsmod命令列出已加载模块信息,实际是读取、分析"/proc/modules",可以用cat命令输出已加载模块信息
# lsmod
# cat /proc/modules
另外,内核中已经加载模块的信息也存在于/sys/module目录下。加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又有一个refcnt文件和一个sections目录。在/sys/module/hello下运行"tree -a"命令,可得到目录树形结构(前提是运行安装安装了tree软件)
modprobe命令
modprobe命令比insmod命令更强大,加载模块时,会同时加载该模块所依赖的其他模块。用modprobe命令加载的模块,如果以"modprobe -r filename"方式卸载,同时卸载依赖的模块。
模块的依赖关系存放在根文件系统的/lib/modules/
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 100ask_led.ko
filename: /mnt/100ask_led.ko
license: GPL
depends:
vermagic: 4.9.88 SMP preempt mod_unload modversions ARMv7 p2v8
Linux内核模块程序结构
一个Linux内核模块主要组成:
1)模块加载函数
如前面通过宏定义module_init声明的hello_init,就是模块加载函数。当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
2)模块卸载函数
如前面通过module_exit声明的hello_exit。当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。
3)模块许可证声明
通过MODULE_LICENSE许可证声明描述内核的许可权限,如不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。
可接受LICENSE:"GPL", "GPL v2", "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", "Proprietary"。
推荐使用"GPL"或"GPL v2"。
4)模块参数(可选)
模块参数是模块被加载时,可以传递给它的值。本身对应模块内部的全局变量。
5)模块导出符号(可选)
内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块的变量或函数。
6)模块作者等信息声明(可选)
模块加载函数
Linux内核模块加载函数通常以__init标识声明,典型模块加载函数形式:
static int __init initialization_function(void)
{
/* 初始化代码 */
}
module_init(initialization_function);
若初始化成功,则应返回0;失败时,返回错误编码,是一个负值,定于<linux/errno.h>。
Linux内核中,驱动开发人员可以通过request_module(const char* fmt, ...)加载内核模块。调用示例:
request_module(module_name);
标识__init有什么用?
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)
{
...
}
module_init(hello_init);
static void __exit hello_exit(void)
{
...
}
module_exit(hello_exit);
模块卸载函数
Linux内核模块卸载函数以__exit标识声明。典型形式:
static void __exit cleanup_function(void)
{
/* 释放代码 */
}
module_exit(cleanup_function);
模块卸载函数在模块卸载的时候执行,不返回任何值,且必须以"module_exit(函数名)"形式声明。功能与模块加载函数相反。
__exit修饰模块卸载函数,告诉内核如果相关模块被直接编译进内核,则cleanup_function函数会被省略,不编译进内核镜像。因为既然模块被内置,就不可能卸载了,卸载函数也没必要存在。
类似地,退出阶段用到的数据,可以用__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_name, int ,S_IRUGO);
装载内核模块时,用户可以向模块传递参数,形式:"insmod(或modprobe) 模块名 参数名=参数值";如果不传递,参数将使用模块内定义的缺省值。
如果模块被内置,就无法insmod,但bootloader可通过在bootargs里设置"模块名, 参数名=值"的形式给该内置的模块传递参数。
支持的类型参数:byte,short,ushort,int,uint,long,ulong,charp(字符指针),bool或invbool(布尔的反)。模块编译时,会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。
另外,模块也支持拥有参数数组,形式:"module_param_array(数组名, 数组类型, 数组长, 参数读/写权限)"
导出符号
Linux "/proc/kallsyms" 文件对应内核符号表,记录了符号及符号所在的内存地址。由于代码编译出来的模块是.ko文件,要想让本模块的符号被别的模块使用,就需要导出符号。
宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名); // 只适用于包含GPL许可权的模块
这样导出的符号就能被其他模块使用。
例子:导出整数加、减运算函数符号的内核模块
#include <linux/init.h>
#include <linux/module.h>
int add_integer(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL_GPL(add_integer); /* 导出符号 */
int sub_integer(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(sub_integer);
MODULE_LICENSE("GPL v2"); /* 导出符号 */
模块声明与描述
Linux内核模块中,可以用以下以MODULE开头几个宏做模块声明:
MODULE_AUTHOR(author); /* 模块的作者 */
MODULE_DESCRIPTION(description); /* 模块的描述 */
MODULE_VERSION(version_string); /* 模块的版本 */
MODULE_DEVICE_TABLE(table_info); /* 模块的设备表 */
MODULE_ALIAS(alternate_name); /* 模块的别名 */
模块的使用计数
Linux 2.4内核,模块自身通过MOD_INC_USE_COUNT,MOD_DEC_USE_COUNT宏来管理自己被使用的计数。
Linux 2.6以后内核,提供模块计数管理接口try_module_get(&module),module_put(&module),取代2.4的计数管理宏。
#include <linux/module.h>
/* 增加模块使用计数
* 若返回0, 表示调用失败; 返回0, 表示希望使用的模块没有被加载或正在被卸载中
*/
int try_module_get(struct module *module);
/* 减少模块使用计数
*
*/
void module_put(struct module *module);
Linux 2.6以后,内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)增加管理此设备的owner模块的使用计数;当不再使用时,内核使用module_put(dev->owner)减少使用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载;只有当设备不再被使用(计数为0)时,模块才允许被卸载。
Linux 2.6以后,对于设备驱动而言,很少需要亲自调用try_module_get()与module_put(),因为会由内核里更底层的代码来实现,从而简化设备驱动开发。
模块的编译
为最开始的简单模块hello.c编写一个简单的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命令即可得到hello.ko文件。
上面是一个模块只包含一个.c文件,如果包含多个.c文件,如file1.c,file2.c,可以用如下方式编写Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o
参考
[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.