内核模块可以在不重新编译内核的情况下添加到内核代码中运行,其可以动态加载和卸载,因此可以根据需要将内核某些功能独立出来作为模块,而不是编译到内核中,这样可以减少内核大小,并且可以按照实际需求选择裁剪或添加某些内核功能。
需要先强调一个最基本的知识,内核模块是要运行在内核态的代码,所以编写内核模块需要包含的头文件都是内核中的头文件,使用的函数都是内核的函数。
编写一个内核模块首先要确保内核打开了CONFIG_MODULES宏,并且已经编译了内核。
需要添加头文件
编写内核模块必须要实现一个init方法和一个exit方法。由于模块是可以在内核运行过程中动态的加载和卸载的,因此,在加载模块时要指定一个init方法作为入口函数,同样的,在卸载模块的时候,exit方法可以做一些清理工作。
一个最简单的内核模块的实现如下(testm.c):
#include
static int __init xxx_init(void)
{
/* TODO: 在这个模块中你想做的事情 */
return 0;
}
static void __exit xxx_exit(void)
{
/* TODO: 卸载这个模块时需要做的清理工作 */
}
module_init(xxx_init);
module_exit(xxx_exit);
定义了init和exit方法后,要使用module_init宏来注册init方法,module_exit宏来注册exit方法。
Makefile的内容很简单:
obj-m += testm.o
[root@zhenfg workshop]# make -C /lib/modules/`uname -r`/build M=$PWD
make: Entering directory `/usr/src/kernels/2.6.18-8.el5-i686'
LD /home/ndisk/workshop/built-in.o
CC [M] /home/ndisk/workshop/testm.o
Building modules, stage 2.
MODPOST
CC /home/ndisk/workshop/testm.mod.o
LD [M] /home/ndisk/workshop/testm.ko
make: Leaving directory `/usr/src/kernels/2.6.18-8.el5-i686'
安装(将编译好的模块放到内核路径下):
[root@zhenfg workshop]# make -C /lib/modules/`uname -r`/build M=$PWD modules_install
make: Entering directory `/usr/src/kernels/2.6.18-8.el5-i686'
INSTALL /home/ndisk/workshop/testm.ko
DEPMOD 2.6.18-8.el5
make: Leaving directory `/usr/src/kernels/2.6.18-8.el5-i686'
如果需要clean工程:
[root@zhenfg workshop]# make -C /lib/modules/`uname -r`/build M=$PWD clean
make: Entering directory `/usr/src/kernels/2.6.18-8.el5-i686'
CLEAN /home/ndisk/workshop/.tmp_versions
make: Leaving directory `/usr/src/kernels/2.6.18-8.el5-i686'
如果你有一套内核源码,并且想基本该内核源码编译模块,则需要使用下面的命令make:
make -C
如果你需要交叉编译,那么还需要指定交叉编译工具链:
make -C
除了上面的各种写法,编译命令还可以这样写:
$(MAKE) -C
也可以将模块代码放到内核源码目录树中,使其随内核编译而生成内核模块,这时需要添加Kconfig配置文件让内核主动去编译这个模块的代码,例如添加配置CONFIG_TEST_MODULE,模块代码Makefile写为obj-$(CONFIG_TEST_MODULE) += testm.o,然后在内核配置文件中设置CONFIG_TEST_MODULE=m。
同时需要修改arch/
source "testmdir/Kconfig"
编译出的内核模块文件(.ko)可以在内核启动之后动态的加载到内核中。用户态可通过insmod或modprobe工具来加载模块。
insmod命令使用很简单,直接指定要加载的文件名即可:
insmod xxxx.ko
modprobe工具可以识别目标模块所依赖的模块,例如,模块A依赖于模块B,当使用modprobe加载模块A时,会自动先加载模块B。而使用insmod的话,则需要手动的先加载B后加载A。可以像下面这样使用modprobe命令:
将模块文件放到文件系统/lib/modules/`uname -r`/kernel/目录下,执行命令:
find . -name "*.ko" | depmod
这时/lib/modules/`uname -r`/modules.dep文件中就生成了各个模块之间的依赖关系(depmod命令用于查找模块之间的依赖关系),例如:
/lib/modules/2.6.18-8.el5/kernel/testone.ko: /lib/modules/2.6.18-8.el5/kernel/testtwo.ko
即testone.ko依赖于testtwo.ko,执行命令:
modprobe testone
这时testtwo.ko和testone.ko都被加载上了。
insmod和modprobe加载模块都是通过系统调用init_module进入内核内核的。他们都是用户态主动发起的。
内核源码中,很多地方通过request_module函数,试图在没有用户介入的情况下来尝试加载某些模块。内核可以通过查找模块别名来搜索自己想要加载的模块:
MODULE_ALIAS(_alias)
或者通过宏MODULE_DEVICE_TABLE(type, name)定义的设备列表中来获取模块别名。
就如同向可执行程序传参一样,在加载模块的时候也可以向模块传入参数。
例如:insmod testone.ko devid=2 svrname="xxx.cn”
在加载testone.ko的时候,同时给testone.ko传入两个参数devid和svrname。
内核中可以识别的模块参数的类型有byte, short, ushort, int, uint, long, ulong, charp, bool or invbool。如果要自定义参数类型XXX,则需要定义与之对应的param_get_XXX、param_set_XXX和param_check_XXX宏。
编写模块代码时,如果要使模块接受参数,可以使用宏module_param(name, type, perm)。例如:
static unsigned int var_int = 32;
module_param(var_int, int, 0644);
MODULE_PARM_DESC(var_int, "this is a integer param."); //对参数的描述
这样,在模块代码中定义了var_int变量,类型为int,module_param()的第三个参数perm是值定义的参数在sysfs中的可见性,如果没有开启sysfs机制,这个值是没用的。当然我们在定义参数的时候这个值必须保证(perm >=0 && perm <= 0777 && (perm & 2 ==0))。
MODULE_PARM_DESC宏用于描述定义的参数,可有可无。
我们还注意到在定义参数var_int的时候给了它一个默认值32,这样,如果在insmod模块的时候没有传入var_int这个参数,那var_int就取其默认值。
下面是模块中定义模块参数的一些例子:
定义一个字符指针:
char * myname = "default";
module_param(myname, charp, 0644);
定义一个数组:
static int namelist[8];
module_param_array(namelist, int, &num, 0644); //数组,传进来的真实长度存在num中
还可以使内部的参数名与外部的参数名有不同的名字。下面三个宏第一个参数是外部名字,第二个是内部的。
1. 如果参数是字符串或字符数组,可以用下面的写法
#define BUF_LEN 8
char species[BUF_LEN];
int spelen = 0;
module_param_string(mod_species, species, BUF_LEN, 0600); /*第三个参数是整型,表示buf的长度*/
这样的话,在用户insmod模块的时候需要传入参数名mod_species,而在模块中却是使用species这个变量接受传入的参数值。
2. 如果参数是除数组外的类型,可以用下面的写法
int couter = 0;
module_param_named(mod_couter, couter, int, 0644);
3. 如果参数是数组,可以用下面的写法
int couterarray[8];
int canum;
module_param_array_named(mod_couterarray, couterarray, int,&canum, 0600);
上述几个定义模块参数的宏都是调用module_param_call()宏来实现的,
module_param_call(name, set, get, arg, perm)
也就是说,模块参数实际上是由module_param_call()宏间接来处理的。
module_param_call()会去查找内核中定义好的set和get方法,并用这两个方法去接受和处理参数。例如,int类型对应的get/set方法为param_get_int()和param_set_int()。module_param_call()的第一个参数name为外部参数名,参数arg为内部使用的参数的指针。
那我们可以在编写模块代码时,直接使用module_param_call来接受模块参数,这样对预定义类型的参数也可以做一些合法性判断,即自定义get/set方法。例如下面定义的int型变量list_size,在传入参数的时候我想先做一些判断:int list_size __read_mostly = 0;
static int set_list_size(const char *val, struct kernel_param *kp)
{
int hr;
if ((hr = param_set_int(val, kp)) != 0)
{
printk("error, only integer is accepted!\n");
return hr;
}
if (*((int *)kp->arg) > 1024 || *((int *)kp->arg) < 4)
{
printk("ztimer_list_size should between 4-1024, we set it to default 8\n");
*((int *)kp->arg) = 8; //list_size = 8;
return 0;
}
return param_set_int(val, kp); //赋值
return 0;
}
module_param_call(ztimer_list_size, set_list_size, param_get_uint, &list_size, 0644);
在这段代码中,我自定义了set方法检查参数是否位于4~1024之间,而get方法使用内核预定义的函数。
在2.6.35以后module_param_call改为module_param_cb,所以上面的代码改为下面的写法:
static struct kernel_param_ops ztimer_param_ops = {
.set = set_list_size;
.get = param_get_uint;
};
module_param_cb(ztimer_list_size, &ztimer_param_ops, &list_size, 0644);
一个模块中定义的符号(函数或全局变量)只能在本模块中可见,其他模块则无法使用。如果要允许模块中的函数或全局变量被其他模块使用,则需要用EXPORT_SYMBOL(sym)宏导出到外部可识别的符号表中。
例如,模块A中定义了函数funcA(),在模块B中想使用这个函数,那么在编写模块A时,就要使用EXPORT_SYMBOL宏将funcA()函数导出。在模块A中代码的写法:
int funcA(void)
{
printk("funcA inmodule A.\n");
return 0;
}
EXPORT_SYMBOL(funcA);
这样在模块B就可以使用funcA()函数了。内核建议EXPORT_SYMBOL宏紧跟在函数定义后面使用。
导出符号还可以使用EXPORT_SYMBOL_GPL(sym),即被导出的符号携带GLP证书许可,那么,在模块B中想引用符号sym,则模块B必须也定义了GPL许可证,否则不能使用sym。
Linux内核的许可证为GNU GPL v2,模块使用下面的自由软件许可都是可以的:
/*
* The following licenseidents are currently accepted as indicating free
* software modules
*
* "GPL" [GNUPublic License v2 or later]
* "GPL v2" [GNUPublic License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNUPublic License v2
* or BSD license choice]
* "Dual MIT/GPL" [GNUPublic License v2
* or MIT license choice]
* "Dual MPL/GPL" [GNUPublic License v2
* or Mozilla license choice]
*/
还可以定义下面的许可来声明模块使用私有许可证:
"Proprietary"
使用私有许可的模块就不能引用使用自由软件许可的模块中的符号。
通过宏MODULE_LICENSE(_license)来定义模块使用的许可证,例如:
MODULE_LICENSE("Dual BSD/GPL");当我们确定某个模块的功能在内核中是必须要有的,可以将一个模块的代码编译进内核中,而不是单独编译出一个模块。就像2.4节介绍的编译方法,只是将CONFIG_TEST_MODULE=m改为CONFIG_TEST_MODULE=y就可以将模块代码以built-in方式编进内核。
一个模块编进内核之后,首先模块的init和exit函数的作用就有了变化,因为我们不需要手动去加载,那这时模块的init函数什么时候被执行呢?
在内核目录下的include/asm-generic/vmlinux.lds.h文件中定义了__initcall_start和__initcall_end为vmlinux中.initcall.init段的开始和结尾,按照obj-y的方式编译的模块的init函数就放在这个段里面。而在内核启动的时候回按照先后顺序执行这个段里的所有函数。
如果把模块编进内核,则下面的一系列定义就有用了,有下面的宏定义可见module_init()被定义的调用序号为6,内核镜像中,这些built-in的初始化函数按照序号从小到大顺序排列,如果相同序号的,就要人为的掌握文件的编译顺序了。当然,这些次序只在built-in的代码有效,模块不用关注这个。
#define pure_initcall(fn) __define_initcall("0",fn,0)
#define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6) //对应module_init(func)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)
另外需要注意,一个模块只能定义一个init方法。
如果要编译的模块只有一个源文件,那Makefile的写法很简单,一开始就讲到过,这样写就可以:
obj-m := testone.o
注意,出现多少个obj-m,就会生成多少个.ko文件。
如果有多个源文件,并且想只编译出一个.ko文件,就这样写(newmod.o算是一个伪对象,内核目录下的模块普遍这样写):
obj-m := newmod.o
newmod -objs := testone.o testfunc.o zdir/otherfunc.o
这样就将testone.c,testfunc.c和zdir/otherfunc.c三个文件编译成了一个模块newmod.ko。
写成newmod -y也可以:
obj-m := newmod.o
newmod -y := testone.o testfunc.o zdir/otherfunc.o
又假如我想编译出两个.ko文件,可以这样写:
obj-m := foo.o bar.o
foo-y :=
bar-y :=
这样编译之后就生成了foo.ko和bar.ko。
在内核源码目录下的Documentation/kbuild/modules.txt文件详细介绍了模块的编写和编译的方法技巧:http://blog.csdn.net/jasonchen_gbd/article/details/45009031。