编写和运行模块

非常高兴现在终于可以开始编程了。本章将介绍模块编程和内核编程所需的所有必要的
概念。我们将要不多的篇幅来编写和运行一个完整的模块。这种专业技术(expertise)
是编写如何模块化设备驱动程序的基础。为了避免一下子给你很多概念,本章仅介绍模
块,不介绍任何类别的设备。

这里介绍的所有内核内容(函数,变量,头文件和宏)也将在本章最后的参考部分再次
介绍。

如果你已经座不住了,下面的代码是一个完整的“Hello, World”模块(这个模块事实
上并没什么功能)。它可以在Linux 2.0或以上版本上编译通过,但不能低于或等于1.2
,关于这一点本章将在稍后的部分解释*。


(代码)

函数printk是由Linux内核定义的,功能与printf相似;模块可以调用printk,这是因为
在insmod加载了模块后,模块就被连编到内核中了,也就可以调用内核的符号了。字符
串<1>是消息的优先级。我之所以在模块中使用了高优先级是因为,如果你使用的是内核
2.0.x和旧的klogd守护进程,默认优先级的消息可能不能显示在控制台上(关于这个问
题,你可以暂且忽略,我们将在第4章,“调试技术”,的“Printk”小节中详细解释)


通过执行insmod和rmmod命令,你可以试试这个模块,其过程如下面的屏幕输出所示。注
意,只有超级用户才能加载 托对啬 块。

(代码)

正如你所见,编写一个模块很容易。通过本章我们将深入探讨这个内容。

模块与应用程序
在深入探讨模块之前,很有必要先看一看内核模块与应用程序之间的区别。

一个应用从头到尾完成一个任务,而模块则是为以后处理某些请求而注册自己,完成这
个任务后它的“主”函数就立即中止了。换句话说就是,init_module()(模块的入口点
)的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能
)的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能
做的。”模块的第二个入口点,cleanup_module,仅当模块被下载前才被调用。它应该
跟内核说,“我不在这了,别再让我做任何事了。”能够卸载也许是你最喜爱的模块化
的特性之一,它可以让你减少开发时间;你无需每次都花很长的时间开机关机就可以测
试你的设备驱动程序。

作为一个程序员,你一定知道一个应用程序可以调用应用程序本身没有定义的函数:前
后的连编过程可以用相应的函数库解析那些外部引用。printf就是这样一个函数,它定
义在libc中。然而,内核要仅能连编到内核中,它能调用的仅是由内核开放出来的那些
函数。例如,上面的helllo.c中的printk函数就是内核版的printf,并由内核开放给模
块给使用;除了没有浮点支持外,它和原函数几乎一模一样。

如图2-1所示,它勾画了为了在运行的内核中加入新函数,是如何调用函数以及如何使用
函数指针的。

由于没有库连接到模块中,源码文件不应该模块任何常规头文件。与内核有关的所有内
容都定义在目录/usr/include/linux和/usr/include/asm下的头文件中。在编译应用程
序也会间接使用这些头文件;其中的内核代码通过#ifdef __KERNEL__保护起来。这两个
内核头文件目录通常都是到内核源码所在位置的符号连接。如果你根本就想要整个内核
源码,你至少还要这两个目录的头文件。在比较新的内核中,你还可以在内核源码中发
现net和scsi头文件目录,但很少有模块会需要这两个目录。

内核头文件的作用将稍后需要它们的地方再做介绍,
内核头文件的作用将稍后需要它们的地方再做介绍,

内核模块与应用程序的另一个区别是,你得小心“名字空间污染”问题。程序员在写小
程序时,往往不注意程序的名字空间,但当这些小程序成为大程序的一部分时就会造成
许多问题了。名字空间污染是指当存在很多函数和全局变量时,它们的名字已不再富有
足够的意义来很容易的区分彼此的问题。不得不处理这种应用程序的程序员必须花很大
的精力来单单记住这些“保留”名,并为新符号寻找新的唯一的名字。如果在写内核代
码时出现这样的错误,这对我们来说是无法忍受的,因为即便最小的模块也要连编到整
个内核中。防止名字空间污染的最佳方法是把所有你自己的符号都声明为static的,而
且给所有的全局量加一个well-defined前缀。此外,你还可以通过声明一个符号表来避
免使用static声明,这些内容将在本章的“注册符号表”小节中介绍。即便是模块内的
私有符号也最好使用选定的前缀,这样有时会减轻调试的工作。通常,内核中使用的前
缀都是小写的,今后我们将贯彻这一约定。

