#1 设置测试系统linux
& 想要在运行的内核当中扩展模块,就必须先准备好一个内核源代码树(能够是“主线”内核,也能够是发行版内核),构造一个新的内核,而后安装到本身的系统中,做为测试系统;
#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);
初始化函数应声明为 st
atic,
它们不会在特定文件以外可见;
__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 标记的函数将被抛弃;