本章介绍一些关于模块和内核编程的必要概念,还要创建和运行一个完整的模块,看看一些被所有模块共享的基本代码。为避免一次抛出太多概念,本章只讨论模块,不涉及到任何特定的设备类别。本章在最后对于介绍过的每个内核项目(函数、变量、头文件、宏)都给出了参考章节。
1 建立测试系统
从本章开始会提供一些示例模块来展示编程概念。这些示例模块应该可以在任何2.6.x版本内核上运行,包括发行版提供的内核。但是建议使用可以从kernel.org下载的主线内核,因为发行版提供的内核可能打了很多补丁,偏离了主线,甚至可能会修改了设备驱动看到的内核API。
为2.6.x版本内核编译模块要求在系统上存在已经配置和编译链接好的内核源代码树,因为2.6版本的模块需要与内核源代码树中的一些目标文件(obj文件)链接到一起。所以现在要做的就是获取内核源代码,编译内核并且安装到系统中。
2 Hello World 模块
代码如下:
#include
#include
MODULE_LICENSE("Dual BSD/GPL"); static int hello_init(void) { printk(KERN_ALERT "Hello,world/n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Goodbye,cruel world/n"); } module_init(hello_init); module_exit(hello_exit); |
- module_init(): 声明模块初始化函数的宏
- module_exit(): 声明模块退出函数的宏
- MODULE_LICENSE: 模块使用自由许可证。如果没有这行,内核在加载模块时会有警告信息。
- printk(): 内核提供的类似C运行时库中printf()的函数。内核是自己运行的,不是在C运行时环境中运行,所以需要这个函数。
- KERN_ALERT: 消息优先级。为模块指定这个比较高的优先级,是因为默认的优先级可能使得消息输出在没有用处的地方。printk()输出在哪里决定于正在运行的内核和klogd守护进程的版本以及系统配置。第四章将详细介绍这个问题。
编译运行如下:
根据所使用系统传递消息行机制的不同,输出可能跟上面的有所不同。特别是,如果是在窗口系统中的终端模拟器里执行上述过程,则消息不会输出到终端屏幕,而是输出到系统日志文件/var/log/messages(不同发行版中文件名可能有所不同)。
编写模块并不难。困难的是理解设备,最大化设备性能。本章随后章节会更深入讨论模块化,而设备特定的问题将在后续章中讨论。
3 内核模块相比于应用程序
很多中小型应用程序从头到尾执行单个任务,而内核模块却只注册自身以便服务于后续的请求,其初始化函数会立即退出。也就是说,模块初始化函数的任务是为后续对模块函数的调用作准备。模块的退出函数则在模块即将被卸载前调用。这种编程模式与事件驱动编程相似。并不是所有应用程序都是事件驱动的,而每个内核模块却都是。另一个主要的不同是,应用程序在终止时可以延迟释放资源,或者避免同时清理所有资源,但是模块的退出函数应该撤销init函数建立的一切资源,否则它们可能会一直存在直到系统重新启动。
运行中卸载模块的能力可以帮助减少开发时间,因为不需要重启系统就可以测试新版本的代码。
应用程序可以调用不是自身定义的函数,只要在链接的时候通过合适的库解析外部函数调用就可以了。然而,模块仅仅链接到内核,只能调用由内核导出的函数。比如说,上面的hello.c使用的printk就是内核版本的printf函数。printk的行为与printf类似,只有少许不同,最主要的是不支持浮点数输出。
模块的源代码文件不应该包含通常的头文件,stdarg.h和非常特殊的情况是唯一的例外。大部分内核相关的头文件位于include/linux和include/asm目录。include的其他一些子目录包含了内核特定子系统相关的头文件。
内核编程和应用编程的另一个重要差异在于处理错误的方式:应用编程中的段错误是无害的,可以用调试器跟踪错误,找出源代码中的问题;而内核中的错误可能会终止整个系统,至少也会终止当前进程。第四章将会介绍如何跟踪内核错误。
3.1 用户空间和内核空间
现代处理器都实现了多个运行级别,不同的级别有不同的用途。在较低级别运行时不允许进行某些操作,而程序代码只能通过有限的门(调用)来在不同运行级别间切换。Linux使用最高和最低运行级别(x86系列处理器有多于两个运行级别,但Linux只使用其中两个级别)。内核在最高运行级别执行,称为管理模式,可以进行任何操作;应用程序在最低运行级别执行,称为用户模式,处理器会限制对硬件和内存的未授权访问。
“内核空间”和“用户空间”的说法,不仅指两种模式的权限级别不同,也指内存映射,也就是地址空间的不同。
进行系统调用,或者发生硬件中断时,系统的执行从用户空间切换到内核空间。执行系统调用的内核代码在调用进程的环境中运行,可以访问调用进程的地址空间。而处理中断的内核代码不与任何进程相关,不在任何进程的环境中运行。
模块的作用是扩展内核的功能,模块代码在内核空间中运行。通常驱动程序代码会执行上述两种任务:一些函数作为系统调用来执行;而另一些会用于中断处理。
3.2 内核中的并发
(1) 系统中有多个进程在运行,任何时刻都可能有多个进程试图使用驱动程序代码。
(2) 很多设备可以中断处理器的执行,这样,驱动程序试图执行某些操作时,其运行可能会被中断,而异步执行中断处理代码。
(3) 系统可能运行在对称式多处理器系统上,这样,驱动程序可能会同时被多个处理器并发执行。
2.6 版的内核是抢占式调度的,这使得即使在单处理器系统上,也有与多处理器系统一样的并发问题。所以,内核代码,包括驱动程序,必须是可重入的,也就是必须可以同时在多个环境中运行。必须仔细设计数据结构和访问共享数据的代码来处理并发,以避免数据被破坏,或者出现竞争条件(错误的执行次序导致不可预料的行为)。本书的每个示例代码都考虑了并发问题。第五章将详细讨论并发问题,介绍用于并发管理的内核原语。
不能假设任何代码段在执行中不会休眠(或者“阻塞”)。编码时不考虑并发可能导致灾难性的结果,并且极难调试。
3.3 当前进程
虽然内核模块不像应用程序那样顺序执行,但是内核执行的大部分操作是代表某特定进程进行的。内核代码可以通过定义在
中的全局变量
current访问当前进程。current是个指向定义在
中的
task_struct结构体的指针,代表当前正在执行的进程。
在执行像open和read这样的系统调用期间,当前进程就是进行调用的进程。内核代码可以通过current访问进程特定的信息,第六章有个例子展示了这种技术。
实际上,current并不是一个全局变量。为支持对称式多处理系统,需要有一种找出相关CPU上当前进程的机制。这种机制必须很快,因为current经常被引用。结果是使用了体系结构相关的机制来实现current,通常是内核栈上的一个task_struct结构体指针。具体实现的细节对内核的其他子系统是隐藏的。设备驱动程序只需要包含
,使用current代表当前进程就可以了。比如说,下面的代码就通过访问task_struct结构体的某些字段,输出了当前进程ID和命令名:
3.4 其他一些细节
(1) 内核的栈很小,可能只有一个页,即4096字节。驱动程序代码要与整个内核空间调用链共享这个很小的栈。所以,声明很大的自动变量是不好的做法。如果需要较大的结构,应该在调用时动态分配。
(2) 名字以双下划线开头的函数通常是底层组件的接口,使用的时候需要注意。
(3) 内核代码不能进行浮点运算,因为浮点运算需要的开销太大。
这就是
菊子曰啦!