内核编程和应用程序编程的最后一个区别是如何处理失效:在应用程序开发期间,段违
例是无害的,利用调试器可以轻松地跟踪到引起问题的错误之处,然而内核失效却是致
命的,如果不是整个系统,至少对于当前进程是这样的。我们将在第4章“调试系统失效
”小节中介绍如何跟踪内核错误。

用户空间和内核空间
本节的讨论概而言之就是,模块是在所谓的“内核空间”中运行的,而应用程序则是在
“用户空间”中运行的。这些都是操作系统理论的最基本概念。


事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。

每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
内存的未授权访问。

正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。

Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
关。

模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责

事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。

每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
内存的未授权访问。

正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。

Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
关。

模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
处理中断。

内核中的并发
内核编程新手首先要问的问题之一就是多任务是如何管理的。事实上,除了调度器之外
,关于多任务并没有什么可以多说的,而且调度器也超出了程序员的一般活动范围。你
可能会遇到这些任务,除了掌握如下这些原则外,模块编写者无需了解多任务。

与串行的应用程序不同,内核是异步工作的,代表进程执行系统调用。内核负责输入/输
出以及系统内对每一个进程的资源管理。

内核(和模块)函数完全在一个线程中执行,除非它们要“睡眠”,否则通常都是在单
个进程的上下文中执行――设备驱动程序应该能够通过交织不同任务的执行来支持并发
。例如,设备可能由两个不同的进程同时读取。设备驱动程序串行地响应若干read调用
,每一个都属于不同的进程。由于代码需要区别不同的数据流,内核(以及设备驱动程
序)必须维护内部数据结构以区分不同的操作。这与一个学生学习交织在一起的若干门
课程并非不无相似之处:每门课都有一个不同的笔记本。解决多个访问问题的另一个方
法就是避免它,禁止对设备的并发访问,但这种怠惰的技术根本不值的讨论。

当内核代码运行时,上下文切换不可能无意间发生,所以设备驱动程序无需是可重入的
,除非它自己会调用schedule。必须等待数据的函数可以调用sleep_on,这个函数接着
又调用schedule。不过你必须要小心,存在某些函数会无意导致睡眠,特别是任何对用
户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备
户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备
驱动程序的扩展操作”的“编写可重入代码”小节中讲解可重入函数。

就对设备驱动程序的多个访问而言,有许多不同的途径来分离这些不同的访问,但都是
依赖于任务相关的数据。这种数据可以是全局内核变量或是传给设备驱动程序函数的进
程相关参数。最重要的用来跟踪进程的全局变量是current:一个指向struct
task_struct结构的指针,在<linux/sched.h>中定义。current指针指向当前正在运行的
用户进程。在系统调用执行期间,如open或read,当前进程就是调用这个调用的进程*。
如果需要的话,内核代码就可以利用current使用进程相关信息。第5章“设备文件的访
问控制”小节中就有使用这种技术的例子。

编译器就象外部引用printk一样处理current。模块可以在任何需要的地方引用current
,insmod会在加载时解析出所有对它的引用。例如,如下语句通过访问struct
task_struct中的某些域打印当前进程的进程ID和命令名:

(代码)

存储在current->comm中的命令名是当前进程最后执行的可执行文件的基名。

编译和加载
本章的剩下部分将介绍编写虽然是无类别但很完整的模块。就是说,模块不属于任何第1
章“设备和模块的类别”中罗列的类别中的任何一个。本章中出现的设备驱动程序称为s
kull,是“Simple Kernel Utility for Loading Localities”的缩写。去掉这个模块
kull,是“Simple Kernel Utility for Loading Localities”的缩写。去掉这个模块
提供的范例函数,你可以重用这个模块,向内核加载你自己的本地代码。*

在我们介绍init_module和cleanup_module的作用之前,首先让我们写一个Makefile来编
译内核可以加载的目标代码。

首先,在包含任何头文件前,我们需要在预处理器中定义符号__KERNEL__。这个符号用
于选择使用头文件的哪一部分。由于libc包含了这些头文件*,应用程序最终也会包含内
核头文件,但应用程序不需要内核原型。于是就用__KERNEL__符号和#ifdef将那些额外
的去掉。将内核符号和宏开放给用户空间的程序会造成那个程序的名字空间污染。如果
你正在为一台SMP(对称多处理器)机器编译,你还需要在包含内核头文件前定义__SMP_
_。这一要求似乎有点不那么方便,但一旦开放人员找到达成SMP透明的正确方法,它就
会逐渐消失的。

另一个很重要的符号就是MODULE,必须在包含<linux/module.h>前定义这个符号。除非
要把设备驱动程序编译到内核映象中去,MODULE应该总是定义了的。由于本书所涉及的
驱动程序都不是直接连编到内核中去的,它们都定义了这个符号。

由于头文件中的函数都是声明为inline的,模块编写者还必须给编译器指定-O选项。gcc
只有打开优化选项后才能扩展内嵌函数,不过它能同时接受-g和-O选项,这样你就可以
调试那些内嵌函数的代码了*。

最后,为了防止发生令人不愉快的错误,我建议你使用-Wall(全面报警)编译选项,并
最后,为了防止发生令人不愉快的错误,我建议你使用-Wall(全面报警)编译选项,并
且还要修改源码去除所有编译器给出的警告,即便这样做会改变你已有的编程风格,你
也要这么做。

所有我目前介绍的定义和选项都在make使用的CFLAGS变量中。

除了一个合适的CFLAGS变量外,将要编写的Makefile还需要一个将不同目标文件连接在
一起的规则。这条规则仅当一个模块被分成若干个不同的源文件时才需要,这种并非很
不常见。通过命令ld -r将模块连接在一起,这条命令虽然调用了连接器,但并没有连编
操作。这是因为输出还是一个目标文件,它是输入文件的混合。-r选项的意思是“可重
定位”; 输出文件是可重定位的,这是因为它尚未嵌入绝对地址。

下面的Makefile实现了上述的所有功能,它能建立由两个源文件组成的模块。如果你的
模块是由一个源文件组成的,只要跳过包含ld -r的那项就可以了。

(代码)

上面文件中那个复杂的install规则将模块安装到一个版本相关的目录中,稍后将做解释
。Makefile中的变量VER是从<linux/version.h>中截取的版本号。

模块编好了,接下来必须把它加载到内核中。正如我前面所说,insmod就是完成这个工
作的。这个程序有点象ld,它要将模块中未解析的符号连编到正在运行的内核的符号表
中。但与连接器不同,它并不修改磁盘文件,而是修改内存映象。insmod有很多命令选
中。但与连接器不同,它并不修改磁盘文件,而是修改内存映象。insmod有很多命令选
项(如果想知道细节,可以看man),可以在模块连编到内核前修改模块中的整数值和字
符串值。因此,如果一个模块设计得体,可以在加载时对其进行配置;加载时配置要比
编译时配置更灵活,但不幸的是,有时候仍然有人使用后者。加载时配置将在本章的后
面“自动和手动配置”小节中讲解。

感兴趣的读者可能想知道内核是怎样支持insmod的:它依赖于kernel/modulec.c中定义
的几个系统调用。sys_create_module为装载模块分配内存(这些内存是由vmalloc分配
的,见第7章“获取内存”中的“vmalloc及其同胞”一节),为了连编模块,系统调用g
et_kernel_syms返回内核符号表,sys_init_module将可重定位目标码复制到内核空间并
调用模块的初始化函数。

如果你看过了内核源码,你就会发现系统调用的名字都有sys_前缀。所有系统调用都是
这样,其他函数并没有这个约定;当你在源码中查找系统调用时,知道这一点会对你有
所帮助。

版本相关性
要时刻牢记,对于你想连编的每一个不同版本的内核,你的模块都要相应地编译一次。
每个模块都定义了一个称为kernel_version的符号,insmod检查这个符号是否与当前内
核版本号匹配。较新的内核已在<linux/module.h>中替你定义了这个符号(这也就是为
什么hello.c中没有对它的声明)。这也意味着,如果你的模块是由多个源文件组成的,
你只能有一个源文件包含了<linux/module.h>。与此相反,当你在Linux 1.2下编译时,
必须在你的源码中定义kernel_version。
必须在你的源码中定义kernel_version。

如果版本不匹配,而你仍然想在不同版本的内核里加载你的模块,可以在insmod命令中
指定-f(“强制”)选项完成,但这个操作不安全,可能会失败。而且很难事先说明要
发生那种情况。由于符号不匹配,加载就会失败,此时你会得到一个错误信息。内核内
部的变化也会造成加载失败。如何这种情况发生了,你可能会在系统运行时得到一个非
常严重的错误,很可能造成系统panic――出于这个缘由,注意版本失配。事实上,通过
内核里的“版本机制”更完美地解决版本失配问题(稍后,第11章“Kerneld和高级模块
化”的“模块内版本控制”小节将介绍这一更先进的内容)。

如果你需要为某个特定的内核编译模块,你必须在上面的Makefile中包含相应内核的头
文件(例如,通过声明不同的INCLUDEPATH)。

为了处理加载时的版本相关性,insmod安装特定的路径查询:如果不能在当前目录找到
模块,就在版本相关的目录中查找,如果还失败就在/lib/modules/misc中查找。上面那
个Makefile中的install规则就遵循了这一约定。

写一个可以在从1.2.13到2.0.x的任一版本的内核上编译的内核是件复杂的任务。模块化
接口已经做了修改,配置越来越容易。你可以看到上面的那个hello.c中,只要你只处理
较新的内核就什么都不用声明。与此不同,可移植的接口如下所示:

(代码)


在2.0或更新的内核中,module.h包含了version.h,而且,如果没有定义__NO_VERSION_
_,module.h还定义了kernel_version。

如果你需要将多个源文件连接在一起组成一个模块,而又有多个文件都需要包含<linux/
module.h>――比如你需要module.h里声明的宏,就可以使用符号__NO_VERSION__。在包
含module.h前定义__NO_VERSION__就可以在你不想要自动声明字符串kernerl_version的
源文件里防止它的发生(ld -r会对一个符号的多处定义报警)。本书中的模块就使用__
NO_VERSION__达成这一目的。

其他基于内核版本的相关性可以通过预处理的条件编译解决――version.h定义了整数宏
LINUX_VERSION_CODE。这个宏展开后是内核版本的二进制表示,一个字节代表版本发行
号的一部分。例如,1.3.5的编码是66309(即,0x10305)。*利用这个信息,你可以轻
松地判断你正处理的是哪个版本的内核。

当你检查某个版本时,使用十进制表示是不方便的。为了在一个源文件里支持多个内核
版本,我将用下面的宏通过版本号的3个部分构建版本编码:

(代码)

内核符号表
我们已经知道insmod是如何利用公开内核符号来解析未定义符号的了。这张表包含了实
现模块化设备驱动程序所需的全局内核 瞑D―函数和变量。可以从文件/proc/ksyms中以
现模块化设备驱动程序所需的全局内核 瞑D―函数和变量。可以从文件/proc/ksyms中以
文本的方式读取这个公开符号表

当你的模块被加载时,你声明的任何全局符号都成为内核符号表的一部分,你可以从文
件/proc/ksyms或命令ksyms的结果了解这一点。

新模块可以使用你开放出来的符号,而且你在其他模块之上堆叠新模块。在主流的内核
源码中也使用了这种模块堆叠的方法:msdos文件系统依赖于fat模块开放出来的符号,
而ppp驱动程序则堆叠在报头压缩模块上。

在处理复杂对象时,模块堆叠非常有用。如果以设备驱动程序的形式实现一个新的抽象
,它可以提供一个设备相关的插接口。比如,帧缓冲视频驱动程序可以将符号开放给下
层VGA驱动程序使用。每个用户都加载帧缓冲视频驱动程序,然后在根据自己安装的设备
加载相应的VGA模块。

分层次的模块化简化了每一层的任务,大大缩减了开发时间。这同我们第1章中讨论的机
制与策略分离很相似。

注册符号表
另一种开放你的模块中的全局符号的方法是使用函数register_symtab,这个函数是符号
表管理的正式接口。这里所涉及的编程接口适用于内核1.2.13和2.0。如果想详细了解2.
1开发用内核所做的变动,请参见第17章“最新发展”。


正如函数register_symtab的名字所暗示,它用来在内核主符号表中注册符号表。这种方
法要比通过静态和全局变量的方法清晰的多,这样程序员就可以把关于哪些开放给其他
模块,哪些不开放的信息集中存放。这种方法比在源文件中到处堆放static声明要好的
多。

如果模块在初始化过程中调用了register_symtab,全局变量就不再是开放的了;只有那
些显式罗列在符号表中的符号才开放给内核。

填写一个符号表是项挺复杂的工作,但内核开发人员已经写好了头文件简化这项工作。
下面若干行代码演示了如何声明和开放一个符号表:

(代码)

有兴趣的读者可以看看<linux/symtab_begin.h>,但它可是内核中最难懂的头文件之一
。事实上,仅想好好使用宏X的话,根本没必要读董它。

由于register_symtab是在模块加载到内核后被调用的,它可以覆盖模块静态或全局声明
的符号。此时,register_symtab用显式符号表替代模块默认开放的公共符号。

这种覆盖是可能的,因为insmod命令处理传递给系统调用sys_init_module的全局符号表
,然后在调用init_module之前注册这个符号表。因此这之后的任何一次显式调用regist
er_symtab都会替换相应模块的符号表。
er_symtab都会替换相应模块的符号表。

如果你的模块不需要开放任何符号,而且你也不想把所有的东西都声明成static的,在i
nit_module里加上下面一行语句就可以了。这次对register_symtab的调用通过注册一个
空表覆盖了模块默认的符号表:

(代码)

如果源文件不想给堆叠在其上的模块提供什么接口,用上面那行语句隐藏所有的符号总
是不错的。

当模块从内 诵对厥 ,它所声明的所有公共符号也就自动?主符号表中注销了。不过是
全局符号还是显式符号表,这一点都适用。

初始化和终止
正如前面已述,init_module向内核注册模块所能提供的所有设施。这里我使用了“设施
”,我的意思是指新功能,是一整个设备驱动程序或新软件抽象,是一个可以由应用程
序使用的新功能。

通过调用内核函数完成新设施的注册。传递的参数通常为一个指向描述这个新设施的数
据结构和要注册的设施名称。这个数据结构通常会包含一些指向模块函数的指针,这就
是模块体内的函数是被调用的机制。


除了用来标别模块类别(如字符和块设备驱动程序)的“主”设施之外,模块还可以注
册如下项目:

其他设备

       由于这类设施仅仅用于总线型鼠标,这些设备曾一度称为鼠标设备。它们都是些
不完整的设备,通常要比那些功能健全的设备简单。

串行端口

       可以在运行时向系统里加入串口设备驱动程序;这也是支持PCMCIA调治解调器的
机制。

行律

       行律是处理终端数据流的软件层。模块可以注册新行律,以非标准方式处理终端
事务。例如,模块kmouse就使用行律从串口鼠标中偷取数据。

终端设备驱动程序

       终端设备驱动程序一组实现终端底层数据处理的函数。控制台和串口设备驱动程
序为了创建终端设备,它们都要注册自己的驱动程序。而多端口串口则有自己的驱动程
序为了创建终端设备,它们都要注册自己的驱动程序。而多端口串口则有自己的驱动程
序。

/proc文件

       /proc包含了用来访问内核信息的文件。由于它们也可以用来调试,第4章的“使
用/proc文件系统”将讲解/proc文件。

二进制文件格式

       对于每个可执行文件,内核扫描“二进制文件格式”列表并按相应的格式执行它
。模块可以实现新的格式,Java模块就是这样做的。

Exec域

       为了提供与其他流行Unix系统的兼容,必须修改内核的某些内部表格。一个“执
行域”就是一组从其他操作系统约定到Linux系统的映射。例如,模块可以定义执行SCO
二进制文件的执行域。

符号表

       这个已在前面的“注册符号表”小节中介绍了。


上面这些项目都不是前一章所考虑的设备类型,而且都支持那些通常集成到驱动程序功
能中的设施,如/proc文件和行律。之所以鼠标和其他设备驱动程序都没有象“完整”字
符设备那样管理,这主要是为了方便。过一会儿,当你读到第3章“字符设备”的“主从
设备号”小节时,原因就明了了。

还可以将模块注册为某些驱动程序的附件,但这样做就太特殊了,这里就不作讨论了;
它们都使用了“注册符号表”中讲到的堆叠技术。如果你想做更深一步的探究,你可以
在内核源码中查查register_symtab,并且找找不同驱动程序的入口点。大部分注册函数
都是以register_开始的,这样你就可以用“register_”在/proc/ksyms找找它们了。

init_module中的错误处理
如果你注册时发生什么错误,你必须取消失败前所有已完成的注册。例如,如果系统没
有足够内存分配新数据结构时,可能会发生错误。尽管这不太可能,但确实会发生,好
的程序代码必须为处理这类事件做好准备。

Linux不为每个模块保留它都注册了那些设施,因此当init_module在某处失败时,模块
必须统统收回。如果你在注销你已经注册的设施时失败了,内核就进入一种不稳定状态
:卸载模块后,由于它们看起来仍然是“忙”的,你再也不能注册那些设施了,而且你
也无法注销它们了,因为你必须使用你注册时的那个指针,而你不太可能得到那个指针
了。恢复这种情况非常复杂,通常,重新启动是最好的解决方法。

我建议你用goto语句处理错误恢复。我讨厌使用goto,但以我个人来看,这是一个它有
我建议你用goto语句处理错误恢复。我讨厌使用goto,但以我个人来看,这是一个它有
所做为的地方(而且,是唯一的地方)。在内核里,通常都会象这里处理错误那样使用g
oto。

下面这段样例在成功和失败时都能正确执行:

(代码)

返回值(err)是一个错误编码。在Linux内核里,错误编码是一个负值,在<linux/errn
o.h>中定义。如果你不使用其他函数返回的错误编码而要生成自己的,你应该包含<linu
x/errno.h>,这样就可以使用诸如-ENODEV,-ENOMEM之类的符号值。总是返回相应的错
误编码是种非常好的习惯,因为这样一来用户程序就利用perror或相似的方法把它们转
换成有意义的字符串了。

很明显,cleanup_module要取消所有init_module中完成的注册。

(代码)

使用计数
为了确定模块是否可以安全地卸载,系统为每个模块保留了一个使用计数。由于模块忙
的时候是不能卸载模块的,系统需要这些信息:当文件系统还被安装在系统上时就不能
删除这个文件系统类型,而且你也不能在还有程序使用某个字符设备时就去掉它。

如果忘了更新使用计数,你就不能再卸载模块了。在开发期间这种情况很可能发生,所
以你一定要牢记。例如,如果进程因你的驱动程序引用了NULL指针而终止,驱动程序就
不可能区关闭设备,使用计数也就无法回复到0。一种可能的解决方法就是在调试期间完
全不使用使用计数,将MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT重新定义为空操作。另
一个解决方法就是利用其他方法将计数强制复位为0(在第5章的“使用ioctl参数”小节
中介绍)。在编写成品模块时,决不能投机取巧。然而在调试时期,有时候忽略一些问
题可以节省时间,是可以接受的。

使用计数的当前值可以在/proc/modules中每一项的第3个域中找到。这个文件显式系统
中当前共加载了那些模块,每一项对应一个模块。其中的域包括,模块名,模块使用的
页面数和当前使用计数。这是一个/proc/modules样例:

(代码)

(autoclean)标志表明模块由kerneld管理(见第11章)。较新的内核中又加入了一些新
的标志,除了一件事外,/proc/modules的基本结构完全相同:在内核2.1.18和更新的版
本中,长度用字节计而不是页面计。

卸载
要卸载一个模块就要使用rmmod命令。由于无需连编,它的任务远比加载简单。这个命令
调用系统调用delete_module,如果使用计数为0它又调用模块的cleanup_module。

cleanup_module实现负责注销所有由模块已经注册了的项目。只有符号表是自动删除的
cleanup_module实现负责注销所有由模块已经注册了的项目。只有符号表是自动删除的


使用资源
模块不使用资源是无法完成自己的任务的,这些资源包括内存,I/O端口和中断,如果你
要用DMA控制器的话,还得有DMA通道。

做为一个程序员,你一定已经习惯了内存分配管理,在这方面编写内核代码没什么区别
。你的程序使用kmalloc分配内存,使用kfree释放内存。除了kmalloc多一个参数,优先
级,外,它们和malloc,free很相似。很多情况下,用优先级GFP_KERNEL就可以了。缩
写GFP代表“Get Free Page(获取空闲页面)。”

与此不同,获取I/O端口和中断乍听起来怪怪的,因为程序员一般同用显式的指令访问它
们,不必让操作系统了解这些。“分配”端口和中断与分配内存不同,因为内存是从一
个资源池中分配,并且每个地址的行为是一样的;I/O端口都各有自己的作用,而且驱动
程序需要在特定的端口上工作,而不能随便使用某个端口。

端口
对于大多数驱动程序而言,它们的典型工作就是读写端口。不管是初始化还是正常工作
的时候,它们都是这样的。为了避免其他驱动程序的干扰,必须保证设备驱动程序以独
占方式访问端口――如果一个模块探测因自己的硬件而写某个端口,而恰巧这个端口又
是属于另一个设备的,这之后一定会发生点怪事。


为了防止不同设备间的干扰,Linux的开发者决定实现端口的请求/释放机制。然而,未
授权的对端口的访问并不会产生类似于“段失效”那样的错误――硬件无法支持端口注
册。

从文件/proc/ioports可以以文本方式获得已注册的端口信息,就象下面的样子:

(代码)

文件中的每一项是有驱动程序锁定的范围(以十六进制表示)。在这些被释放前,其他
驱动程序不允许访问这些端口。

避免冲突有两个途径。首先,向系统增加新设备的用户检查/proc/ioports,然后在配置
新设备使用空闲端口――这种方法假设设备可以通过跳线进行配置。然后,当软件驱动
程序初始化自己时,它能自动探测新设备而对其他设备无害:驱动程序不会探测已由其
他驱动程序使用的I/O端口。

事实上,基于I/O注册的冲突避免对于模块化驱动程序很合适,但对于连编到内核里的驱
动程序来说却可能失败。尽管我们不涉及这种驱动程序,但还是很是必要注意到,对于
一个在启动时初始化自己的驱动程序来说,由于它要使用之后会被注册的端口,很可能
会造成对其他设备的误配置。虽然如此,还是没有办法让一个符合规范的驱动程序与已
配置好的硬件交互,除非以前加载的驱动程序不注册它的端口。基于以上原因,探测ISA
设备是件很危险的事,而且如果随正式Linux内核发行的驱动程序为了因与尚未加载的模
设备是件很危险的事,而且如果随正式Linux内核发行的驱动程序为了因与尚未加载的模
块对应的设备交互,拒绝在模块加载时执行探测功能。

设备探测的问题是因为只有一种方法标别设备,即通过写目标端口然后再读的方法――
处理器(而且是任何程序)只能查看数据线上的电子信号。驱动程序编写者知道一旦设
备连接到某个特定的端口上,它就会响应相应的查询代码。但是如果另一个设备连到了
端口上,程序仍然会写这个设备,但天知道它会怎么响应这个异常的探测操作。有时可
以通过读外设的BIOS,查看一个已知的字串来避免端口探测;已有若干SCSI设备使用了
这种技术,但并不是每个设备都要有自己的BIOS。

一个符合规范的驱动程序应该调用check_region查看是否某个端口区域已由其他驱动程
序锁定,之后就用request_region将端口锁住,当驱动程序不再使用端口时调用release
_region释放端口。这些函数的原型在<linux/ioports.h>中。

注册端口的典型顺序如下所示(函数skull_probe_hw包含了所有设备相关代码,这里没
有出现):

(代码)

在cleanup_module里释放端口:

(代码)


系统也使用了一套类似的请求/释放策略维护中断,但注册/注销中断比处理端口复杂,
整个过程的详细解释将放在第9章“中断处理”中介绍。

与前面讲到的关于设施的注册/注销相似,对资源的请求/释放方法也适合使用已勾勒的
基于goto的实现框架。

对于编写PCI设备驱动程序的人来说,不存在这里所讲的探测问题。我将在第15章“外部
总线简介”中介绍。

ISA内存
本节技术性很强,如果你对处理硬件问题不是很有把握,可以简单跳过这节。

在Intel平台上,ISA槽上的目标设备可能会提供片上内存,范围在640KB到1MB之间(0xA
0000到0xFFFFF);这也是设备驱动程序可以使用的一类资源。

这种内存部件反映了8086处理器那个时代,当时8086的寻址只有一兆的大小。PC设计人
员决定,低端的640KB当做RAM,而保留另外的384KB用于ROM和内存映射设备。今天,即
便是最强力的个人电脑也还有这个在第一兆字节里的空洞。Linux的PC版保留了这片内存
,根本不考虑使用它。本节给出的代码可以让你访问这个区域的内存,但它仅限于x86平
台,而且Linux内核要至少是2.0.x的,x是多少都可以。2.1版改变了物理内存的访问方
式,比如,640KB-1MB这段范围内的I/O内存就不能再这样访问。访问I/O内存的正确方式
是第18章“硬件管理”“低1M内的ISA内存”小节中的内容,这超出了本章的范围。
是第18章“硬件管理”“低1M内的ISA内存”小节中的内容,这超出了本章的范围。

尽管内核提供了端口和中断的请求/释放机制,当前它还是没能提供给I/O内存类似的机
制,所以你得自己做了。如果我能理解Linus是如何看待PC体系结构的化,这里给的方法
就不会变化了。

有时某个驱动程序需要在初始化时探测ISA内存;例如,我需要告诉视频截取器(frame
grabber)在哪映射截取的图象。问题是,如果没有探测方法,我将无法辨别那段范围内
哪块内存正在使用。人们需要能够辨别3种不同的情况:映射了RAM,有ROM(例如,VGA
BIOS),或者那段区域空闲。

skull样例给出一种处理这些内存的方法,但由于skull和物理设备无关,它打印完640KB
-1MB这段内存区域的信息后就退出了。然而,有必要谈一谈用于分析内存的代码,因为
它必须处理一些竞争条件。竞争条件就是这样一种情形,两个任务可以竞争同一个资源
,而且未同步的操作可能会损坏系统。

尽管驱动程序编写者无需处理多任务,我们还是必须记住,中断可能在你的代码中间发
生,而且中断处理函数可能会不提醒你就修改全局量。尽管内核提供了许多工具处理竞
争条件,下面给得出的简单规则阐述了处理这个问题的方法;对这个问题的彻底对策将
在第9章的“竞争条件”小节中给出。

