现在,你是不是想编写内核模块。你应该懂得C语言,写过一些用户程序, 那么现在你将要见识一些真实的东西。在这里,你会看到一个野蛮的指针是如何
毁掉你的文件系统的,一次内核崩溃意味着重启动。
什么是内核模块?内核模块是一些可以让操作系统内核在需要时载入和执 行的代码,这同样意味着它可以在不需要时由操作系统卸载。它们扩展了操作系
统内核的功能却不需要重新启动系统。举例子来说,其中一种内核模块设备驱 动程序模块,它们用来让操作系统正确识别、使用安装在系统上的硬件设备。如
果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜 像来加入新的功能。这还意味着一个臃肿的内核。
你可以通过执行lsmod命令来查看内核已经加载了哪 些内核模块, 该命令通过读取/proc/modules文件的内容 来获得所需信息。
这些内核模块是如何被调入内核的?当操作系统内核需要的扩展功能不存 在时,内核模块管理守护进程kmod执行modprobe去加载内核模
块。两种类型的参数被传递给modprobe:
-
一个内核模块的名字像softdog或是ppp。
-
通用识别符像char-major-10-30。
当传递给modprobe是通用识别符时,modprobe首先在文件 /etc/modules.conf查找该字符串。如果它发现的一行别名像:
alias char-major-10-30 softdog |
它就明白通用识别符是指向内核模块softdog.o。
一个基本内核模块的Makefile
obj-m += hello-1.o
all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean |
现在你可以通过执行命令 make编译模块。更详细的文档请参考 linux/Documentation/kbuild/makefiles.txt。
请注意2.6的内核现在引入一种新的内核模块命名规范:内核模块现在使用.ko的文件后缀(代替 以往的.o后缀),这样内核模块就可以同常规的目标文件区别开。这样做的理由是它们包含一个附加的.modinfo段,
那里存放着关于模块的附加信息。我们将马上看到这些信息的好处。
使用modinfo hello-*.ko来看看它是什么样的信息。
hello-1.c
/* * hello-1.c - The simplest kernel module. */ #include /* Needed by all modules */ #include /* Needed for KERN_ALERT */
int init_module(void) { printk(KERN_INFO "Hello world 1.\n");
/* * A non 0 return means init_module failed; module can't be loaded. */ return 0; }
void cleanup_module(void) { printk(KERN_INFO "Goodbye world 1.\n"); } |
hello-2.c
/* * hello-2.c - Demonstrating the module_init() and module_exit() macros. * This is preferred over using init_module() and cleanup_module(). */ #include /* Needed by all modules */ #include /* Needed for KERN_ALERT */ #include /* Needed for the macros */
static int __init hello_2_init(void) { printk(KERN_INFO "Hello, world 2\n"); return 0; }
static void __exit hello_2_exit(void) { printk(KERN_INFO "Goodbye, world 2\n"); }
module_init(hello_2_init); module_exit(hello_2_exit); |
现在我们已经写过两个真正的模块了。添加编译另一个模块的选项十分简单,如下:
Example 2-4. 两个内核模块使用的Makefile
obj-m += hello-1.o obj-m += hello-2.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean |
现在让我们来研究一下linux/drivers/char/Makefile这个实际中的例子。就如同你看到的, 一些被编译进内核
(obj-y),但是这些obj-m哪里去了呢?对于熟悉shell脚本的人这不难理解。这些在Makefile中随处可见
的obj-$(CONFIG_FOO)的指令将会在CONFIG_FOO被设置后扩展为你熟悉的obj-y或obj-m。这其实就是你在使用 make menuconfig编译内核时生成的linux/.config中设置的东西。
这里展示了内核2.2以后引入的一个新特性。注意在负责“初始化”和“清理收尾”的函数定义处的变化。宏 __init的使用会在初始化完成后丢弃该函数并收回所占内存,如果该模块被编译进内核,而不是动态加载。
也有一个宏__initdata同__init 类似,只不过对变量有效。
宏__exit将忽略“清理收尾”的函数如果该模块被编译进内核。同宏 __init一样,对动态加载模块是无效的。这很容易理解。编译进内核的模块 是没有清理收尾工作的,
而动态加载的却需要自己完成这些工作。
这些宏在头文件linux/init.h定义,用来释放内核占用的内存。 当你在启动时看到这样的Freeing unused kernel memory: 236k freed内核输出,上面的
那些正是内核所释放的。
hello-3.c
/* * hello-3.c - Illustrating the __init, __initdata and __exit macros. */ #include /* Needed by all modules */ #include /* Needed for KERN_ALERT */ #include /* Needed for the macros */
static int hello3_data __initdata = 3;
static int __init hello_3_init(void) { printk(KERN_INFO "Hello, world %d\n", hello3_data); return 0; }
static void __exit hello_3_exit(void) { printk(KERN_INFO "Goodbye, world 3\n"); }
module_init(hello_3_init); module_exit(hello_3_exit); |
在2.4或更新的内核中,一种识别代码是否在GPL许可下发布的机制被引入, 因此人们可以在使用非公开的源代码产品时得到警告。这通过在下一章展示的宏
MODULE_LICENSE()当你设置在GPL证书下发布你的代码时,
你可以取消这些警告。这种证书机制在头文件linux/module.h 实现,同时还有一些相关文档信息。
/* * The following license idents are currently accepted as indicating free * software modules * * "GPL" [GNU Public License v2 or later] * "GPL v2" [GNU Public License v2] * "GPL and additional rights" [GNU Public License v2 rights and more] * "Dual BSD/GPL" [GNU Public License v2 * or BSD license choice] * "Dual MPL/GPL" [GNU Public License v2 * or Mozilla license choice] * * The following other idents are available * * "Proprietary" [Non free products] * * There are dual licensed components, but when running with Linux it is the * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL * is a GPL combined work. * * This exists for several reasons * 1. So modinfo can show license info for users wanting to vet their setup * is free * 2. So the community can ignore bug reports including proprietary modules * 3. So vendors can do likewise based on their own policies */ |
类似的,宏MODULE_DESCRIPTION()用来描述模块的用途。 宏MODULE_AUTHOR()用来声明模块的作者。宏MODULE_SUPPORTED_DEVICE() 声明模块支持的设备。
这些宏都在头文件linux/module.h定义,
并且内核本身并不使用这些宏。它们只是用来提供识别信息,可用工具程序像objdump查看。
作为一个练习,使用grep从目录linux/drivers看一看这些模块的作者是如何
为他们的模块提供识别信息和档案的。
我推荐在/usr/src/linux-2.6.x/目录下使用类似grep -inr MODULE_AUTHOR
*的命令。不熟悉命令行工具的人可能喜欢网上那样的方法, 搜索提供LXR做索引的内核源代码树的网站(或在自己的本地机器上安装它)。
hello-4.c
/* * hello-4.c - Demonstrates module documentation. */ #include #include #include #define DRIVER_AUTHOR "Peter Jay Salzman " #define DRIVER_DESC "A sample driver"
static int __init init_hello_4(void) { printk(KERN_INFO "Hello, world 4\n"); return 0; }
static void __exit cleanup_hello_4(void) { printk(KERN_INFO "Goodbye, world 4\n"); }
module_init(init_hello_4); module_exit(cleanup_hello_4);
/* * You can use strings, like this: */
/* * Get rid of taint message by declaring code as GPL. */ MODULE_LICENSE("GPL");
/* * Or with defines, like this: */ MODULE_AUTHOR(DRIVER_AUTHOR); /* Who wrote this module? */ MODULE_DESCRIPTION(DRIVER_DESC); /* What does this module do */
/* * This module uses /dev/testdevice. The MODULE_SUPPORTED_DEVICE macro might * be used in the future to help automatic configuration of modules, but is * currently unused other than for documentation purposes. */ MODULE_SUPPORTED_DEVICE("testdevice"); |
模块也可以从命令行获取参数。但不是通过以前你习惯的argc/argv。
要传递参数给模块,首先将获取参数值的变量声明为全局变量。然后使用宏MODULE_PARM()(在头文件linux/module.h)。运行时,insmod将给变量赋予命令行的参数,如同 ./insmod mymodule.ko myvariable=5。为使代码清晰,变量的声明和宏都应该放在
模块代码的开始部分。以下的代码范例也许将比我公认差劲的解说更好。
宏module_param()需要三个参数,变量的名字,其类型和在sysfs中关联文件的权限。
整数型既可为通常的signed也可为unsigned。
如果你想使用整数数组或者字符串,请看module_param_array()和module_param_string()。
int myint = 3; module_param(myint, int, 0); |
数组同样被支持。但是情况和2.4时代有点不一样了。为了追踪参数的个数,你需要传递一个指向数目变量的指针作为第三个参数。
在你自己,你也可以忽略数目并传递NULL。我们把两种可能性都列出来:
int myintarray[2]; module_param_array(myintarray, int, NULL, 0); /* not interested in count */ int myshortarray[4]; int count; module_parm_array(myshortarray, short, & count, 0); /* put count into "count" variable */ |
将初始值设为缺省使用的IO端口或IO寻址是一个不错的作法。如果这些变量有缺省值,则可以进行自动设备检测,
否则保持当前设置的值。我们将在后续章节解释清楚相关内容。在这里我只是演示如何向一个模块传递参数。
最后,还有这样一个宏,MODULE_PARM_DESC()被用来注解该模块可以接收的参数。该宏
两个参数:变量名和一个格式自由的对该变量的描述。
hello-5.c
/* * hello-5.c - Demonstrates command line argument passing to a module. */ #include #include #include #include #include
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Peter Jay Salzman");
static short int myshort = 1; static int myint = 420; static long int mylong = 9999; static char *mystring = "blah"; static int myintArray[2] = { -1, -1 }; static int arr_argc = 0;
/* * module_param(foo, int, 0000) * The first param is the parameters name * The second param is it's data type * The final argument is the permissions bits, * for exposing parameters in sysfs (if non-zero) at a later stage. */
module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); MODULE_PARM_DESC(myshort, "A short integer"); module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); MODULE_PARM_DESC(myint, "An integer"); module_param(mylong, long, S_IRUSR); MODULE_PARM_DESC(mylong, "A long integer"); module_param(mystring, charp, 0000); MODULE_PARM_DESC(mystring, "A character string");
/* * module_param_array(name, type, num, perm); * The first param is the parameter's (in this case the array's) name * The second param is the data type of the elements of the array * The third argument is a pointer to the variable that will store the number * of elements of the array initialized by the user at module loading time * The fourth argument is the permission bits */ module_param_array(myintArray, int, &arr_argc, 0000); MODULE_PARM_DESC(myintArray, "An array of integers");
static int __init hello_5_init(void) { int i; printk(KERN_INFO "Hello, world 5\n=============\n"); printk(KERN_INFO "myshort is a short integer: %hd\n", myshort); printk(KERN_INFO "myint is an integer: %d\n", myint); printk(KERN_INFO "mylong is a long integer: %ld\n", mylong); printk(KERN_INFO "mystring is a string: %s\n", mystring); for (i = 0; i < (sizeof myintArray / sizeof (int)); i++) { printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]); } printk(KERN_INFO "got %d arguments for myintArray.\n", arr_argc); return 0; }
static void __exit hello_5_exit(void) { printk(KERN_INFO "Goodbye, world 5\n"); }
module_init(hello_5_init); module_exit(hello_5_exit); |
我建议用下面的方法实验你的模块:
satan# insmod hello-5.ko mystring="bebop" mybyte=255 myintArray=-1 mybyte is an 8 bit integer: 255 myshort is a short integer: 1 myint is an integer: 20 mylong is a long integer: 9999 mystring is a string: bebop myintArray is -1 and 420 satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mystring="supercalifragilisticexpialidocious" \ > mybyte=256 myintArray=-1,-1 mybyte is an 8 bit integer: 0 myshort is a short integer: 1 myint is an integer: 20 mylong is a long integer: 9999 mystring is a string: supercalifragilisticexpialidocious myintArray is -1 and -1 satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mylong=hello hello-5.o: invalid argument syntax for mylong: 'h' |
有时将模块的源代码分为几个文件是一个明智的选择。
这里是这样的一个模块范例。
Example 2-8. start.c
/* * start.c - Illustration of multi filed modules */
#include /* We're doing kernel work */ #include /* Specifically, a module */
int init_module(void) { printk(KERN_INFO "Hello, world - this is the kernel speaking\n"); return 0; } |
另一个文件:
Example 2-9. stop.c
/* * stop.c - Illustration of multi filed modules */
#include /* We're doing kernel work */ #include /* Specifically, a module */
void cleanup_module() { printk(KERN_INFO "Short is the life of a kernel module\n"); } |
最后是该模块的Makefile:
Example 2-10. Makefile
obj-m += hello-1.o obj-m += hello-2.o obj-m += hello-3.o obj-m += hello-4.o obj-m += hello-5.o obj-m += startstop.o startstop-objs := start.o stop.o
all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean |
这是目前为止所有例子的完整的Makefile。前五行没有什么特别之处,但是最后一个例子需要两行。
首先,我们为联合的目标文件构造一个名字,其次,我们告诉make什么目标文件是模块的一部分。
很显然,我们强烈推荐你编译一个新的内核,这样你就可以打开内核中一些有用的排错功能,像强制卸载模块(MODULE_FORCE_UNLOAD): 当该选项被打开时,你可以rmmod -f
module强制内核卸载一个模块,即使内核认为这是不安全的。该选项可以为你节省不少开发时间。
但是,你仍然有许多使用一个正在运行中的已编译的内核的理由。例如,你没有编译和安装新内核的权限,或者你不希望重启你的机器来运行新内核。
如果你可以毫无阻碍的编译和使用一个新的内核,你可以跳过剩下的内容,权当是一个脚注。
如果你仅仅是安装了一个新的内核代码树并用它来编译你的模块,当你加载你的模块时,你很可能会得到下面的错误提示:
insmod: error inserting 'poet_atkm.ko': -1 Invalid module format |
一些不那么神秘的信息被纪录在文件/var/log/messages中;
Jun 4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686 REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3' |
换句话说,内核拒绝加载你的模块因为记载版本号的字符串不符(更确切的说是版本印戳)。版本印戳作为一个静态的字符串存在于内核模块中,以 vermagic:。 版本信息是在连接阶段从文件init/vermagic.o中获得的。 查看版本印戳和其它在模块中的一些字符信息,可以使用下面的命令 modinfo module.ko:
[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko license: GPL author: Peter Jay Salzman description: A sample driver vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3 depends:
|
我们可以借助选项--force-vermagic解决该问题,但这种方法有潜在的危险,所以在成熟的模块中也是不可接受的。
解决方法是我们构建一个同我们预先编译好的内核完全相同的编译环境。如何具体实现将是该章后面的内容。
首先,准备同你目前的内核版本完全一致的内核代码树。然后,找到你的当前内核的编译配置文件。通常它可以在路径 /boot下找到,使用像config-2.6.x的文件名。你可以直接将它拷贝到内核代码树的路径下: cp /boot/config-`uname -r` /usr/src/linux-`uname
-r`/.config。
让我们再次注意一下先前的错误信息:仔细看的话你会发现,即使使用完全相同的配置文件,版本印戳还是有细小的差异的,但这足以导致
模块加载的失败。这其中的差异就是在模块中出现却不在内核中出现的custom字符串,是由某些发行版提供的修改过的
makefile导致的。检查/usr/src/linux/Makefile,确保下面这些特定的版本信息同你使用的内核完全一致:
VERSION = 2 PATCHLEVEL = 6 SUBLEVEL = 5 EXTRAVERSION = -1.358custom ... |
像上面的情况你就需要将EXTRAVERSION一项改为-1.358。我们的建议是将原始的makefile备份在 /lib/modules/2.6.5-1.358/build下。 一个简单的命令cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname
-r`即可。 另外,如果你已经在运行一个由上面的错误的Makefile编译的内核,你应该重新执行 make,或直接对应/lib/modules/2.6.x/build/include/linux/version.h从文件 /usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆盖后者的。
现在,请执行make来更新设置和版本相关的头文件,目标文件:
[root@pcsenonsrv linux-2.6.x]# make CHK include/linux/version.h UPD include/linux/version.h SYMLINK include/asm -> include/asm-i386 SPLIT include/linux/autoconf.h -> include/config/* HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/split-include HOSTCC scripts/basic/docproc HOSTCC scripts/conmakehash HOSTCC scripts/kallsyms CC scripts/empty.o ... |
如果你不是确实想编译一个内核,你可以在SPLIT后通过按下CTRL-C中止编译过程。因为此时你需要的文件
已经就绪了。现在你可以返回你的模块目录然后编译加载它:此时模块将完全针对你的当前内核编译,加载时也不会由任何错误提示。