Chinaunix首页 | 论坛 | 博客
  • 博客访问: 731386
  • 博文数量: 130
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 2198
  • 用 户 组: 普通用户
  • 注册时间: 2011-11-29 12:48
个人简介

每一个“丑得人神共愤”的泡妞高 手都有一颗坚忍的心,这证明了人类 在绝境中毫不妥协的求生精神,反正丑都丑了,索性放开手脚大干一场,这就叫“无产阶级失去的是锁链,得到的是全世界”

文章分类

全部博文(130)

文章存档

2013年(130)

我的朋友

分类: LINUX

2013-04-17 19:08:20

构造和运行内核模块

内核模块简介

内核模块是具有独立功能的程序,它可以被单独编译,但是不能单独运行,它的运行必须被链接到内核作为内核的一部分在内核空间中运行。模块编程和内核版本密切相连,因为不同的内核版本中某些函数的函数名会有变化,因此模块编程也可以说是内核编程。内核模块本身不被编译进内核映像,从而控制了内核的大小,但模块一旦被加载,就和内核中的其他部分完全一样。 

本文就一个简单的hello模块进行分析,为大家讲解内核模块开发的一些基本知识

模块源码

PS:由于chinaunix在线编辑器的原因,以下代码复制保存后需做相应的格式调整才可正常使用,需要代码的同学可点击此处下载全部源码

    hello.c

点击(此处)折叠或打开

  1. #include <linux/init.h>
  2. #include <linux/module.h>
  3. #include <linux/kernel.h>
  4. #include <linux/moduleparam.h>

  5. static char *whom = "world";
  6. static int howmany = 1;
  7. module_param(howmany, int, S_IRUGO);
  8. module_param(whom, charp, S_IRUGO);

  9. static int __init hello_init(void)
  10. {
  11.     int i;

  12.     for (i=0; i<howmany; i++){
  13.         printk(KERN_ALERT "Hello, %s\n", whom);
  14.     }
  15.     return 0;
  16. }

  17. static void __exit hello_exit(void)
  18. {
  19.     printk(KERN_ALERT "Goodbye\n");
  20. }

  21. module_init(hello_init);
  22. module_exit(hello_exit);

  23. MODULE_LICENSE("Dual BSD/GPL");
  24. MODULE_AUTHOR("WuHuaXu");
  25. MODULE_DESCRIPTION("Just for fun");

    Makefile

点击(此处)折叠或打开

  1. ifneq ($(KERNELRELEASE),)
  2. #call from kernel build system
  3.     obj-m := hello.o
  4. else
  5.     KERNELDIR ?= /lib/modules/$(shell uname -r)/build
  6.     PWD := $(shell pwd)
  7. default:
  8.     $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
  9. endif

  10. clean:
  11.     rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers

模块编译

编译使用make命令即可,许多同学第一次看到这个Makefile时可能对这些语句很不理解,这是很正常的,因为内核有其特殊的构造系统,编译外部内核模块的时候需要和内核构造系统进行连接。上面的代码执行流程如下:make读取当前目录下的Makefile文件,发现KERNELRELEASE为定义就去执行else部分,切换到KERNELDIR目录下,读取下面的Makefile文件(这是-C选项的作用),在进行模块编译前切换到PWD目录(这是M=...的作用);在PWD目录下编译模块时,make再次读取当前目录下的Makefile,这时KERNELRELEASE变量已经定义(在KERNELDIR下的Makefile中定义),make会读取到obj-m := hello.o,根据内核编译系统的规则,进行hello.c-->hello.o-->hello.ko的编译。
对Make语法还不怎么了解的同学可以看看《GUN Make 项目管理》第三版这本书,对内核构建系统感兴趣的同学可以看看这篇kbuild的译文【linux-2.6.31】kbuild.pdf

模块的装载与卸载

装载模块:sudo insmod hello.ko howmany=5 whom="Jack"(howmany和whom为参数,可以选择性地指定)

卸载模块:sudo rm hello

查看已装载模块:lsmod

查看模块信息:modinfo hello

装载和卸载模块后,可通过dmesg查看模块的输出,验证模块是否成功装载和卸载。装载和卸载模块还可以是用modprobe命令,它与insmod的区 别在与它会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有这类引用,modprobe会在当前模块搜索路径中查找定义了这些符号的其他模块。如果modprobe找到这些模块(即要装载的模块所依赖的模块),它会同时将这些模块装载到内核。如果在这种情况下使用insmod,则该命令会失败,并在系统日志文件中记录“unresolved symbols”消息。

源码分析

  1. module.h包含有可装载模块需要的大量符号和函数定义;包含init.h 的目的是指定初始化和清除函数;包含moduleparam.h这样我们就可以在装载模块时想模块传递参数;kernel.h包含一些函数原型和常用宏定义
  2. 内核允许对模块指定参数,而这些参数可在装载模块时指定。当insmod改变模块参数前,模块必须让这些参数对insmod可见,参数必须使用module_param宏来声明。module_param需要的三个参数分别为变量的名称、类型以及用于sysfs入口项的访问许可掩码。
  3. __init暗示内核该函数仅在初始化期间使用,模块被装载后,模块装载器就会将初始化函数丢弃,这样可将该函数占用的内存释放出来,以作他用;__exit表示该函数仅用于模块卸载,编译器会把该函数放在特殊的ELF段中,如果模块被编译进内核或者内核的配置不允许卸载模块,则被标记为__exit的函数将简单地被丢弃。如果一个模块未定义清除函数,则内核不允许卸载该模块。
  4. module_init和module_exit宏用于指定模块的初始化和清除函数,初始化函数在模块被装载到内核时调用,而清除函数在模块被卸载时调用。
  5. 函数printk在linux内核中定义,功能和标准C库中的pringf类似。内核需要自己单独的打印输出函数,这是因为它在运行时不能依赖于C库。模块能够调用printk是因为在insmod函数装入模块后,模块就连接到了内核,因而可以访问内核的共用符号。
  6. MODULE_LICENSE宏告诉内核该模块采用的自由许可证;MODULE_AUTHOR宏描述模块作者;MODULE_DESCRIPTION宏用来说明模块的简短描述


剖析内核模块

内核模块也是ELF格式文件,对ELF文件格式感兴趣的同学可以学习下《程序员的自我修养》这本书。我们使用objdump命令来查看文件:

其中我们可以看到我们熟悉的.text,.data,.bss等区段(section)。您还可以在模块中找到其他支持动态特性的区段:.init.text 区段包含 module_init 代码,.exit.text 区段包含 module_exit 代码。.modinfo 区段包含各种表示模块许可证、作者和描述等的宏文本。

内核模块生命周期

在用户空间中,insmod启动模块加载过程。insmod 命令定义需要加载的模块,并调用 init_module 用户空间系统调用,开始加载过程。2.6 版本内核的 insmod 命令经过修改后变得非常简单(70 行代码),可以在内核中执行更多工作。insmod 并不进行所有必要的符号解析(处理 kerneld),它只是通过 init_module 函数将模块二进制文件复制到内核,然后由内核完成剩余的任务。init_module 函数通过系统调用层,进入内核到达内核函数 sys_init_module。这是加载模块的主要函数,它利用许多其他函数完成困难的工作。类似地,rmmod 命令会调用 delete_module用户空间系统调用,而 delete_module 最终会进入内核,并调用 sys_delete_module 将模块从内核删除。
加载和卸载模块时用到的主要命令和函数


在模块的加载和卸载期间,模块子系统维护了一组简单的状态变量,用于表示模块的操作。加载模块时,状态为 MODULE_STATE_COMING。如果模块已经加载并且可用,状态为 MODULE_STATE_LIVE。此外,卸载模块时,状态为 MODULE_STATE_GOING。

模块加载细节

现在,我们看看加载模块时的内部函数。当调用内核函数 sys_init_module 时,会开始一个许可检查,查明调用者是否有权执行这个操作(通过 capable 函数完成)。然后,调用 load_module 函数,这个函数负责将模块加载到内核并执行必要的调试(后面还会讨论这点)。load_module 函数返回一个指向最新加载模块的模块引用。这个模块加载到系统内具有双重链接的所有模块的列表上,并且通过 notifier 列表通知正在等待模块状态改变的线程。最后,调用模块的 init() 函数,更新模块状态,表明模块已经加载并且可用。
内部(简化的)模块加载过程


加载模块的内部细节是 ELF 模块解析和操作。load_module 函数(位于 ./linux/kernel/module.c)首先分配一块用于容纳整个 ELF 模块的临时内存。然后,通过 copy_from_user 函数将 ELF 模块从用户空间读入到临时内存。作为一个 ELF 对象,这个文件的结构非常独特,易于解析和验证。
下一步是对加载的 ELF 映像执行一组健康检查(它是有效的 ELF 文件吗?它适合当前的架构吗?等等)。完成健康检查后,就会解析 ELF 映像,然后会为每个区段头创建一组方便变量,简化随后的访问。因为 ELF 对象的偏移量是基于 0 的(除非重新分配),所以这些方便变量将相对偏移量包含到临时内存块中。在创建方便变量的过程中还会验证 ELF 区段头,确保加载的是有效模块。
任何可选的模块参数都从用户空间加载到另一个已分配的内核内存块(第 4 步),并且更新模块状态,表明模块已加载(MODULE_STATE_COMING)。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配 per-CPU 块。
在前面的步骤,模块区段被加载到内核(临时)内存,并且知道哪个区段应该保持,哪个可以删除。步骤 7 为内存中的模块分配最终的位置,并移动必要的区段(ELF 头中的 SHF_ALLOC,或在执行期间占用内存的区段)。然后执行另一个分配,大小是模块必要区段所需的大小。迭代临时 ELF 块中的每个区段,并将需要执行的区段复制到新的块中。接下来要进行一些额外的维护。同时还进行符号解析,可以解析位于内核中的符号(被编译成内核映象),或临时的符号(从其他模块导出)。
然后为每个剩余的区段迭代新的模块并执行重新定位。这个步骤与架构有关,因此依赖于为架构(./linux/arch//kernel/module.c)定义的 helper 函数。最后,刷新指令缓存(因为使用了临时 .text 区段),执行一些额外的维护(释放临时模块内存,设置系统文件),并将模块最终返回到 load_module。

模块卸载细节

卸载模块的过程和加载模块基本一样,除了必须进行几个健康检查外(确保安全删除模块)。卸载模块过程首先在用户空间调用 rmmod(删除模块)命令。在 rmmod 命令内部,对 delete_module 执行系统调用,它最终会导致在内核内部调用 sys_delete_module。
内部(简化的)模块卸载过程


当调用内核函数 sys_delete_module(将要删除的模块的名称作为参数传入)之后,第一步便是确保调用方具有权限。接下来会检查一个列表,查看是否存在依赖于这个模块的其他模块。这里有一个名为 modules_which_use_me 的列表,它包含每个依赖模块的一个元素。如果这个列表为空,就不存在任何模块依赖项,因此这个模块就是要删除的模块(否则会返回一个错误)。接下来还要测试模块是否加载。用户可以在当前安装的模块上调用 rmmod,因此这个检查确保模块已经加载。在几个维护检查之后,倒数第二个步骤是调用模块的 exit 函数(模块内部自带)。最后,调用 free_module 函数。
调用 free_module 函数之后,您将发现模块将被安全删除。该模块不存在依赖项,因此可以开始模块的内核清理过程。首先,从安装期间添加的各种列表中(系统文件、模块列表等)删除模块。其次,调用一个与架构相关的清理例程(可以在 ./linux/arch//kernel/module.c 中找到)。然后迭代具有依赖性的模块,并将这个模块从这些列表中删除。最后,从内核的角度而言,清理已经完成,为模块分配的各种内存已被释放,包括参数内存、per-CPU 内存和模块的 ELF 内存(core 和 init)。

备注

要获得模块管理的细节,源代码本身就是最佳的文档。关于在模块管理中调用的主要函数,请查看 ./linux/kernel/module.c(以及 ./linux/include/linux/module.h 中的头文件)。您还可以在 ./linux/arch//kernel/module.c 中找到几个与架构相关的函数。最后,可以在 ./linux/kernel/kmod.c 中找到内核自动加载函数(可以根据需要从内核自动加载模块)。这个功能可以通过 CONFIG_KMOD 配置选项启用。

参考文献

  1. 《LINUX设备驱动程序》第二章
  2. Linux 可加载内核模块剖析

原文链接:http://blog.chinaunix.net/uid-26497520-id-3597214.html,看着作者熬夜工作的份上转载请注明出处

阅读(5138) | 评论(1) | 转发(1) |
1

上一篇:没有了

下一篇:内核链表分析

给主人留下些什么吧!~~

CU博客助理2013-06-09 15:45:25

嘉宾点评:非常不错的内核驱动模块学习笔记,从介绍、实践到分析一应俱全,内容详实,条理清晰。相信作者对于内核模块一定有深入的研究和理解。对于Linux内核的学习者,这是一篇非常不错的学习教材,绝对值得推荐。建议可以今后将其分篇详述,或是追加各种问题的解析。
(感谢您参与“原创博文评选”获奖结果即将公布)