l        如果仅仅是读取共享的量,而不是写,将其声明为volatile,要求编译器不对
其进行优化。这样,编译好的代码在每次源码读取它时读取这个量了。
其进行优化。这样,编译好的代码在每次源码读取它时读取这个量了。

l        如果代码需要检查和修改这个值,必须在操作期间关闭中断,这样可以防止其
他进程在我们检查过这个值后,但恰恰又在我们修改这个量之前修改这个量。

我们建议采用如下关闭中断的顺序:

(代码)

这里cli代表“clear interrupt flag(清除中断标志)”。上面出现的函数都定义在<a
sm/system.h>中。

应该避免使用经典的cli和sti序列,因为有时你无法在关闭中断前断定中断是否打开了
。如果此时调用sti就是产生很不规则的错误出现,很难追踪这样的错误。

由于那段内存只能通过写物理内存和读取检查才能标别,而且如果测试期间有中断的化
,有可能会被其他程序修改,因此检查RAM段的代码同时利用了volatile声明和cli。下
面的这段代码并不是很简单,如果一个设备正在象它的内存写数据,而这段代码又在扫
描这段区域,它就会误认为这段区域是空闲区。好在这样的情况很少发生。

在下面的源代码中,每个printk都带有一个KERN_INFO前缀。这个符号拼接在格式字串前
面做消息的优先级,它定义在<linux/kernel.h>中。这个符号展开后与本章开始的hello
..c中使用的<1>字串很相似。
..c中使用的<1>字串很相似。

(代码)

如果你在探测时注意恢复你所修改的字节,探测内存不会造成与其他设备的冲突。*

作为一个细心的读者,你可能会知道在15MB-16MB地址域内的ISA内存是怎么回事。很不
幸,那是个更棘手的问题,我们将在第8章的“1M以上的ISA内存”小节中讨论。

自动和手动配置
根据系统的不同,驱动程序需要了解的若干参数也会随之变化。例如,设备必须了解硬
件的I/O地址或内存区域。

注意,本节所讨论的大部分问题并不适用于PCI设备(第15章介绍)。

根据设备的不同,除了I/O地址外,还有一些其他参数会影响系统的驱动程序的行为,如
设备的品牌和发行号。驱动程序为了正确地工作有必要了解这些参数的具体值。用正确
的数值设置驱动程序(即,配置它)是一项需要在初始化期间完成的复杂的任务。

基本说来,有两种方式可以获得这些正确的数值:或者是用户显式地给出它们,或者是
驱动程序自己探测。无疑,自动探测是最好的驱动程序配置方法,而用户配置则是最好
实现的;作为驱动程序编写者的一种权衡,他应该尽可能地实现自动配置,但又允许用
户配置作为一种可选的方式替代自动配置。这种配置方法的另一个好处就是,在开发期
户配置作为一种可选的方式替代自动配置。这种配置方法的另一个好处就是,在开发期
间可以给定参数,从而不用自动探测,可以在以后实现它。

insmod在加载时接受命令行中给定的整数和字串值,可以给参数赋值。这条命令可以修
改在模块中定义的全局变量。例如,如果你的源码中包含了这些变量:

(代码)

那么你就可以使用如下命令加载模块:

(代码)

例子里使用了printk,它可以显式,当init_module被调用时,赋值已经发生了。注意,
insmod可以给任何整型或字符指针变量赋值,不管它们是否是公共符号表中的一部分。
但对于声明为数组的串是不能在加载时赋值的,因为它已经在编译时解析出来了,以后
就不能修改了。

自动配置可以设计为按如下方式工作:“如果配置变量是默认值,就执行自动探测;否
则,保留当前值。”为了让这种方法可以工作,“默认”值应该不是任何用户可以在加
载时设定的值。

下面这段代码给出了skull是如何自动探测设备的端口地址的。在这个例子中,使用自动
探测查找多个设备,而手动配置只限于一个设备。注意,函数skull_detect在上面已经
探测查找多个设备,而手动配置只限于一个设备。注意,函数skull_detect在上面已经
给出了,而skull_init_board负责完成设备相关的初始化工作,这里没有给出。

(代码)

为了方便用户在insmod命令行中给出相应的参数,而且如果这些符号不会放到主符号表
中的话,实际使用的驱动程序可以去掉配置变量的前缀(在本例中就是skull_)。如果
它们确实要放到主符号表中,好的办法就是声明两个符号:一个没有前缀,在加载时赋
值,一个有前缀,用register_symtab放到符号表中。

在用户空间编写驱动程序
到现在为止,一个首次接触内核问题的Unix程序员困难会对编写模块非常紧
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值