02 构造和运行模块

#1 设置测试系统

     &  想要在运行的内核当中扩展模块,就必须先准备好一个内核源代码树(可以是“主线”内核,也可以是发行版内核),构造一个新的内核,然后安装到自己的系统中,作为测试系统;
 
#2 Hello World 模块
     &  构造好内核树之后,就可以开始编写模块了。我们先从简单的 “Hello World 模块”入手:
 1 #include <linux/init.h>
 2 #include <linux/module.h>
 3 MODULE_LICENSE("Dual BSD/GPL");
 4  
 5 static int hello_init(void)
 6 {
 7     printk(KERN_ALERT "Hello, world\n");
 8     return 0;
 9 }
10  
11 static int hello_exit(void)
12 {
13     printk(KERN_ALERT "Goodbye, cruel world\n");
14 }
15  
16 module_init(hello_init);
17 module_exit(hello_exit);

    上述模块定义了 hello_init 和 hello_exit 两个函数,hello_init 在模块被装载到内核的时候调用,而 hello_exit 在模块被移除内核时调用;
 
    module_init 和 module_exit 则是使用了内核的特殊宏来表示 hello_init 和 hello_exit 所要实现的功能;
 
    此外,特殊宏 MODULE_LICENSE 则声明了该模块的许可权限(如果没有这个声明,内核在装载该模块时会产生内核被污染警告),可用的 LICENSE 包括:GPL 、 GPL v2 、 GPL and additional rights 、 Dual BSD/GPL 、Dual MPL/GPL  、Proprietary;
 
    函数 printk 是在内核中定义的,这是因为内核在运行时不能依赖于 C 库,模块可以调用 printk 是因为在 insmod 函数装入模块之后,模块就连接到了内核,因而可以访问内核的公用符号;
 
    KERN_ALERT 定义了消息的优先级,消息的优先级降序排列为:[0] KERN_EMERG 、[1] KERN_ALERT 、[2] KERN_CRIT 、[3] KERN_ERR 、[4] KERN_WARNING 、[5] KERN_NOTICE 、[6] KERN_INFO 和 [7] KERN_DEBUG;新的优先级被指定为1~8之间的整数值,打印规则是:显示设定的优先级到最高优先级之间的信息,即如果值为1,则只有优先级为0的消息才能到达控制台,如果值为8,则0~7优先级的所有信息都会显示出来;
 
     &  模块编译和测试:
    make:根据 makefile 规定的编译规则将源程序编译成模块 .ko 文件[关于 makefile 会在之后的笔记当中单独编辑];
    su:切换到超级用户模式,只有超级用户才能加载和卸载模块[熟悉Linux 的同学应该都知道用户权限的问题,这里就不说了];
    insmod ./hello.ko :加载模块;
    rmmod ./hello.ko :移除模块;
 
#3 用户空间和内核空间
     &  模块运行在内核空间里,应用程序运行在用户空间中;
          在内核空间中,处理器可以进行所有的操作;而在用户空间中,处理器控制着对硬件的直接访问以及对内存的非授权访问;
          当且仅当进程执行系统调用或者被硬件中断挂起的时候,用户空间可以切换到内核空间;
 
#4 当前进程
     &  内核代码通过全局变量 current 来获得当前进程,current 是一个指向 struct task_struct 的指针;
          在 open 、read 等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程;
          需要提到的一点是,在2.6 内核中,引入一种新的机制,即current 不再是一个全局变量,而是将 task_struct 结构的指针隐藏在内核栈中,这样,设备驱动只要包含<linux/sched.h>头文件即可引用当前进程;而且新的机制支持SMP(对称多处理器)
 
