📄 (ldd) ch02-编写和运行模块(转载).txt
字号:
(代码)
存储在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都会替换相应模块的符号表。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -