Linux内核模块的编写方法和技巧

内核模块可以在不重新编译内核的情况下添加到内核代码中运行,其可以动态加载和卸载,因此可以根据需要将内核某些功能独立出来作为模块,而不是编译到内核中,这样可以减少内核大小,并且可以按照实际需求选择裁剪或添加某些内核功能。

1. 编写一个内核模块

需要先强调一个最基本的知识,内核模块是要运行在内核态的代码,所以编写内核模块需要包含的头文件都是内核中的头文件,使用的函数都是内核的函数。

编写一个内核模块首先要确保内核打开了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

2. 编译内核模块

写好代码后进行编译,编译内核模块的目的是生成一个.ko文件,加载模块就是加载这个.ko文件到内核中。

2.1 基于宿主机上正在运行的内核进行编译

[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'

2.2 基于内核源码进行编译

如果你有一套内核源码,并且想基本该内核源码编译模块,则需要使用下面的命令make:

make -C M=$PWD

如果你需要交叉编译,那么还需要指定交叉编译工具链:

make -C ARCH=mips M=$PWD CROSS_COMPILE=

其中ARCH参数不写也可以。

2.3 通过make工具的modules目标进行编译

除了上面的各种写法,编译命令还可以这样写:

$(MAKE) -C SUBDIRS=$(PWD)  CROSS_COMPILE=modules

即:使用SUBDIRS参数来指定模块代码目录,但这时要在末尾加上modules目标才会编译成模块。

2.4 将代码放到内核源码中进行编译

也可以将模块代码放到内核源码目录树中,使其随内核编译而生成内核模块,这时需要添加Kconfig配置文件让内核主动去编译这个模块的代码,例如添加配置CONFIG_TEST_MODULE,模块代码Makefile写为obj-$(CONFIG_TEST_MODULE) += testm.o,然后在内核配置文件中设置CONFIG_TEST_MODULE=m。

同时需要修改arch//Kconfig文件,将新添加的Kconfig文件导入到内核配置中:

source "testmdir/Kconfig"

3. 加载模块

编译出的内核模块文件(.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)定义的设备列表中来获取模块别名。

4. 向模块传递参数

就如同向可执行程序传参一样,在加载模块的时候也可以向模块传入参数。

例如: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);

5. 导出符号

一个模块中定义的符号(函数或全局变量)只能在本模块中可见,其他模块则无法使用。如果要允许模块中的函数或全局变量被其他模块使用,则需要用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");

6. 将模块代码以built-in方式编进内核

当我们确定某个模块的功能在内核中是必须要有的,可以将一个模块的代码编译进内核中,而不是单独编译出一个模块。就像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方法。

7. 编译多个源文件

如果要编译的模块只有一个源文件,那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。



你可能感兴趣的:(Linux系统,Linux编程)