在正式进行驱动开发前,需要了解有关模块编程和内核编程的一些基本概念。在本节中将会构造几个完整的(但绝对没啥功用的)模块。设置测试系统一般的发行版本都会装好内核代码树的,用过的Red Hat Enterprise Linux 5.x, Fedora 15/16, CentOS5.x系统中,其在/usr/src/kernels/$(shelluname -r)/目录下.你最好使用的内核源码树是和运行时的内核一个版本,起码初学时不会遇到微妙的问题。
2.6版本的 Linux 内核提供了用户工具来管理模块:让我们从老熟人:Hello World 模块说起:
/* hello.c */ #include <linux/init.h> #include <linux/module.h> MODULE_AUTHOR(" anonyWoo <[email protected]>"); MODULE_DESCRIPTION("To say Hello to the word"); MODULE_LICENSE("Dual BSD/GPL"); static __init int hello_init(void) { printk(KERN_ALERT "Hello, world!\n"); return 0; } static __exit void hello_exit(void) { printk(KERN_ALERT "Seeyou than, cruel world!\n"); } module_init(hello_init); module_exit(hello_exit);在module.h中定义了大量的MODULE_前缀宏,当然也包含了用于声明模块初始化及模块注销时清除函数的宏:
其中的__init就是告诉内核该函数只在初始化时使用一次,随后其占据的内存就被内核释放了。所以如果你的初始化函数还为其他的功能模块提供功能,那就别加上__init了。
与其相似的,__exit就是当模块移除时,内核会调用来移除模块。如果模块被编译进了内核或者模块不允许移移除,内核2.6用宏CONFIG_MODULE_UNLOAD来配置模块不允许被移除。那么__exit函数只是诶简单的丢弃。如果一个模块未定义一个清除函数,则模块时不可移除的。
关于__init还得说俩句,内核时怎么知道当hello_init函数初始化完成后,在哪释放她占的内存呢?这就有必要了解一下ELF(Executable and Linking Format, 可执行可链接格式)。
图 1
看见上面的objdump输出了吧:其中的.init.text和.exit.text代码段。任何加了__init和__exit前缀的函数都会被链接到相应的段中去,到时内核会自动销毁他们。
另外,你也可以进入/sys/module/hello/ sections 目录中,也会看到各个节。
且看init.h中的:
/* These macros are used to mark some functions or
* initialized data (doesn't apply to uninitialized data)
* as `initialization' functions. The kernel can take this
* as hint that the function is used only during the initialization
* phase and free up usedmemory resources after
*
* Usage:
* For functions:
*
* You should add __init immediately before the functionname, like:
*
* static void __init initme(int x, int y)
* {
* extern int z; z= x * y;
* }
#define __init __attribute__ ((__section__((".init.text")))
__attribute__是GNU C的一个扩展,主要用来声明一些特殊的属性,而section就是其中之一。上面的意思是由__init修饰的代码会被链接到到.init.text段中(section, 节)
#define __define_initcall(level,fn,id) \
static initcall_t__initcall_##fn##id__used\
__attribute__((__section__(".initcall" level ".init"))) = fn
#define device_initcall(fn) __define_initcall("6",fn,6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x)
static __init int hello_init(void)
模块到底是如何装载与卸载的呢?
我们在用户空间中:
[wang2@wuhz hello_world]$ insmod hello.ko
insmod 会调用用户空间的系统调用init_module将hello.ko二进制文件复制到内核空间中去,接着交给内核;然后进入内核到达内核函数sys_init_module(2.6.32中在linux/sysycalls.h中)。这是加载模块的主要函数,它利用非常多的其他函数完成工作。
rmmod则会使delete_module执行system call调用,而delete_module最终会进入内核,并调用sys_delete_module将hello.ko模块从内核删除。
装载细节:
init_module->sys_init_module->capable(CAP_SYS_MODULE)
->load_module->layout_and_allocate->copy_and_check
->setup_load_info
->check_modinfo
现在,我们看看加载模块时的内部函数。
当调用内核函数 sys_init_module 时,会开始一个许可检查,只有特权用户才可以执行这个操作(通过 capable 函数完成)。
然后,调用 load_module 函数,这个函数首先会为将模块分配ELF块内存,接着调用copy_from_user将hello.ko模块加载到内核并执行必要的检查。图片1中的各个节(section或叫段)共同组成了ELF;
接着会进行ELF类型type检查,架构arch检查,size检查,etc.
然后创建一些快捷变量(Convenience variables),方便随后的访问,同时检查ELF的区段头部,ELF的start address 0x00000000,这样就容易定位各个节的地址了;这期间还检查了是否开启了CONFIG_MODULE_UNLOAD标志(文章开始说过了);
还会检查version;进行版本检查,需要三个节的配合:
6.modinfo
9.__versions
11.gnu.linkonce.this_module
接受模块参数,并且更新模块状态正在加载:
mod->state= MODULE_STATE_COMING 。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配 per-CPU 块;
前面load_module分配的ELF内存时临时的,现在知道谁可以留谁得走了,就为模块分配最终的位置,并移动相应的节;
然后执行另一个分配,大小是模块必要区段所需的大小。迭代临时 ELF 块中的每个区段,并将需要执行的区段复制到新的块中。接下来要进行一些额外的维护。同时还进行符号解析,可以解析位于内核中的符号(被编译成内核映象),或临时的符号(从其他模块导出);
然后为每个剩余的区段迭代新的模块并执行重新定位(do relocations);这个步骤与架构有关,因此依赖于为架构(./linux/arch/<arch>/kernel/module.c)定义的 helper 函数;
最后,刷新指令缓存(因为使用了临时.text 区段),执行一些额外的维护(释放临时模块内存,设置系统文件,是/sys/module/hello入口项,通过mod_sysfs_init(mod);来创建的,这回在设备模型章节中我们好好学习学习),返回mod。
卸载细节:
当调用内核函数 sys_delete_module(将要删除的模块的名称作为参数传入)之后,第一步同样是确保调用方具有权限;
接下来会检查一个列表,查看是否存在依赖于这个模块的其他模块。这里有一个名为 modules_which_use_me 的列表,它包含每个依赖模块的一个元素。如果这个列表为空,就不存在任何模块依赖项,因此这个模块就是要删除的模块(否则会返回一个错误);
接下来还要测试模块是否加载。用户可以在当前安装的模块上调用 rmmod,因此这个检查确保模块已经加载;
在几个维护检查之后,倒数第二个步骤是调用模块的exit 函数(模块内部自带);
最后,调用 free_module 函数。
调用 free_module 函数之后,您将发现模块将被安全删除。该模块不存在依赖项,因此可以开始模块的内核清理过程。
首先,从安装期间添加的各种列表中(系统文件、模块列表等)删除模块;
其次,调用一个与架构相关的清理例程(可以在 ./linux/arch/<arch>/kernel/module.c 中找到);
然后,迭代具有依赖性的模块,并将这个模块从这些列表中删除;
最后,从内核的角度而言,清理已经完成,为模块分配的各种内存已被释放,包括参数内存、per-CPU 内存和模块的 ELF 内存(core 和 init)。
版本依赖:
如果出现下面情况:
[root@wuhz hello_world]# insmod hello.ko
Error inserting './hello.ko': -1 Invalidmodule format
可以使用命令
#dmesg | tail -n 20
查看一下具体的原因,一般是构建hello.ko模块的环境(内核源码树)和当前的kernel不一致造成的。
内核符号表:
你应该知道,linux系统中有非常多的优秀的简洁的小工具,一个优秀的管理元可以通过整合这些个小工具来完成超复杂的工作!就如上面的命令:
#dmesg | tail -n 20
tail接受dmesg的输出,并显示最后20行的信息!同样的各个模块之间也可以共享各自的功能,输出接口就好了,准确点是符号(symbols)。insmod会使用公共内核符号表来解析模块中未定义的符号。公共内核符号表中包含了所有的全局内核项(func和variables)的地址,这使得模块化成为可能。当然,当你的模块装载到内核的时候,他所导出的符号也会成为内核符号表的一部分供其他模块使用!
linux为我们提供一些宏方便我们导出符号以供她用:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);/* 显然由他导出的符号只能供GPL授权的模块使用 */
他们只能在模块的全局部分导出,不能在函数中导出,因为上面两个宏会被扩展成一个特殊变量的申明,而改变量必须是全局的。
用objdump看一下会多出俩个节__ksymtab_gpl和__kcrctab_gpl(checksum of somesymbols).在装载时,内核从这个节(section或叫段)中来寻找模块导出的变量。详见<linux/module.h>文件。
预备知识:
所有的模块代码中都包含下面两行代码:
#include <linux/module.h> /* 其包含了模块需要的大量符号和函数定义*/
#include <linux/init.h> /* 制定初始化和清除函数 */
也可能含有:
#include <linux/moduleparam.h> /* 可参数化装载模块 */
指定模块使用的许可证:
MODULE_LICENSE("GPL"); /* "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 MIT/GPL" [GNU Public License v2 * or MITlicense choice] * "Dual MPL/GPL" [GNU Public License v2 * orMozilla license choice] * * Thefollowing other idents are available * * "Proprietary" [Non free products] */
也有些其他以MODULE_为前缀的宏()定义在<linux/module.h>:
MODULE_AUTHOR("anonyWoo"); MODULE_DESCRIPTION("helloa.kois to say to world: Hello!"); MODULE_VERSION(_version);
模块参数:
/*helloa.c 英语注释的比较差,见谅哈 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stat.h> /* this header contains the accesspermission,e.g. S_IRUGO as showed bellow */
#include <linux/kernel.h>
#define MAXSIZE 5
MODULE_LICENSE("Dual BSD/GPL"); /*to state the lincense */
static int array[MAXSIZE] = {1, 2, 3, 4};
static char *whom = "anonyWoo";
static int howmany = 1;
/** #insmod helloa.ko howmany=25
* */
module_param(howmany, int, S_IRUGO);
/* #insmod helloa.kowhom="Miss.wong" */
module_param(whom, charp, S_IRUGO);
/** passing the parameter from the commandline has this form:
#insmod ./helloa.ko array=4,5,6 */
module_param_array(array, int, NULL,S_IRUGO);
/*static __init int hello_init(void)*/
static int hello_init(void)
{
int i, j;
for(i = 0; i < howmany; i++)
printk(KERN_ALERT "Hello,world %d time(s), hello %s\n", i + 1, whom);
for(j = 0; j <MAXSIZE; j++ )
printk(KERN_ALERT "This is%d time(s) output\n", *(array + j));
return 0;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world!\n");
}
EXPORT_SYMBOL_GPL(hello_init);
module_init(hello_init);
module_exit(hello_exit);
Makefile
#if we had define the KERNELRELEASE, for itcalls from the kernel tree,
#that we can use the buildin statement asbellow.
ifneq ($(KERNELRELEASE),)
obj-m := helloa.o
#else, call from the command line directly,
#in this case, we have to call the kernel tree
else
#这里按照ldd3的似乎不行,遂改成如下形式了
KERNELDIR ?= /usr/src/kernels/$(shell uname -r)/
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
#make
#insmod helloa.ko howmany=5 whom=”Miss.wong”array=7,8,9
#dmesg | tail -n 10
...
Hello, world 1 time(s), hello Miss.wong
Hello, world 2 time(s), hello Miss.wong
Hello, world 3 time(s), hello Miss.wong
Hello, world 4 time(s), hello Miss.wong
Hello, world 5 time(s), hello Miss.wong
This is 7 time(s) output
This is 8 time(s) output
This is 9 time(s) output
This is 4 time(s) output
#lsmod | grep helloa
helloa 1314 0
#rmmod helloa
Goodbye, cruel world!
关于module_param(variable,type, perm);
/* type 可以是bool, charp, int, invbool(inverse bool), long, short, ushort, uint,ulong, or intarray。 */
module_param_array(array_name, array_item_type, array_nump, perm );
array_nump会是用户提供的参数个数,且模块会拒绝接受超过数组大小的值,由sizeof(array[0]) , 零数组长度,动态获得其长度,指定NULL就可以了。
perm在<linux/stat.h>中定义,用于sysfs中入口项的访问掩码:eg. umask=0700,file: rwx------则相减得:---------
结束:
Ch02 is over...
To be continued...