#5  装载和卸载模块
     &  insmod 和 ld 的区别:
    insmod 将模块的代码和数据装入内核,然后使用内核符号表解析模块中任何未解析的符号;
    链接器 ld 的工作是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址,同时完成程序中各目标文件的地址空间的组织;
     ld 不同,使用 insmod,内核不会修改模块的磁盘文件,而是仅仅修改内存中的模块副本;
 
      &  insmod 的装载过程:
 
                
 
      &  insmod 和 modprobe 的区别:
    modprobe 也是用来装载模块的,与 insmod 不同的是,modprobe 会考虑要装载的模块是否引用了当前内核当中不存在的符号;如果有这类引用,modprobe 会在当前模块搜索路径中查找定义了这些符号的其他模块,并将这些模块一起装入内核,也即:modprobe 在装载的时候起到检查并修正符号依赖性的作用;而insmod 明显不具备这种功能,若在这种情况下使用 insmod,则会返回“unresolve symbol”(未解析的符号)错误;
 
      &  rmmod 用于从内核中移除模块;当内核认为模块忙时和内核配置被禁止移除模块时,rmmod 无法移除模块,这时可以使用强制移除,但更为保险的方式是重新引导系统;
 
      &  lsmod 用以显示当前装载到内核的所有模块,实际上,lsmod 通过读取 /proc/modules 虚拟文件来获得当前已装载的模块的信息
 
#6 内核符号表
      &  insmod 使用公用内核符号表来解析模块中未定义的符号,这是因为公用内核符号表包含了所有的全局内核项(函数和变量)的地址;当模块被装入内核后,它导出的任何符号都会变成内核符号表的一部分;这些导出的符号可以被新模块使用,这就是模块层叠技术;
 
      &  如果一个模块需要向其他模块导出符号,应使用下面两个宏:
        EXPORT_SYMBOL(name);
        EXPORT_SYMBOL_GPL(name);
    后者使得导出的版本只能被GPL 许可证下的模块使用;
 
       需要注意的是,符号必须在模块文件的全局部分导出,而不能在函数中导出,这是因为符号的导出由宏 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 控制,而这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的;
 
#7 版本依赖
      &  在缺少 modversions 的情况下,模块代码必须针对要链接的每个版本的内核重新编译,也就是说,不同的内核版本可能具有不同的数据结构和函数原型,模块的构造严重依赖于内核的版本;
    我们在构造模块时,可以将模块与当前内核树中的 vermagic.o 目标文件进行链接,通过 vermagic.o [包含了大量有关内核的信息] 来检查模块和正在运行内核的兼容性,如果不匹配,就不会装载该模块;
 
#8 模块头文件
      &  大部分内核代码中都包含一堆头文件,以便获得函数、数据类型和变量的定义;其中,有两个头文件是模块当中必须要包含的,它们是:module.h 和 init.h;
    module.h 包含可装载模块需要的大量符号和函数的定义;init.h 指定了模块初始化和清除函数;
    此外,也可能包含 moduleparam.h 头文件,moduleparam.h 用来在装载模块时向模块传递参数;
 
#9 初始化函数
       模块初始化函数注册模块提供的任何功能;
 
    初始化函数实际定义通常如下:
1 static int __init initialization_function(void)
2  {
3         /* Initialization code here */
4  }
5 
6  module_init(initialization_function);
 
   初始化函数应声明为 static它们不会在特定文件之外可见;
 
    __init 标志告诉内核给定的函数只是在初始化使用;模块加载后会丢掉这个初始化函数,使它的内存可做其他用途;
      类似的标签 :__initdate / __devinit / __devinitdata
          __initdata 给只在初始化时用的数据;__init 和 __initdata 是可选的
          __devinit 和 __devinitdata 在内核源码里,只有在内核未被配置为支持 hotplug 设备时它们才被翻译为 __init 和 _initdata;
 
    module_init 的使用是强制的;这个宏会在模块的目标代码中增加一个特殊的段,用于说明初始化函数所在的位置;没有这个定义,初始化函数永远不会被调用;
 
    模块可以注册许多不同类型的设施,对每种设施对应有具体的内核函数用来完成注册;大部分的模块注册函数使用 register_ 作为前缀,可在内核源代码中使用 grep register_ 找到它;
 
#10 清除函数
      清除函数用于在模块被移除前注销接口并向系统中返回所有资源;
 
    清除函数的定义如下:
    static void __exit cleanup_function(void)
    {
        /* Cleanup code here */
    }
    module_exit(cleanup_function);
    __exit 标记该代码仅用于模块卸载,当且仅当模块被卸载或系统关闭时被调用,其他用法都是错误的
 
    如果模块被直接嵌入到内核中或内核标记不允许卸载模块时,__exit 标记的函数将被抛弃;

 

posted @ 2013-10-08 21:52  Jan5  阅读(348)  评论(0编辑  收藏  举报