<一>:设置测试系统
首先准备好一个内核源码树,构造一个新内核,然后安装到自己的系统中。
<二>:HelloWorld模块
#include <linux/init.h> //定义了驱动的初始化和退出相关的函数 #include <linux/module.h> //定义了内核模块相关的函数、变量及宏 MODULE_LICENSE("Dual BSD/GPL"); //该宏告诉内核,该模块采用自由许可证 static int hello_init(void) //初始化函数仅在模块加载时调用 { printk(KERN_ALERT"Helloworld\n"); return 0; } static void hello_exit(void) //卸载函数仅在模块卸载时调用 { printk(KERN_ALERT"Goodbye,cruelworld\n"); } module_init(hello_init); module_exit(hello_exit);
<三>:核心模块与应用程序的对比
1.内核模块与应用程序之间的种种不同:
a.大多数小规模及中规模应用程序是从头到尾执行单个任务,而模块却只是预先注册自己以便服务于将来的某个请求,然后它的初始化函数就立即结束。
b.事件驱动的应用程序和内核代码之间的另一个不同是:应用程序在退出时,可以不管资源的释放或者其他的清除工作,但模块的退出函数却必须仔细撤销初始化函数所做的一切,否则,在系统重新引导之前某些东西就会残留在系统中。
c.应用程序可以调用它并未定义的函数,这是因为连接过程能够解析外部引用从而使用适当的函数库。而模块仅仅被链接到内核,因此它能调用的函数仅仅是内核导出的那些函数,而不存在任何可链接的函数库。
d.内核编程和用用编程的另外一点重要不同在于各环境下处理错误的方式不同:应用程序开发过程中的段错误是无害的,并且总是可以使用调试器跟踪到源代码中的问题所在,而一个内核错误即使不影响整个系统,也至少会杀死当前进程。
2.用户空间和内核空间
模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间里。操作系统的作用是为应用程序提供一个对计算机硬件的一致视图,同时操作系统必须负责程序的独立操作并保护资源不受非法访问。这个任务只有在CPU能够保护系统软件不受应用程序破换时才能完成。人们选择的方法是在CPU中实现不同操作模式(或者级别)。每当应用程序执行系统调用或者被硬件中断挂起时,Unix将执行模式从用户空间切换到内核空间。模块化代码在内核空间中运行,用于扩展内核的功能。通常一个驱动程序要执行两个任务:模块中的某些函数作为系统调用的一部分而执行,而其他函数则负责中断处理。
3.内核中的并发
有几方面的原因促使内核编程必须考虑并发问题:
a.Linux系统中通常正运行多个并发进程,并且可能有多个进程同时使用我们的驱动程序。
b.大多数设备能够中断处理器,而中断处理程序异步运行,而且可能在驱动程序正试图处理其他任务时被调用
c.有一些软件抽象也在异步运行着。
d.Linux还可以运行在对称多处理器系统上,因此可能同时有不止一个CPU运行我们的驱动程序。
e.在2.6中内核代码已经是抢占式的,这意味着即使在单处理器系统上也存在许多类似多处理器系统的并发问题。
4.当前进程
内核代码可通过访问全局项current来获得当前进程。current在<asm/current.h>中定义,是一个指向struct task_struct的指针,而task_struct结构在<linux/sched.h>文件中定义。current指针指向当前正运行的进程。在open/read等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。
如果需要,内核代码可以通过current获得与当前进程相关的信息。
设备驱动程序只要包含<linux/sched.h>头文件即可引用当前进程。例如,下面的语句通过访问struct task_struct
的某些成员来打印当前进程的ID和命令名:
printk(KERN_INFO"The process is\"%s\"(pid%i)\n,current->comm,current->pid);
5.其他一些细节
应用程序在虚拟内存中布局,并具有一块很大的栈空间。当然,栈是用来保存函数调用历史以及当前活动函数中的自动变量的。而相反的是,内核具有非常小的栈,它可能只和一个4096字节大小的页那样小。
经常会在内核API中看到具有两个下划线前缀(__)的函数名称。具有这种名称的函数通常是接口的底层组件,应该谨慎使用。
<四>:编译和装载
1.编译模块
首先来看看模块时如何构造的,具体细节参考内核源代码中Documentation/kbuild目录下的文件。在构造内核模块前,应确保具有正确版本的编译器、模块工具盒其他必要的工具。内核文档目录中Documentation/Changes文件列出了需要的工具版本。makefile里的一些规则:
如果要构造的模块名称为module.ko,并由两个源文件生成(比如file1.c和file2.c),则正确的makefile可如下编写:
obj-m := module.o module-objs := file1.o file2.o
2.装载和卸载模块
装载模块一般使用insmod程序,它和ld有些类似,它将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何为解析的符号。然而,与链接器不同,内核不会修改模块的磁盘文件,而仅仅修改内存中的副本。insmod可以接受一些命令行选项,并且可以在模块链接到内核之前给模块中的整型和字符串型变量赋值。与insmod类似的是modprobe工具,它不仅装载该模块,还装载该模块所一栏的模块。
卸载模块用rmmod工具,可从内核中移除模块。如果内核认为模块仍然在使用状态,或者内核被配置为禁止移除模块,则无法移除该模块。
lsmod程序列出当前装载到内核中的所有模块,还提供了其他一些信息,比如其他模块是不是在使用某个特定模块等。
3.版本依赖
4.平台依赖
<五>:内核符号表
insmod使用公共内核符号表来解析模块中未定义的符号。公共内核符号表中包含了所有全局内核项的地址,这是实现模块驱动程序所必须的。当模块被装入内核后,它所导出的任何符号都会变成内核符号表的一部分。新模块可以使用由我们自己的模块导出的符号,这样,我们可以在其他模块上层叠新的模块。
modprobe是处理层叠模块的一个使用工具,它的功能在很大程度上和insmod类似,但是它除了装入指定模块外还同时装入指定模块所依赖的其他模块。通过层叠技术,我们可以将模块划分为多个层,通过简化每个层可缩短开发时间。
Linux内核头文件提供了一个方便的方法来管理符号对模块外部的可见性,从而减少了可能造成的名字空间污染,并且适当隐藏信息。如果一个模块需要向其他模块导出符号,则应该使用下面的宏。
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
GPL版本使得要导出的模块只能只能被GPL许可证下的模块使用。符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上面这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。该变量将在模块可执行文件的特殊部分(即一个“ELF段”)中保存,在装载时,内核通过这个段来寻找模块导出的变量。
<六>:预备知识
大部分内核代码中都要包含相当数量的头文件,以便获得函数、数据类型和变量的定义。有几个头文件时专门用于模块的,因此必须在出现在每个可装载的模块中。因此,所有的模块代码中都包含下面两行代码:
#include <linux/module.h> /*module.h包含有可装载模块需要的大量符号和函数的定义*/
#include <linux/init.h> /*包含init.h的目的是指定初始化和清除函数*/
大部分模块还包括moduleparam.h头文件,这样就可以在装载模块时向模块传递参数。
MODULE_LICENSE("GPL");内核能够识别的该许可证。如果一个模块没有显示地标记为上述内核可识别的许可证,则会假定是专有的,而内核装载这种模块就会被“污染”。
可在模块中包含的其他描述性定义为包括MODULE_AUTHOR(描述模块作者)、MODULE_DESCRIPTION(用来说明模块用途的简短描述)、MODULE_VERSION(代码修订号)等。
<七>:初始化和关闭
1.模块的初始化函数负责注册模块所提供的任何设施。这里的设施指的是一个可以被应用程序访问的新功能,它可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。初始化函数的实际定义通常如下所示:
static int __init initialization_function(void)
{
/*这里时初始化代码*/
}
module_init(initialization_function);
初始化函数应该被声明为static,因为这种函数在特定文件之外没有其他意义。__init标记对内核来讲是一种暗示,表明该函数仅在初始化期间使用。在模块被装载之后,模块装载器就会将初始化函数扔掉,这样可将该函数占用的内存释放出来,以作他用注意,不要在结束初始化之后仍要使用的函数上使用这两个标记。
module_initd的使用是强制的。这个宏在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。没有这个定义,初始化函数永远不会被调用。模块可以注册许多不同类型的设施,包括不同类型的设备、文件系统、密码交换等。对于每种设施,对应有具体的内核函数用来完成注册。大部分注册函数名字带有register_前缀。
2.清除函数
每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。该函数定义如下:
static void __exit cleanup_function(void)
{
/*这里是清除代码*/
}
module_exit(cleanup_function);
__exit修饰词标记该代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段中)。如果模块被直接内嵌到内核中,或者内核的配置不允许卸载模块,则被标记为__exit的函数将被简单地丢弃。出于以上原因,被标记为__exit的函数只能在模块被卸载或者系统关闭时被调用,其他的任何用法都是错误的。module_exit声明为对于帮助内核找到模块的清除函数式必须的。如果一个模块未定义清除函数,则内核不允许卸载该模块。
3.初始化过程中的错误处理
4.模块装载竞争
a.在注册完成之后,内核的某些部分可能会立即使用我们刚刚注册的任何设施。即,在初始化函数还在运行的时候,内核就完全可能会调用我们的模块。因此,在首次注册完成之后,代码就应该准备好被内核的其他部分调用;在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施。
b.当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时应该如何处理。如果这种情况可能发生在我们的模块上,则根本不应该出现初始化失败的情况,毕竟模块已经成功导出了可用的功能及符号。如果初始化一定要失败,则应该仔细处理内核其他部分正在进行的操作,并且要等待这些操作的完成。
<八>:模块参数
由于系统的不同,驱动程序需要的参数也许会发生变化。这包括设备编号以及其他一些用来控制驱动程序操作方式的参数。内核允许对驱动程序指定参数,而这些参数可在装载驱动程序模块时改变。这些参数的值可在运行insmod或modprobe命令装载模块时赋值,而modprobe还可以从它的配置文件(/etc/modprob.conf)中读取参数值。
在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见。参数必须使用module_param宏来声明,这个宏在moduleparam.h中定义。module_param需要三个参数:变量的名称、类型以及用于sysfs入口项的访问许可掩码。这个宏必须放在任何函数之外,通常是在源文件的头部。例如:
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
内核支持的模块参数类型有: bool invbool charp int long short uint ulongushort
模块装载器也支持数组参数,在提供数组值时用逗号划分个数组成员。要声明数组参数,需要使用下面的宏:
module_param_arry(name, type, num, perm);
其中name是数组名称,type是数组元素类型,num是一个整型变量,而perm是常见的访问许可值。