您的位置:首页 > 其它

LDD3笔记:第二章 构造和运行模块

2011-11-27 11:21 204 查看
在正式进行驱动开发前,需要了解有关模块编程和内核编程的一些基本概念。在本节中将会构造几个完整的(但绝对没啥功用的)模块。设置测试系统一般的发行版本都会装好内核代码树的,用过的Red Hat Enterprise Linux 5.x, Fedora 15/16, CentOS5.x系统中,其在/usr/src/kernels/$(shelluname -r)/目录下.你最好使用的内核源码树是和运行时的内核一个版本,起码初学时不会遇到微妙的问题。

2.6版本的 Linux 内核提供了用户工具来管理模块:

lsmod: 查看mod

modprobe:insmod 和 rmmod 的包装

depmod:用于创建模块依赖项

modinfo:用于为模块宏查找值,查看模块信息

让我们从老熟人:Hello World 模块说起:

/* hello.c */
#include <linux/init.h>
#include <linux/module.h>
MODULE_AUTHOR(" anonyWoo <wuhz2007@gmail.com>");
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_前缀宏,当然也包含了用于声明模块初始化及模块注销时清除函数的宏:

module_init();

module_exit();

模块初始化函数:

static __init int hello_init(void)

其中的__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, 节)

在init.h中你会看到如下定义:

#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)

module_init(hello_init) /* 就是将hello_init指针放到到 .initcall6.init 子节中了

* module_init会被do_initcall调用来初始化 */

模块到底是如何装载与卸载的呢?

我们在用户空间中:

[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...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: