一、设置测试系统
Hello World模块
内核时调用的hello_init,另一个是移除时调用的hello_exit;MODULE_LICENSE用来告诉内核该模块采用自由许可证。
代码中KERN_ALERT定义了这条消息的优先级。具有默认优先级的消息可能不会输出在控制台上,这取决于内核版本。
用户可以通insmod和rmmod来测试这个模块,要超级用户权限。
读者必须在makefile能找到的地方正确配置内核树。
二、核心模块与应用程序对比
大多数小规模应用程序都是从头到尾执行某个单个任务,模块却只是预先注册自己以便服务于将来的某个请求。应用程序在退出时,可以不管资源的释放或者其他的清除工作,但是内核退出函数必须仔细撤销初始化函数所作的一切!!否则在系统重新引导之前某些东西会残留在系统中。
还有,应用程序可以调用未定义的函数如printf,在连接过程中能够解析外部引用从而使用适当的函数库。然而,模块仅仅被连接到内核!!因此,只能调用由内核导出的那些函数,而不存在任何可链接的函数库。
下图展示了如何在模块中使用函数调用和函数指针,从而为运行中的内核增加新的功能。
大多数相关头文件保持在include/linux和include/asm中。
内核编程和应用程序编程的另外一点主要不同在于应用程序开发过程中的段错误是无害的,并且总是可以使用调试器跟踪到源代码中的问题所在,而一个内核错误即使不影响整个系统也会杀死当前进程。
1、用户空间和内核空间
模块运行在内核空间,应用程序运行在用户空间。
操作系统的作用是为应用程序提供一个对计算机硬件一致视图,必须负责程序的独立操作并保护资源不受非法访问。这个重要任务只有在CPU能够保护系统软件不受应用程序破坏时才能完成。
Unix使用最高和最低级别,内核运行在最高级别(超级用户态),应用程序运行在最低级别(用户态)。处理器控制着对硬件的直接访问以及对内存的非授权访问。
每个模式有自己的内存映射(自己的地址空间)。
应用程序执行系统调用或者硬件中断挂起时,Unix将执行模式从用户空间切换到内核空间。它代表调用进程执行操作,因此能够访问进程地址空间的所有数据。而处理硬件中断的内核代码和进程是异步的。
模块中的某些函数作为系统调用的一部分来执行,而其他函数则负责中断处理。
2、内核中的并发
内核编程区别于常见应用编程主要在于对并发的处理。大部分应用程序都是从头到尾顺序执行的,并不关心其他事项会改变他们的运行环境。而内核代码并不在这样简单的世界中运行,即使是最简单的内核模块都要注意同一时刻可能会有很多事情发生。
必须考虑并发的原因:
Linux系统通常正运行多个并发进程,并且可能有多个进程同时使用外面的驱动程序。
大多数设备有中断处理器,而中断处理程序异步运行,而且可能在驱动程序正试图处理其他任务时被调用。
另外,有一些软件抽象也在异步运行着。
Linux还可以运行在对称多处理器(SMP)系统上,因此可能同时有不止一个CPU运行我们的驱动程序。
Linux代码必须是可重入的,必须能够同时运行在多个上下文中。
因此,内核数据结构需要仔细设计才能保证多个线程分开执行,访问共享数据的代码也必须避免破坏共享数据。要编写能够处理并发问题而同时避免竞争(不同的执行顺序导致不同的、非预期行为发生的情况)的代码。
3、当前进程
内核代码可通过访问全局项current来获取当前进程,current在<asm.current.h>中定义,是一个执向struct task_struct的指针,而task_struct结构在<linux/sched.h>文件中定义。在open\read等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。为了支持smp系统,内核开发者设计了一种能找到运行在相关CPU上的当前进程的机制。这种机制必须是快速的,因为对current的引用会频繁发生。将指向task_struct结构的指针隐藏在内核栈中。设备驱动只要保护<linux/sched.h>头文件即可引用当前进程。
下面语句通过访问Struct task_struct的某些成员来打印当前进程的ID和命令名:
存储在current->comm成员中的命令名是当前进程所执行的程序文件的基本名称(base name)
4、一些其他细节:
内核具有非常小的栈,它可能只和一个4096字节大小的页那样小,我们自己的函数必须和整个内核空间调用链一同共享这个栈。因此,需要大的结构时候一定要动态分配该结构。
内核API看到(__)的函数,这种函数通常是接口的底层组件,谨慎使用。
内核空间不能实现浮点数运算。如果打开了浮点支持,在某些架构上需要在进入和退出内核空间时保持和恢复浮点处理器状态。这种额外的开销没有任何价值,内核代码中也不需要浮点运算。
三、编译和装载
1、编译模块
Documentation/kbuild
内核文档目录Documentation/Changes文件列出了需要的工具版本
注意内核源代码对编译器进行大量假定,新的编译器可能会导致问题。
创建makefile很简单
obj-m : = hello.o
如果是两个源文件:
构造模块的make命令:
上述命令首先改变目录到-C选项指定的位置,其中保持的内核的顶层目录Makefile文件。M=选项让该makefile在构造modules目标之前返回到模块源代码目录。然后,modules目标指向obj-m变量中的设定的模块:在上面的例子中,我们将变量设置成了module.o
上面这样的make命令还是有些烦人,因此内核开发者又开发了一种makefile的方法,用下面方法:



2、装载和卸载模块
insmod和ld有点类似,将模块的代码和数据装入内核,使用内核的符号表解析模块中任何未解析的符号。
内核支持insmod:
它依赖于kernel/module.c中的一个系统调用,函数sys_init_module给模块分配内核内存(函数vmalloc负责内存分配),然后,该系统调用将模块正文复制到内存区域,并通过内核符号表解析模块中的内核引用,最后调用模块的初始化函数。
注意系统调用函数都有sys_前缀,其他任何函数都没有这个前缀。
modprobe工具:和insmod类似,modprobe也用来将模块装载到内核中。他和insmod的区别在于,他会考虑要装载的模块是否引用了一些当前内核不存在的符号,如果有这类引用那么modprobe会在当前模块搜索路径中查找定义了这些符号的其他模块,如果modprobe找到了这些模块(要装载的模块所依赖的模块),他同时会将这些模块装载到内核。如果使用insmod则会在系统日志文件里报错"unresolved symbols"。
lsmod程序列出当前装载到内核中的所有模块,还提供了一些其他信息如其他模块是不是使用某个特定模块等。lsmod通过读取**/proc/modules虚拟文件来获得这些信息。有关当前已装载模块的信息也可以在sysfs虚拟文件系统的/sys/module**下找到!
3、版本依赖
在缺少modversions的情况下,模块代码必须针对要连接的每个版本的内核重新编译。我们不在这里讨论modbersions。

4、平台依赖
5、内核符号表

6、预备知识
module.h包含可装载模块需要的大量符号和函数定义,包含init.h的目的是指定初始化和清除函数。
大部分模块还包括modeleparam.h头文件,这样我们就可以在装载模块时向模块传递参数



7、初始化和关闭
初始化函数应该声明为static 因为这种函数在特定文件之外没有其他意义。__init标记看起来有点陌生,它对内核来说是一种暗示,表明该函数仅在初始化期间使用。在模块被装载之后,模块装载器就会将初始化函数扔掉,这样该函数占用的内存就可以释放出来。__init和__initdata的使用是可选的。
初始化之后的代码就不要加了。
__devinit和__devinitdata:只有在内核未被配置为支持热插拔设备的情况下,这两个标记才会被翻译为__init和__initdata。
module_init的使用是强制性的,这个宏会在模块的目标代码增加一个特殊的段,用于说明内核初始化函数所在的位置。
模块可以注册许多不同类型的设施,包括不同类型的设备、文件系统、密码变化等。对每种设施,对应有具体的内核函数用来完成注册。传递到内核注册函数中的参数通常是指向用来描述新设施及设施名称的数据结构指针,而数据结构通常包含指向模块函数的指针,这样,模块体中的函数就会在恰当的时间被内核调用。
能够注册的设施类型超出了在第一章中给出的设备类型列表,他们包括串口、杂项设备、sysfs入口、/proc文件、可执行域以及线路规程(line discipline)等。
内核符号表:层叠技术
grep EXPORT_SYMBOL
大部分注册函数都有register_前缀,因此找到他们的另一种方法是在内核源码中grep register_
8、清除函数
static void __exit cleanup_function(void)
{
/这里是清除代码/
}
module_exit(cleanup_function);
9、初始化过程中的错误处理
在注册时候要时刻注意注册可能会失败,即使是最简单的动作都需要内存分配,而所需要的内存可能无法获得。因此模块代码必须始终检查返回值,并确保所请求的操作已真正成功。
在注册时遇到任何错误,首先要判断模块是否可以继续初始化。在某个注册失败后可以通过降低功能来继续运转,因此只要有可能,模块应该继续向前并尽可能提供其功能。
如果发生了某个特定类型的错误之后无法继续装载模块,则需要将出错之前的任何工作都撤销掉!或重新引导系统,不然系统会处于不稳定的状态。
处理错误时候使用goto非常有效,可避免大量复杂的“结构化”逻辑。内核经常使用goto来处理错误。
err错误编码是定义在<linux/errno.h>中的负整数。每次返回合适的错误编码是一个好习惯,因为用户程序可以通过perror函数或类似的途径将他们转换为有意义的字符串。
模块的清除函数需要撤销初始化函数所注册的所有设施,并且在习惯上以相反于注册的顺序撤销设施:
void __exit my_cleanup_function(void)
{
unregister_those(ptr3,“skull”);
unregister_that(ptr2,“skull”);
unregister_this(ptr1,“skull”);
return ;
}
如果初始化和清除工作涉及很多设施,则goto方法可能变得难以管理,因为所有用于清楚设施的代码在初始化函数中重复,同时一些标号交织在一起。

我们可以使用或不适用外部标志来标记每个初始化步骤的成功,不管是否需要使用标志,这种方式的初始化能够很好地扩展到对大量设备的支持,因此比前面的技术更具有优越性。然而需要注意的是,因为清除函数被非退出代码调用,因此不能将清除函数标记为__exit。
10、模块装载竞争
四、模块参数
由于系统的不同,驱动程序需要的参数也许会发生变化。这包含设备编号以及其他一些用来控制驱动程序操作方式的参数。 如:SCSI适配器的驱动程序经常要处理一些选项,这些选项用来控制标记命令队列的使用,而集成设备电路驱动程序允许用户控制DAM操作。如果读者的驱动程序用来控制一些早期的硬件,需要明确告知驱动程序硬件的IO或者IO内存的位置。
为了满足种种需求,内核允许对驱动程序指定参数,这些参数可在装载驱动程序模块时改变。
这些参数的值可在运行insmod或modprobe命令装载模块时赋值,而modprob还可以从它的配置文件(/etc/modprob.conf)中读取参数值。
假设前面的"hello world"模块添加了两个参数:一个是整数值名称为howmany,一个是字符串whom。在装载这个模块时,向whom问候howmany次,这样可以用下面的命令行来装载该模块:
上述这条命令会让hellop 打印10次"hello,Mom"
在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见。参数必须使用module_param宏来声明,这个宏在moduleparam.h中定义。module_param需要三个参数:变量的名称,类型以及用于sysfs入口项的访问许可掩码。这个宏必须放在任何函数之外,通常是在源文件的头部。这样,hellop通过下面的代码来声明它的参数并使之对insmod可见:
static char *whom = “world”;
static int howmany = 1;
module_param(howmany,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);



五、在用户空间编写驱动程序
相对于内核空间编程,用户空间编程有自己的一些优点:
- 可以和整个C库链接,驱动程序不用借助外部程序就可以完成许多非常规任务。
- 可以使用通常的调试器调试驱动程序代码,而不用费力地调试正则运行的内核。
- 如果用户空间驱动程序被挂起,则可以简单地杀掉它。驱动程序带来的问题不会挂起整个系统,除非所驱动的硬件已经发生严重的故障。
- 和内核内存不同,用户内存可以换出。如果驱动程序很大但是不经常使用,则除了正在使用的情况外,不会占用太多内存。
- 良好设计的驱动程序仍然支持对设备的并发访问。
- 如果读者必须编写闭源码的驱动程序,则用户空间驱动程序可更加容易地避免因为修改内核接口而导致的不明确的许可问题。
例如,USB驱动程序可以在用户空间编写;具体可参阅libusb项目(libusb.sourceforge.net)以及内核源代码中的"gadgetfs"。X服务器是用户空间驱动程序的另一个例子。他十分清楚硬件可以做什么、不可以做什么
好的驱动程序进程可以允许设备的并发访问。


被折叠的 条评论
为什么被折叠?



