Linux内核模块编程(一)
简介
模块(module)是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是可以装载到系统中作为内核的一部分运行,从而可以动态扩充内核的功能。模块最主要的用处就是用来实现设备驱动程序。
使用模块的优点:
l将来修改内核时,不必全部重新编译整个内核,可节省不少时间
2系统中如果需要使用新模块,不必重新编译内核,只要插入相应的模块即可
模块的使用方式:(modules-1.3.57.tar.gz)
命令 功能 备注
modprobe symbolic link to modprobe
depmod 造 module dependency file,以告诉将来的 insmod 要去哪抓 modules 来插。 这个 dependency file 就在/lib/modules/[您的kernel版本]/modules.dep
insmod 把某个 module 插入 kernel 中
rmmod 把某个没在用的 module 拔出 kernel symbolic link to insmod
lsmod 把现在 kernel 中插入的 modules 列出来a shell script in 1.3.69f
ksyms symbolic link to insmod
kerneld 一个 daemon,可负责自动呼叫 insmod 插入 module,是一个很方便的 daemon。它也同时查看,若某 module 插入後但很久没人在用了,就会把它 拔出来,以省记忆体。
相关文件:
/etc/rc.d/ rc.modules
/etc/rc.d/rc.S or /etc/rc.d/rc.syinit
/etc/conf.modules or /etc/modules.conf
模块的装载/卸载:
静态:在系统启动时就装载
动态:使用insmod等命令在系统运行过程中装载
注:
1.现在kerneld已经被kmod所取代,原因主要是kerneld是使用System V IPC,经过了中间层,比较复杂。
2.需要超级用户权限。
3.形式:
Module: #pages: Used by:
msdos 5 1
vfat 4 1 (autoclean)
fat 6 [vfat msdos] 2 (autoclean)
工作原理
摘要:
内核符号表
模块装载
模块卸载
多个模块间的依赖关系。
Kernel 里有一个变量叫 module_list,每当 user 将一个 module 载到 kernel 里的时候,这个 module 就会被记录在 module_list 里面。当 kernel 要使用到这个 module 提供的 function 时,它就会去 search 这个 list,找到 module,然后再使用其提供的 function 或 variable。每一个 module 都可以 export 一些 function 或变量来让别人使用。除此之外,module 也可以使用已经载到 kernel 里的 module 提供的 function。这种情形叫做 module stack。比方说,module A 用到 module B 的东西,那在加载 module A 之前必须要先加载 module B。否则 module A 会无法加载。除了 module 会 export 东西之外,kernel 本身也会 export 一些 function 或 variable。同样的,module 也可以使用 kernel 所 export 出来的东西。由于大家平时都是撰写 user space 的程序,所以,当突然去写 module 的时候,会把平时写程序用的 function 拿到 module 里使用。像是 printf 之类的东西。我要告诉各位的是,module 所使用的 function 或 variable,要嘛就是自己写在 module 里,要嘛就是别的 module 提供的,再不就是 kernel 所提供的。你不能使用一般 libc 或 glibc所提供的 function。像 printf 之类的东西。这一点可能是各位要多小心的地方。(也许你可以先 link 好,再载到 kernel,我好象试过,但是忘了)。
Linux核心是一种monolithic类型的内核,即单一的大程序,核心中所有的功能部件都可以对其全部内部数据结构和例程进行访问。核心的另外一种形式是微内核结构,此时核心的所有功能部件都被拆成独立部分, 这些部分之间通过严格的通讯机制进行联系。这样通过配置进程将新部件加入核心的方式非常耗时。比如说我们想为一个NCR 810 SCSI卡配置SCSI驱动,但是核心中没有这个部分。那么我们必须重新配置并重构核心。 Linux可以让我们可以随意动态的加载与卸载操作系统部件。Linux模块就是这样一种可在系统启动后的任何时候动态连入核心的代码块。当我们不再需要它时又可以将它从核心中卸载并删除。Linux模块多指设备驱动、伪设备驱动, 如网络设备和文件系统。
Linux为我们提供了两个命令:使用insmod来显式加载核心模块,使用rmmod来卸载模块。同时核心自身也可以请求核心后台进程kerneld来加载与卸载模块。
动态可加载代码的好处在于可以让核心保持很小的尺寸同时非常灵活。在我的Intel系统中由于使用了模块,整个核心仅为406K字节长。由于我只是偶尔使用VFAT文件系统, 所以我将Linux核心构造成当mount VFAT分区时自动加载VFAT文件系统模块。当我卸载VFAT分区时系统将检测到我不再需要VFAT文件系统模块,将把它从系统中卸载。模块同时还可以让我们无需重构核心并频繁重新启动来尝试运行新核心代码。尽管使用模块很自由,但是也有可能同时带来与核心模块相关的性能与内存损失。可加载模块的代码一般有些长并且额外的数据结构可能会占据一些内存。同时对核心资源的间接使用可能带来一些效率问题。
一旦Linux模块被加载则它和普通核心代码一样都是核心的一部分。它们具有与其他核心代码相同的权限与职 责;换句话说Linux核心模块可以象所有核心代码和设备驱动一样使核心崩溃。
模块为了使用所需核心资源所以必须能够找到它们。例如模块需要调用核心内存分配例程kmalloc()来分配 内存。模块在构造时并不知道kmalloc()在内存中何处,这样核心必须在使用这些模块前修改模块中对 kmalloc()的引用地址。核心在其核心符号表中维护着一个核心资源链表这样当加载模块时它能够解析出模块 中对核心资源的引用。Linux还允许存在模块堆栈,它在模块之间相互调用时使用。例如VFAT文件系统模块 可能需要FAT文件系统模块的服务,因为VFAT文件系统多少是从FAT文件系统中扩展而来。某个模块对其他模 块的服务或资源的需求类似于模块对核心本身资源或服务的请求。不过此时所请求的服务是来自另外一个事先 已加载的模块。每当加载模块时,核心将把新近加载模块输出的所有资源和符号添加到核心符号表中。
当试图卸载某个模块时,核心需要知道此模块是否已经没有被使用,同时它需要有种方法来通知此将卸载模块。 模块必须能够在从核心种删除之前释放其分配的所有系统资源,如核心内存或中断。当模块被卸载时,核心将从核心符号表中删除所有与之对应的符号。
可加载模块具有使操作系统崩溃的能力,而编写较差的模块会带来另外一种问题。当你在一个或早或迟构造的核心而不是当前你运行的核心上加载模块时将会出现什么结果?一种可能的情况是模块将调用具有错误参数的核心例程。核心应该使用严格的版本控制来对加载模块进行检查以防止这种这些情况的发生。
1 模块的加载
图1 核心模块链表
核心模块的加载方式有两种。首先一种是使用insmod命令手工加载模块。另外一种则是在需要时加载模块;我们称它为请求加载。当核心发现有必要加载某个模块时,如用户安装了核心中不存在的文件系统时,核心将请求核心后台进程(kerneld)准备加载适当的模块。这个核心后台进程仅仅是一个带有超级用户权限的普通用户进程。当系统启动时它也被启动并为核心打开了一个进程间通讯(IPC)通道。核心需要执行各种任务时用它来向kerneld发送消息。
kerneld的主要功能是加载和卸载核心模块, 但是它还可以执行其他任务, 如通过串行线路建立PPP连接并在适当时候关闭它。kerneld自身并不执行这些任务,它通过某些程序如insmod来做此工作。它只是核心的代理,为核心进行调度。
insmod程序必须找到要求加载的核心模块。请求加载核心模块一般被保存在/lib/modules/kernel-version 中。这些核心模块和系统中其他程序一样是已连接的目标文件,但是它们被连接成可重定位映象。即映象没有被连接到在特定地址上运行。这些核心模块可以是a.out或ELF文件格式。insmod将执行一个特权级系统调用来找到核心的输出符号。这些都以符号名以及数值形式,如地址值成对保存。核心输出符号表被保存在核心维护的模块链表的第一个module结构中,同时module_list指针指向此结构。只有特殊符号被添加到此表中,它们在核心编译与连接时确定,不是核心每个符号都被输出到其模块中。例如设备驱动为了控制某个特定系统中断而由核心例程调用的"request_irq"符号。在我的系统中,其值为0x0010cd30。我们可以通过使用ksyms工具或者查看/proc/ksyms来观看当前核心输出符号。ksyms工具既可以显示所有核心输出符号也可以只显示那些已加载模块的符号。insmod将模块读入虚拟内存并通过使用来自核心输出符号来修改其未解析的核心例程和资源的引用地址。这些修改工作采取由insmod程序直接将符号的地址写入模块中相应地址来修改内存中的模块映象。
当insmod修改完模块对核心输出符号的引用后,它将再次使用特权级系统调用来申请足够的空间来容纳新核 心。核心将为其分配一个新的module结构以及足够的核心内存来保存新模块, 并将它放到核心模块链表的尾部。 然后将其新模块标志为UNINITIALIZED。
图1给出了一个加载两个模块:VFAT和FAT后的核心链表示意图。不过图中没有画出链表中的第一个模块: 用来存放核心输出符号表的一个伪模块。lsmod可以帮助我们列出系统中所有已加载的核心模块以及相互间 依赖关系。它是通过重新格式化从核心module结构中建立的/proc/modules来进行这项工作的。核心为其分配的内存被映射到insmod的地址空间, 这样它就能访问核心空间。insmod将模块拷贝到已分配空间中, 如果为它分配的核心内存已用完,则它将再次申请。不过不要指望多次将加载模块到相同地址,更不用说在两个不同 Linux系统的相同位置。另外此重定位工作包括使用适当地址来修改模块映象。
这个新模块也希望将其符号输出到核心中,insmod将为其构造输出符号映象表。每个核心模块必须包含模块 初始化和模块清除例程,它们的符号被设计成故意不输出, 但是insmod必须知道这些地址, 这样它可以将它们传递给核心。所有这些工作做完之后,insmod将调用初始化代码并执行一个特权级系统调用将模块的初始化与清除例程地址传递给核心。
当将一个新模块加载到核心中间时,核心必须更新其符号表并修改那些被新模块使用的老模块。那些依赖于其他模块的模块必须维护在其符号表尾部维护一个引用链表并在其module数据结构中指向它。图12.1中VFAT 依赖于FAT文件系统模块。所以FAT模块包含一个对VFAT模块的引用;这个引用在加载VFAT模块时添加。核心调用模块的初始化例程,如果成功它将安装此模块。模块的清除例程地址被存储在其module结构中,它将在 模块卸载时由核心调用。最后模块的状态被设置成RUNNING。
2 模块的卸载
模块可以通过使用rmmod命令来删除, 但是请求加载模块将被kerneld在其使用记数为0时自动从系统中删除。 kerneld在其每次idle定时器到期时都执行一个系统调用以将系统中所有不再使用的请求加载模块从系统中 删除。这个定时器的值在启动kerneld时设置;我系统上的值为180秒。这样如果你安装一个iso9660 CDROM并且你的iso9660文件系统是一个可加载模块, 则在卸载CD ROM后的很短时间内此iso9660模块将从核心中删除。
如果核心中的其他部分还在使用某个模块, 则此模块不能被卸载。例如如果你的系统中安装了多个VFAT文件系统则你将不能卸载VFAT模块。执行lsmod我们将看到每个模块的引用记数。如:
Module: #pages: Used by:
msdos 5 1
vfat 4 1 (autoclean)
fat 6 [vfat msdos] 2 (autoclean)
此记数表示依赖此模块的核心实体个数。在上例中VFAT和msdos模块都依赖于fat模块, 所以fat模块的引用记数为2。vfat和msdos模块的引用记数都为1,表示各有一个已安装文件系统。如果我们安装另一个VFAT文件系统则vfat模块的引用记数将为2。模块的引用记数被保存在其映象的第一个长字中。这个字同时还包含AUTOCLEAN和VISITED标志。请求加载模块使用这两个标志域。如果模块被标记成AUTOCLEAN则核心知道此模 块可以自动卸载。VISITED标志表示此模块正被一个或多个文件系统部分使用;只要有其他部分使用此模块则这个标志被置位。每次系统被kerneld要求将没有谁使用的请求模块删除时,核心将在所有模块中扫描可能的候选者。但是一般只查看那些被标志成AUTOCLEAN并处于RUNNING状态的模块。如果某模块的VISITED 标记被清除则它将被删除出去。如果某模块可以卸载,则可以调用其清除例程来释放掉分配给它的核心资源。它所对应的module结构将被标记成DELETED并从核心模块链表中断开。其他依赖于它的模块将修改它们各自的引用域来表示它们间的依赖关系不复存在。此模块需要的核心内存都将被回收。
编程实现
模块的组织结构
2.0/2.2
至少需要两个函数:init_module()和cleanup_module()。一般在init_module()完成初始化工作,例如内存分配(kmalloc);在cleanup_module()中完成回收工作。
/* The necessary header files */
/* Standard in kernel modules */
#include /* We
e doing kernel work */
#include /* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS //这两行应该修改为#ifdef MODVERSIONS
#include
#endif
/* Initialize the module */
int init_module()
{
Printk("Hello, world - this is the kernel speakingn");
/* If we return a non zero value, it means that
* init_module failed and the kernel module
* can be loaded */
Return 0;
}
/* Cleanup - undid whatever init_module did */
void cleanup_module()
{
Printk("Short is the life of a kernel modulen");
}
2.3.*/2.4.*中的用法:
2.3/2.4中的用法不同,应该使用:
module_init(init_proc_fs)
module_exit(exit_proc_fs)
优点:有了返回值。
static int __init init_proc_fs(void)
{
int err = register_filesystem(&proc_fs_type);
if (!err) {
proc_mnt = kern_mount(&proc_fs_type);
err = PTR_ERR(proc_mnt);
if (IS_ERR(proc_mnt))
unregister_filesystem(&proc_fs_type);
Else
err = 0;
}
return err;
}
static void __exit exit_proc_fs(void)
{
unregister_filesystem(&proc_fs_type);
kern_umount(proc_mnt);
}
module_init(init_proc_fs)
module_exit(exit_proc_fs)
模块的Makefile
Options:
-D__KERNEL__ // in kernel space
-DMODULE // create module
-DLINUX // which can be compiled on more than one operating system
-D__SMP__ // Symmetrical MultiProcessing
-DCONFIG_MODVERSIONS // should include /usr/include/linux/modversions.h
fomit-frame-pointer告诉gcc不要为那些不需要的函数保存页面指针.这会使得我们的寄存器在调用init_module以后保持不变.
多个源程序组成的模块的Makefile
1.在除了一个以外的所有源文件中,增加一行#define __NO_VERSION__。这是很重要的,因为module.h一般包括kernel_version的定义,这是一个全局变量,包含模块编译的内核版本。如果你需要version.h,你需要把自己把它包含进去,因为如果有__NO_VERSION__的话module.h不会自动包含。
2.象通常一样编译源文件。
3.把所有目标文件联编成一个。在X86下,用ld –m elf_i386 –r –o .o <1st source file>
printk
printk是内核空间中printf的替代品,其用法为:printk("format string", var)。n定义了日志等级,范围从0到7,其中0到4是紧急事件,5和6是普通信息,7为调试(例如SMP_DEBUG)。其它用户空间的函数也不能使用;程序员在内核空间中可以使用的函数都记录在/proc/ksyms中,也可以使用ksyms -a命令查看。
注:在终端下insmod模块,printk信息送往终端
By the way, the reason why the Makefile recommends against doing insmod from X is because when the kernel has a message to print with printk, it sends it to the console. When you don use X, it just goes to the virtual terminal you
e using (the one you chose with Alt-F) and you see it. When you do use X, on the other hand, there are two possibilities. Either you have a console open with xterm -C, in which case the output will be sent there, or you don , in which case the output will go to virtual terminal 7 -- the one `covered by X.
函数/变量和内核版本的关系
刚才我们说到 kernel 本身会 export 出一些 function 或 variable 来让 module 使用,但是,我们不是万能的,我们怎幺知道 kernel 有开放那里东西让我们使用呢 ? Linux 提供一个 command,叫 ksyms,你只要执行 ksyms -a 就可以知道 kernel 或目前载到 kernel 里的 module 提供了那些 function 或 variable。底下是我的系统的情形:
c0216ba0 drive_info_R744aa133
c01e4a44 boot_cpu_data_R660bd466
c01e4ac0 EISA_bus_R7413793a
c01e4ac4 MCA_bus_Rf48a2c4c
c010cc34 __verify_write_R203afbeb
. . . . .
在 kernel 里,有一个 symbol table 是用来记录 export 出去的 function 或 variable。除此之外,也会记录着那个 module export 那些 function。上面几行中,表示 kernel 提供了 drive_info 这个 function/variable。所以,我们可以在 kernel 里直接使用它,等载到 kernel 里时,会自动做好 link 的动作。由此,我们可以知道,module 本身其实是还没做 link 的一些 object code。一切都要等到 module 被加载 kernel 之后,link 才会完成。各位应该可以看到 drive_info 后面还接着一些奇怪的字符串。_R744aa133,这个字符串是根据目前 kernel 的版本再做些 encode 得出来的结果。为什幺额外需要这一个字符串呢 ?
Linux 不知道从那个版本以来,就多了一个 config 的选项,叫做 Set version number in symbols of module。这是为了避免对系统造成不稳定。我们知道 Linux 的 kernel 更新的很快。在 kernel 更新的过程,有时为了效率起见,会对某些旧有的 data structure 或 function 做些改变,而且一变可能有的 variable 被拿掉,有的 function 的 prototype 跟原来的都不太一样。如果这种情形发生的时候,那可能以前 2.0.33 版本的 module 拿到 2.2.1 版本的 kernel 使用,假设原来 module 使用了 2.0.33 kernel 提供的变量叫 A,但是到了 2.2.1 由于某些原因必须把 A 都设成 NULL。那当此 module 用在 2.2.1 kernel 上时,如果它没去检查 A 的值就直接使用的话,就会造成系统的错误。也许不会整个系统都死掉,但是这个 module 肯定是很难发挥它的功能。为了这个原因,Linux 就在 compile module 时,把 kernel 版本的号码 encode 到各个 exported function 和 variable 里。
所以,刚才也许我们不应该讲 kernel 提供了 drive_info,而应该说 kernel 提供了 driver_info_R744aa133 来让我们使用。这样也许各位会比较明白。也就是说,kernel 认为它提供的 driver_info_R744aa133 这个东西,而不是 driver_info。所以,我们可以发现有的人在加载 module 时,系统都一直告诉你某个 function 无法 resolved。这就是因为 kernel 里没有你要的 function,要不然就是你的 module 里使用的 function 跟 kernel encode 的结果不一样。所以无法 resolve。解决方式,要嘛就是将 kernel 里的 set version 选项关掉,要嘛就是将 module compile 成 kernel 有办法接受的型式。
那有人就会想说,如果 kernel 认定它提供的 function 名字叫做 driver_info_R744aa133 的话,那我们写程序时,是不是用到这个 funnction 的地方都改成 driver_info_R744aa133 就可以了。答案是 Yes。但是,如果每个 function 都要你这样写,你不会觉得很烦吗 ? 比方说,我们在写 driver 时,很多人都会用到 printk 这个 function。这是 kernel 所提供的 function。它的功能跟 printf 很像。用法也几乎都一样。是 debug 时很好用的东西。如果我们 module 里用了一百次 printk,那是不是我们也要打一百次的 printk_Rdd132261 呢 ? 当然不是,聪明的人马上会想到用 #define printk printk_Rdd132261 就好了嘛。所以啰,Linux 很体贴的帮我们做了这件事。
如果各位的系统有将set version的选项打开的话,那大家可以到/usr/src/linux/include/linux/modules这个目录底下。这个目录底下有所多的..ver档案。这些档案其实就是用来做#define用的。我们来看看ksyms.ver 这个档案里,里面有一行是这样子的 :
#define printk _set_ver(printk)
set_ver是一个macro,就是用来在printk后面加上version number的。有兴趣的朋友可以自行去观看这个macro的写法。用了这些ver檔,我们就可以在module里直接使用printk这样的名字了。而这些ver档会自动帮我们做好#define的动作。可是,我们可以发现这个目录有很多很多的ver檔。有时候,我们怎幺知道我们要呼叫的function是在那个ver档里有定义呢?Linux又帮我们做了一件事。/usr/src/linux/include/linux/modversions.h这个档案已经将全部的ver档都加进来了。所以在我们的module里只要include这个档,那名字的问题都解决了。但是,在此,我们奉劝各位一件事,不要将modversions.h这个档在module里include进来,如果真的要,那也要加上以下数行:
#ifdef MODVERSIONS
#include linux/modversions.h
#endif
加入这三行的原因是,避免这个 module 在没有设定 kernel version 的系统上,将 modversions.h 这个档案 include 进来。各位可以去试试看,当你把 set version 的选项关掉时,modversions.h 和 modules 这个目录都会不见。如果没有上面三行,那 compile 就不会过关。所以一般来讲,modversions.h 我们会选择在 compile 时传给 gcc 使用。就像下面这个样子。
gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS main.c
-include usr/src/linux/include/linux/modversions.h
在这个 command line 里,我们看到了 -D__KERNEL__,这是说要定义 __KERNEL__ 这个 constant。很多跟 kernel 有关的 header file,都必须要定义这个 constant 才能 include 的。所以建议你最好将它定义起来。另外还有一个 -DMODVERSIONS。这个 constant 我刚才忘了讲。刚才我们说要解决 fucntion 或 variable 名字 encode 的方式就是要 include modversions.h,其实除此之外,你还必须定义 MODVERSIONS 这个 constant。再来就是 MODULE 这个 constant。其实,只要是你要写 module 就一定要定义这个变量。而且你还要 include module.h 这个档案,因为 _set_ver 就是定义在这里的。
讲到这里,相信各位应该对 module 有一些认识了,以后遇到 module unresolved 应该不会感到困惑了,应该也有办法解决了。
发布人:netbull 来自:奥索网