7内核模块

7模块

模块是一种向Linux内核添加设备驱动程序、文件系统及其他组件的有效方法,而无需连编新内核或重启系统。模块消除了宏内核的许多限制,这些限制总是被(特别是微内核支持者)当做反对宏内核的论据。这些论据主要涉及缺乏动态可扩展性的问题。在本章中,我们将讨论内核与模块交互的方式。换句话说,模块如何装载和卸载,以及内核如何检测不同模块之间的相互依赖。因而必须掌握模块二进制文件(及其ELF格式)的结构

7.1概述

优点:

  1. 通过使用模块,内核发布者能够预先编译大量驱动程序,而不会致使内核映像的尺寸发生膨胀。在自动检测硬件或用户提示之后,安装例程选择适当的模块并将其添加到内核中。这使得即使不熟练的用户也能够为系统设备安装驱动程序,而无需连编新内核。这在Linux系统获得广泛接受的过程中,是一个重大进展(甚至可以称之为先决条件)
  2. 内核开发者可以将试验性的代码打包到模块中,模块可以卸载,修改代码或重新打包后可以重新装载。这使得可以快速测试新特性,无需每次都重启系统

许可证问题也可以借助于模块来解决。众所周知,Linux内核源代码的许可证为GNU GPL v2(General Public License,Version 2),是第一批和最广泛使用的开源许可证之一。一个主要问题是下述事实:这可能是合理合法的,也可能不是,许多硬件生产商对控制其附加设备所需的文档保密,或要求开发者签署保密协议,开发者得遵守协议内容,对使用相关文档信息开发的源代码保守秘密,不向公众公开。这意味着驱动程序无法包含到正式的内核源代码中,后者的源代码总是开放的.通过使用只提供编译后形式、不提供源代码的二进制模块,可以解决这个问题
在这里插入图片描述

7.2使用模块

添加和移除模块涉及几个系统调用,这些通常由modutils工具包调用,后者在几乎每个系统上都会安装

7.2.1添加和移除

从用户的角度来看,模块可以通过两个不同的系统程序添加到运行的内核中。它们分别是modprobe和insmod前者考虑了各个模块之间可能出现的依赖性(在一个模块依赖于一个或多个合作者模块的功能时)。相比之下,insmod只加载一个单一的模块到内核中,而该模块可能只信赖内核中已存在的代码,并不关注所依赖的代码是通过模块动态加载,还是持久编译到内核中

modprobe在识别出目标模块所依赖的模块之后,在内核也会使用insmod。在用户空间对模块的处理即基于insmod。

在加载模块时所需的操作,与通过ld和ld.so借助于动态库链接应用程序的操作,二者表现出了很强的相似性。从外部看来,模块只是普通的可重定位目标文件,file调用可以很快证实这一点

wolfgang@meitner> file vfat.ko 
vfat.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

当然,模块既不是可执行文件,也不是系统程序设计中常见的程序库。但二进制模块文件的基本结构,是基于可执行文件和程序库所采用的同样方案

file命令的输出表明模块文件是可重定位的,这是用户空间程序设计中一个熟悉的术语。可重定位文件的函数都不会引用绝对地址,而只是指向代码中的相对地址,因此可以在内存的任意偏移地址加载,当然,在映像加载到内存中时,映像中的地址要由动态链接器ld.so进行适当的修改。 内核模块同样如此。其中的地址也是相对的,而不是绝对的。但重定位的工作由内核自身执行,而不是动态装载器

而在较早的内核版本(直至内核版本2.4)中,模块的装载是一个多步过程(在内核中分配内存,接下来在用户空间中对数据重定位,最后将二进制代码复制到内核中),现在只需要一个系统调用init_module,即可在内核中完成所有的操作

在处理该系统调用时,模块代码首先复制到内核内存中。接下来是重定位工作和解决模块中未定义的引用。因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以需要在这里处理未定义的引用

  • 处理未解决的引用
    为与内核的剩余部分协作,模块必须使用内核提供的函数.如 ramfs模块允许在内存中建立一个文件系统(通常称之为RAM磁盘),(类似于任何其他实现文件系统的代码)因而必须调用register_filesystem函数将自身添加到内核中可用文件系统的列表。该模块还使用了内核代码中的generic_file_read和generic_file_write标准函数(还有其他的函数),大多数内核文件系统都会使用这些。

    在用户空间中使用库时,会发生相似的情况。程序使用定义在外部库中的函数时,会在二进制代码中存储指向相关函数的指针,而不是函数自身的实现。当然,其他符号类型(如全局变量)是可以存储的,但函数不行。在程序链接(使用ld)时会解决对静态库的引用,而二进制文件装载时(使用ld.so)会解决对动态库的引用

    nm工具可用于产生模块(或任意目标文件)中所有外部函数的列表。以下例子中,给出了romfs模块使用的若干被列为外部引用的函数:

    wolfgang@meitner> nm romfs.ko 
        U generic_read_dir 
        U generic_ro_fops 
        ... 
        U printk 
        ... 
        U register_filesystem
    

    输出中的U代表未解决的引用。要注意,如果内核连编时没有启用KALLSYMS_ALL,则输出中不会有generic_ro_fops。这种情况下,只会看到表示函数的符号,而其他符号如generic_ro_fops这样的常数结构不会看到。

    很明显这些函数定义在内核的基础代码中,因而已经加载到内存。但如何找到与相关函数名称匹配的地址,以便解决这些引用呢?为此,内核提供了一个所有导出函数的列表。该列表给出了所有导出函数的内存地址和对应函数名,可以通过proc文件系统访问,即文件/proc/kallsyms

    wolfgang@meitner> cat /proc/kallsyms | grep printk 
        ffffffff80232a7f T printk
    

    T表示符号位于代码段,而D表示符号位于数据段。有关目标文件的布局,更多信息请参考附录E

7.2.2依赖关系

一个模块还可以依赖一个或多个其他模块.如vfat模块,它依赖fat模块
nm很清楚地说明了这种情况

wolfgang@meitner> nm vfat.ko 
    ... 
    U fat_alloc_new_dir 
    U fat_attach 
    ... 
wolfgang@meitner> nm fat.ko 
    ... 
    0000000000001bad T fat_alloc_new_dir 
    0000000000004a67 T fat_attach 
    ...

在向内核添加了fat模块之后,这些函数的地址信息也更新到了/proc/kallsyms。对于内核中持久编译的代码和随后添加的模块导入的代码,有一个数组,其数组项用于将符号分配到虚拟地址空间中对应的地址

在向内核添加模块时,需要考虑下列相关问题

  • 内核提供的函数符号表,可以在模块加载时动态扩展其长度。读者在下文会看到,模块可以指定其代码中哪些函数可以导出,哪些函数仅供内部使用。
  • 如果模块之间有相互依赖,那么向内核添加模块的顺序很重要。例如,如果试图在fat之前向内核装载vfat模块将会失败,因为若干函数引用的地址无法解决(相应的代码无法运行)

modutils标准工具集中的depmod工具可用于计算系统的各个模块之间的依赖关系。每次系统启动时或新模块安装后,通常都会运行该程序。找到的依赖关系保存在一个列表中。默认情况下,写入文件/lib/modules/version/modules.dep。其格式也不复杂。首先是目标模块的二进制文件名称,接下来是依赖的模块.

这个文件的信息由modprobe处理,该工具在现存的依赖关系能够自动解决的情况下向内核插入模块。其方法很简单:modprobe读入依赖文件的内容,搜索描述目标模块的行,搜集并建立所有依赖模块的列表。因为这些模块可能还依赖其他模块,所以需要在依赖文件中搜索对应的项,然后检查各个对应项的内容。该过程会一直持续下去,直至确认所有(直接或间接)依赖模块的名称。将所有涉及的模块插入到内核的实际任务,则委托给insmod工具

我们最感兴趣的问题仍然没有得到解答。如何识别模块之间的依赖关系呢?在解决该问题时,depmod没有采用内核模块的特性,只是使用了上文给出的信息。使用nm工具,不仅可以从模块读取该信息,还可以从普通的可执行文件或库中读取。

depmod分析所有可用的模块的二进制代码,对每个模块建立一个列表,包含所有已定义符号和未解决的引用,最后将各个模块的列表彼此进行比较。如果模块A包含的一个符号在模块B中是未解决的引用,则意味着模块B依赖模块A,接下来在依赖文件中以B : A的形式增加一项,即确认了上述事实。模块引用的大多数符号都定义在内核中,而不是定义在其他模块中。因此,在模块安装时产生了文件/lib/modules/version/System.map(同样使用depmod)。该文件列出了内核导出的所有符号。如果其中包含了某个模块中未解决的引用,那么该引用就不成问题了,在模块装载时引用将自动解决。如果未解决的引用无法在该文件或其他模块中找到,则模块不能添加到内核中,因为其中引用了外部函数,而又找不到实现。

7.2.3查询模块信息

还有一些额外的信息来源,是直接存储在模块二进制文件中,并且指定了模块用途的文本描述。这些可以使用modutils中的modinfo工具查询。它们可以存储以下各种数据项

  • 该驱动程序的开发者,通常带有电子邮件地址。该信息很有用,特别是对于错误报告(还能给开发者带来一些个人的满足感)
  • 驱动程序功能的简短描述
  • 可以传递给模块的配置参数,可能有对参数语义的确切描述
  • 指定支持的设备(例如,fd模块支持的是软盘)
  • 该模块按何种许可证分发

模块信息还可以提供一个独立的列表,给出该驱动程序支持的不同设备类型

这些额外的信息如何合并到二进制模块文件中呢?在所有使用ELF格式(参见附录E)的二进制文件中,有各种单元将二进制数据组织到不同类别中,这些在技术上称之为段。为允许在模块中添加信息,内核引入了一个名为.modinfo的段。读者在下文会看到,这个过程对模块的程序员来说是相对透明的,因为内核提供了一组简单的宏,用于向二进制文件插入数据。当然,附加信息的存在并不会改变代码的行为,因为所有处理模块但对该信息不感兴趣的程序都会忽略.modinfo段

模块许可证的有关信息保存在二进制文件中是由于法律原因

7.2.4自动加载ko模块

通常,模块的装载发起于用户空间,由用户或自动化脚本启动。在处理模块时,为达到更大的灵活性并提高透明度,内核自身也能够请求加载模块

只要内核能够访问二进制代码,那么将其加载到内核空间并不困难。但如果没有用户空间的帮助,内核也无法完成该工作。必须在文件系统中定位该二进制文件,并且必须解决依赖关系。由于在用户空间完成这些比内核空间容易得多,内核将该工作委托给一个辅助进程kmod。要注意,kmod并不是一个永久性的守护进程,内核会按需启动它

如:假定VFAT文件系统没有集成到内核中,只以模块的形式提供。如果用户发出以下命令装载一个软盘:mount -t vfat /dev/fd0 /mnt/floppy.在vfat模块载入内核之前,通常会返回一个错误信息,表明不支持对应的文件系统,因为内核中没有注册。但实际上情况不是这样。即使该模块没有装载,软盘仍然可以装载,没有任何问题。在mount调用结束时,所需的模块已经调入内核了

这是如何完成的?在内核处理mount系统调用时,它发现在其数据结构中没有所需文件系统vfat的信息。因而它试图使用request_module函数加载对应的模块,该函数将在7.4.1节讨论。该函数使用kmod机制启动modprobe工具,modprobe接下来按照惯例插入vfat模块。换句话说,内核依赖于用户空间中的一个应用程序使用内核函数来添加模块,如图7-2所示
在这里插入图片描述

完成这之后,内核再次试图获取所需文件系统的信息。由于modprobe调用,该信息现在已经保存在内核的数据结构中,当然前提是该模块实际存在。否则,modprobe系统调用会返回对应的错误码。

内核源代码中,很多不同地方调用了request_module。借助该函数,内核试图通过在没有用户介入的情况下自动加载代码,使得尽可能透明地访问那些委托给模块的功能

可能出现这样的情况:无法唯一确定哪个模块能够提供所需的功能。如将一个USB存储设备添加到系统时的情形。宿主机控制器驱动程序识别出新设备。所需装载的模块是usb-storage,但内核如何知道这一点?问题的答案是,附加到每个模块的一个小“数据库”。数据库的内容描述了该模块所支持的设备。对于USB设备,数据库的信息包括所支持接口类型的列表、厂商ID或能够标识该设备的任意类似信息。另一个例子是为PCI设备提供驱动程序的模块,也使用了与设备关联的唯一ID。这种模块提供了所有支持设备的列表。

数据库信息通过模块别名(module aliase)提供。这些是模块的通用标识符,其中编码了所描述的信息。宏MODULE_ALIAS用于产生模块别名。

//include/linux/module.h
/* Generic info of form tag = "info" */
//模块的.modinfo段包含了一般信息,使用MODULE_INFO设置
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)

/* For userspace: you can also call me... */
//用于产生模块别名,别名保存在模块二进制文件的.modinfo段,在用户空间中可据此访问模块。该机制可用于区分备选驱动程序
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)

//include/linux/moduleparam.h
#define __MODULE_INFO(tag, name, info)					  \
static const char __module_cat(name,__LINE__)[]				  \
  __attribute_used__							  \
  __attribute__((section(".modinfo"),unused)) = __stringify(tag) "=" info

MODULE_ALIAS提供的别名保存在模块二进制文件的.modinfo段中。如果一个模块提供了几个不同的服务,则直接插入适当的别名。例如,同一模块包含了RAID 4、RAID 5、RAID 6的代码情况

//drivers/md/raid5.c
MODULE_ALIAS("md-personality-4"); /* RAID5 */
MODULE_ALIAS("md-raid5");
MODULE_ALIAS("md-raid4");
MODULE_ALIAS("md-level-5");
MODULE_ALIAS("md-level-4");
MODULE_ALIAS("md-personality-8"); /* RAID6 */
MODULE_ALIAS("md-raid6");
MODULE_ALIAS("md-level-6");

比直接别名更重要的是设备数据库。内核提供了宏MODULE_DEVICE_TABLE来实现这样的数据库。上文给出的8139too模块的设备表是由以下代码创建的:

//drivers/net/8139too.c
static struct pci_device_id rtl8139_pci_tbl[] = {
	{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
	{0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
	{0x1113, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
    ...
    {PCI_ANY_ID, 0x8139, 0x13d1, 0xab06, 0, 0, RTL8139 },

	{0,}
};
MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);

该宏在模块二进制文件中提供了一个标准化的名称,可根据该名称访问设备表:

//include/linux/module.h
//模块二进制文件中提供了一个标准化的名称,可根据该名称访问设备表,宏展开标准化别名为__mod_(type)_device_table
#define MODULE_DEVICE_TABLE(type,name)		\
  MODULE_GENERIC_TABLE(type##_device,name)

#define MODULE_GENERIC_TABLE(gtype,name)			\
extern const struct gtype##_id __mod_##gtype##_table		\
  __attribute__ ((unused, alias(__stringify(name))))

就PCI来说,这会产生ELF符号__mod_pci_device_table,这是rtl8139_pci_tbl的别名

在编译模块(ko)时,转换脚本(scripts/mod/file2alias.c)会针对不同总线系统(PCI、USB、IEEE1394等,这些设备表格式都不同)解析设备表,并产生用作数据库项的MODULE_ALIAS项。这使得可以像处理模块别名一样处理设备数据库项,而无需复制数据库信息。由于转化过程基本上就是解析ELF文件并完成一些字符串重写,我在这里不会非常详细地讨论它。8139too模块的输出如下:

//drivers/net/8139too.mod.c
MODULE_ALIAS("pci:v000010ECd00008139sv*sd*bc*sc*i*"); 
MODULE_ALIAS("pci:v000010ECd00008138sv*sd*bc*sc*i*"); 
MODULE_ALIAS("pci:v00001113d00001211sv*sd*bc*sc*i*"); 
... 
MODULE_ALIAS("pci:v00001743d00008139sv*sd*bc*sc*i*"); 
MODULE_ALIAS("pci:v0000021Bd00008139sv*sd*bc*sc*i*"); 
MODULE_ALIAS("pci:v*d00008139sv000010ECsd00008139bc*sc*i*"); 
MODULE_ALIAS("pci:v*d00008139sv00001186sd00001300bc*sc*i*"); 
MODULE_ALIAS("pci:v*d00008139sv000013D1sd0000AB06bc*sc*i*");

模块别名是解决自动装载模块问题的基础,但该问题尚未完全解决。内核需要用户空间的一些支持。在内核注意到它需要对具有特定性质的某个设备加载模块之后,它需要向一个用户空间守护进程传递适当的请求。该守护进程接下来寻找恰当的模块并插入到内核中。7.4节将讲解该机制的实现方式。

7.3插入和删除模块

用户空间工具和内核的模块实现之间的接口,包括两个系统调用。

  • init_module:将一个新模块插入到内核中。用户空间工具只需提供二进制数据。所有其他工作(特别是重定位和解决引用)由内核自身完成
  • delete_module:从内核移除一个模块。当然,前提是该模块的代码不再使用,并且其他模块也不再使用该模块导出的函数

还有一个request_module函数(不是系统调用),用于从内核端加载模块。它不仅用于加载模块,还用于实现热插拔功能

7.3.1模块的表示

//include/linux/module.h

//模块结构体(ko注册,在内核中的表示)
struct module
{
	enum module_state state;//模块当前的状态

	/* Member of list of modules */
	struct list_head list;//链表元素,表头为modules,所有加载模块链表,表头是定义在kernel/module.c的全局变量modules

	/* Unique handle for this module */
	char name[MODULE_NAME_LEN];//模块名称,必须唯一,一般为.ko文件去掉.ko,如:vfat.ko模块名为vfat

	/* Exported symbols */
	const struct kernel_symbol *syms;//模块导出的符号,这是个数组,负责将标识符(name)分配到内存地址(value)
	unsigned int num_syms;//数组使用的个数
	const unsigned long *crcs;//与syms数组对应的数组,存储导出符号的校验和,用于实现版本控制(参见7.5节)。

	/* GPL-only exported symbols. */
	const struct kernel_symbol *gpl_syms;//GPL模块的符号,这是个数组
	unsigned int num_gpl_syms;//数组使用的个数
	const unsigned long *gpl_crcs;//与gpl_syms数组对应的数组,存储导出符号的校验和

	/* symbols that will be GPL-only in the near future. */
	const struct kernel_symbol *gpl_future_syms;//将来的GPL模块的符号,这是个数组
	unsigned int num_gpl_future_syms;//数组使用的个数
	const unsigned long *gpl_future_crcs;//与gpl_future_syms数组对应的数组,存储导出符号的校验和

	/* Exception table */
	unsigned int num_exentries;//extable数组的长度
	const struct exception_table_entry *extable;//数组,如果模块定义了新的异常,保存异常的描述

	/* 模块的二进制数据分为两个部分:初始化部分和核心部分。前者包含的东西在装载结束后都
	 * 可以丢弃(例如,初始化函数)。后者包含了正常运行期间需要的所有数据。
	 */
	/* Startup function. */
	int (*init)(void);//指向一个在模块初始化时调用的函数

	/* If this is non-NULL, vfree after init() returns */
	void *module_init;//初始化部分的起始地址

	/* Here is the actual code + data, vfree'd on unload. */
	void *module_core;//核心部分的起始地址

	/* Here are the sizes of the init and core sections */
	/* 模块的二进制数据分为两个部分:初始化部分和核心部分。前者包含的东西在装载结束后都可以丢弃(例如,初始化函数)。后者包含了正常运行期间需要的所有数据
    init_size:module_init长度(起始地址开始后的长度),单位为字节
	 * core_size:module_core长度(核心地址开始后的长度),单位为字节
	 */
	unsigned long init_size, core_size;

	/* The size of the executable code in each section.  */
	unsigned long init_text_size, core_text_size;

	/* Arch-specific module values */
	struct mod_arch_specific arch;//特定于处理器的挂钩,取决于特定的系统,其中可能包含了运行模块所需的各种其他数据。大多数体系结构都不需要任何附加信息,因此将struct mod_arch_specific定义为空结构,编译器在优化期间会移除掉。

	//如果模块会污染内核,则设置taints,污染意味着内核怀疑该模块做了一些有害的事情,可能妨碍内核的正确运作。,TAINT_PROPRIETARY_MODULE等值
	unsigned int taints;	/* same bits as kernel:tainted */

#ifdef CONFIG_MODULE_UNLOAD
	/* Reference counts */
	struct module_ref ref[NR_CPUS];//用于引用计数。系统中的每个CPU,都对应到该数组中的一个数组项。该项指定了系统中有多少地方使用了该模块。

	/* What modules depend on me? */
	struct list_head modules_which_use_me;//用作一个链表头,链表元素为module_use->list,将模块连接到内核用于描述模块间依赖关系的数据结构,所有引用(依赖)该模块的其他模块都在该链表中

	/* Who is waiting for us to be unloaded */
	struct task_struct *waiter;//指向导致模块卸载并且正在等待该操作结束的进程的task_struct实例

	/* Destruction function. */
	void (*exit)(void);//指向的函数用于在模块移除时负责特定于模块的清理工作(例如,释放分配的内存区域)。
#endif

#ifdef CONFIG_KALLSYMS
	/* We keep the symbol and string tables for kallsyms. */
	Elf_Sym *symtab;//记录该模块所有符号的信息(不仅是显式导出的符号)
	unsigned long num_symtab;//记录该模块所有符号的信息(不仅是显式导出的符号)
	char *strtab;//记录该模块所有符号的信息(不仅是显式导出的符号)

	/* Section attributes */
	struct module_sect_attrs *sect_attrs;

	/* Notes attributes */
	struct module_notes_attrs *notes_attrs;
#endif

	/* Per-cpu data. */
	void *percpu;//指向属于模块的各CPU数据。它在模块装载时初始化

	/* The command line arguments (may be mangled).  People like
	   keeping pointers to this stuff */
	char *args;//指向装载期间传递给模块的命令行参数

};

enum module_state
{
	MODULE_STATE_LIVE,//在运行
	MODULE_STATE_COMING,//在装载
	MODULE_STATE_GOING,//在移除
};

struct kernel_symbol
{
	unsigned long value;//存储标识符的内存地址
	const char *name;//标识符
};
//用于模块引用计数 ,该数据类型会对齐到L1高速缓存,try_module_get和module_put函数用于引用+1和-1
struct module_ref
{
	local_t count;
} ____cacheline_aligned;

KALLSYMS是一个配置选项(但只用于嵌入式系统,在普通计算机上总是启用的),启用该选项后,将在内存中建立一个列表,保存内核自身和加载模块中定义的所有符号(否则只存储导出的函数)。如果oops消息(如果内核检测到与背离常规的行为,例如反引用NULL指针)不仅输出十六进制数字(地址),还要输出涉及函数的名称,那么该选项就很有用

MODULE_UNLOAD,没有这个选项或者这个选项配置为“n”就不能卸载任何模块

MODVERSIONS启用版本控制。该选项防止将接口定义与当前版本不再匹配的废弃模块载入内核。7.5节会更详细地讨论该选项。

MODULE_FORCE_UNLOAD允许模块从内核强制移除,即使仍然有引用该模块的地方,或其他模块正在使用其代码,也是如此。在系统正常运转时,绝不需要这种蛮干法,但它可能在开发系统期间用到

KMOD选项使内核在需要模块时自动加载。这需要与用户空间的一些交互

7.3.2依赖关系和引用

如果模块B使用了模块A提供的函数,那么模块A和模块B之间就存在关系。可以用两种不同的方式来看这种关系。
(1) 模块B依赖模块A。除非模块A已经驻留在内核内存,否则模块B无法装载。
(2) 模块B引用模块A。换句话说,除非模块B已经移除,否则模块A无法从内核移除。事实上,条件应该是所有引用模块A的模块都已经从内核移除。在内核中,这种关系称之为模块B使用模块A。
为正确管理这些依赖关系,内核需要引入另一个数据结构

//kernel/module.c
/* 管理模块间的引用依赖关系,与struct module的modules_which_use_me成员共同建立,
 * 如果模块B使用了模块A的函数,则会创建新的这个结构体,list添加到模块A的modules_which_use_me链表中
 * ,module_which_uses指向模块B
 */
struct module_use
{
	struct list_head list;//链表元素,链表头为module->modules_which_use_me,放入A依赖B中的B成员的链表头中
	struct module *module_which_uses;//指向A依赖B中的A
};

在这里插入图片描述

上图依赖关系如下
依赖关系文件modules.dep:
ip_tables.ko: ip_conntrack.ko
iptable_nat.ko: ip_tables.ko 
ip_nat_ftp.ko: iptable_nat.ko ip_tables.ko ip_conntrack.ko 
ip_conntrack.ko: 
ip_conntrack_ftp.ko: ip_conntrack.ko

:前的模块引用(依赖)了:后的模块

内核数据结构只给出了依赖特定模块的其他模块的链表,在装载一个新模块之前,需要找出该模块所依赖的模块并先行加载,上述数据结构并不适用于此(至少,如果不遍历所有模块,就无法一一遍历各个模块的modules_which_use_me链表并重新分析其中的信息)。但这并不必要,因为用户空间中的信息就足够了

加载上图所有模块只需两个命令:

wolfgang@meitner> /sbin/modprobe ip_nat_ftp 
wolfgang@meitner> /sbin/modprobe ip_conntrack_ftp

内核提供了already_uses函数,来判断模块A是否需要另一个模块B:
use_module用于建立模块A和模块B之间的关系:模块A需要模块B才能正确运行

//kernel/module.c
//判断模块A是否引用(依赖)模块B,依赖返回1,不依赖返回0
static int already_uses(struct module *a, struct module *b)
//增加模块A对模块B的依赖关系
static int use_module(struct module *a, struct module *b)

7.3.3模块的二进制结构

readelf -S module.ko可以列出模块中的所有段

模块使用ELF二进制格式,模块中包含了几个额外的段,普通的程序或库中不会出现。除了少量由编译器产生、与我们的讨论不相关的段(主要是重定位段),模块由以下ELF段组成:

  • __ksymtab、__ksymtab_gpl和__ksymtab_gpl_future段包含一个符号表,包括了模块导出的所有符号。__ksymtab段中导出的符号可以由内核的所有部分所用(不考虑许可证),__kysmtab_gpl中的符号只能由GPL兼容的部分使用,而__ksymtab_gpl_future中的符号未来只能由GPL兼容的部分使用
  • __kcrctab、__kcrctab_gpl和__kcrctab_gpl_future包含模块所有(只适用于GPL、或未来只适用于GPL)导出函数的校验和。__versions包含该模块使用的、来自于外部源代码的所有引用的校验和

除非内核配置时启用了版本控制特性,否则不会建立上述段

  • __param存储了模块可接受的参数有关信息
  • __ex_table用于为内核异常表定义新项,前提是模块代码需要使用该机制
  • .modinfo存储了在加载当前模块之前,内核中必须先行加载的所有其他模块名称。换句话说,该特定模块依赖的所有模块名称。此外,每个模块都可以保存一些特定的信息,可以使用用户空间工具modinfo查询,特别是开发者的名字、模块的描述、许可证信息和参数列表
  • .exit.text包含了在该模块从内核移除时,所需使用的代码(和可能的数据)。该信息并未保存在普通的代码段中,这样,如果内核配置中未启用移除模块的选项,就不必将该段载入内存
  • 初始化函数(和数据)保存在.init.text段。之所以使用一个独立的段,是因为初始化完成后,相关的代码和数据就不再需要,因而可以从内存移除
  • .gnu.linkonce.this_module提供了struct module的一个实例,其中存储了模块的名称(name)和指向二进制文件中的初始化函数和清理函数(init和cleanup)的指针。根据本段,内核即可判断特定的二进制文件是否为模块。如果没有该段,则拒绝装载文件

在模块自身和所依赖的所有其他内核模块都已经编译完成之前,上述的一些段是无法生成的,例如列出模块所有依赖关系的段。因为源代码中没有明确给出依赖关系信息,内核必须通过分析目标模块的未解决引用和所有其他模块导出的符号,来获取该信息。

生成模块需要执行下述3个步骤:

  • 首先,模块源代码中的所有C文件都编译为普通的.o目标文件
  • 在为所有模块产生目标文件后,内核可以分析它们。找到的附加信息(例如,模块依赖关系)保存在一个独立的文件中,也编译为一个二进制文件
  • 将前述两个步骤产生的二进制文件链接起来,生成最终的模块

附录B详细地讲述了内核联编过程,并讨论了编译模块时可能遇到的问题

  1. 初始化和清理函数
    模块的初始化函数和清理函数,保存在.gnu.linkonce.module段中的module实例中。该实例位于上述为每个模块自动生成的附加文件中。其定义如下

    //module.mod.c 
    struct module __this_module 
    __attribute__((section(".gnu.linkonce.this_module"))) = { 
        .name = KBUILD_MODNAME, 
        .init = init_module,//对应ko文件中module_init宏注册的函数
        #ifdef CONFIG_MODULE_UNLOAD 
        .exit = cleanup_module, //对应ko文件中module_exit宏注册的函数
        #endif 
        .arch = MODULE_ARCH_INIT, 
    };
    

    KBUILD_MODNAME包含了模块的名称,只有将代码编译为模块时才定义。如果代码将持久编译到内核中,就不会产生__this_module对象,因为不需要对模块对象进行后处理。MODULE_ARCH_INIT是一个预处理器符号,可以指向模块的特定于体系结构的初始化方法。该特性当前只用于m68k CPU。

    <init.h>中的module_initmodule_exit宏用于定义init函数和exit函数。每个模块都包含以下类型的代码,定义了init函数和exit函数

    #ifdef MODULE
    static int __init xyz_init(void) {
    /* 初始化代码 */
    }
    static void __exit xyz_cleanup (void) {
    /* 清理代码 */
    }
    module_init(xyz_init);
    module_exit(xyz_exit);
    #endif
    

    __init__exit前缀用于将这两个函数放置到二进制代码正确的段中:

    //include/linux/init.h
    #define __init		__attribute__ ((__section__ (".init.text"))) __cold
    #define __initdata	__attribute__ ((__section__ (".init.data")))
    #define __exitdata	__attribute__ ((__section__(".exit.data")))
    #define __exit_call	__attribute_used__ __attribute__ ((__section__ (".exitcall.exit")))
    

    所用data后缀的变体,用于将数据(不是函数)放置到.init和.exit段中

  2. 导出符号EXPORT_SYMBOL

    内核为导出符号提供了两个宏:EXPORT_SYMBOLEXPORT_SYMBOL_GPL。顾名思义,二者分别用于一般的导出符号和只用于GPL兼容代码的导出符号。同样,其目的在于将相应的符号放置到模块二进制映象的适当段中:

    //include/linux/module.h
    #define __EXPORT_SYMBOL(sym, sec)				\
        extern typeof(sym) sym;					\
        __CRC_SYMBOL(sym, sec)					\
        static const char __kstrtab_##sym[]			\
        __attribute__((section("__ksymtab_strings")))		\
        = MODULE_SYMBOL_PREFIX #sym;                    	\
        static const struct kernel_symbol __ksymtab_##sym	\
        __attribute_used__					\
        __attribute__((section("__ksymtab" sec), unused))	\
        = { (unsigned long)&sym, __kstrtab_##sym }
    
    #define EXPORT_SYMBOL(sym)					\
        __EXPORT_SYMBOL(sym, "")
    
    #define EXPORT_SYMBOL_GPL(sym)					\
        __EXPORT_SYMBOL(sym, "_gpl")
    
    #define EXPORT_SYMBOL_GPL_FUTURE(sym)				\
        __EXPORT_SYMBOL(sym, "_gpl_future")
    

    使用举例:

    EXPORT_SYMBOL(get_rms)
    /*****************************************************************/ 
    EXPORT_SYMBOL_GPL(no_free_beer)
    
    上述代码通过预处理器处理后,如下所示:
    
    static const char __kstrtab_get_rms[] 
        __attribute__((section("__ksymtab_strings"))) = "get_rms"; 
    static const struct kernel_symbol __ksymtab_get_rms 
        __attribute_used__ __attribute__((section("__ksymtab" ""), unused)) = 
            (unsigned long)&get_rms, __kstrtab_get_rms 
    /*****************************************************************/
    
    static const char __kstrtab_no_free_beer[] 
        __attribute__((section("__ksymtab_strings"))) = "no_free_beer";
    
    static const struct kernel_symbol __ksymtab_no_free_beer 
        __attribute_used__ __attribute__((section("__ksymtab" "_gpl"), unused)) = 
            (unsigned long)&no_free_beer, __kstrtab_no_free_beer
    
    • __kstrtab_function是一个静态变量,保存在__ksymtab_strings段中。它是一个字符串,其值对应于(function)函数的名称。
    • __ksymtab(或__kstrtab_gpl)段中存储了一个kernel_symbol实例。它包括两个指针,一个指向导出的函数,另一个指向在字符串表中刚建立的项。这使得内核根据函数的字符串名称,即可找到匹配的代码地址。在解决引用时需要这样做,相关内容将在7.3.4节讨论

    MODULE_SYMBOL_PREFIX可用于为一个模块的所有导出符号分配一个前缀,这在某些体系结构上是必要的(但大多数将空串定义为前缀)。
    在对导出函数启用内核版本控制特性时(更多细节请参考7.5节),会使用__CRC_SYMBOL;否则该宏定义为空串(为简单起见,我在这里作如此假定)。

  3. 一般模块信息MODULE_INFO

    模块的.modinfo段包含了一般信息,使用MODULE_INFO设置:

    //include/linux/module.h
    #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
    
    //include/linux/moduleparam.h
    #define __MODULE_INFO(tag, name, info)					  \
    static const char __module_cat(name,__LINE__)[]				  \
    __attribute_used__							  \
    __attribute__((section(".modinfo"),unused)) = __stringify(tag) "=" info
    
  • 模块许可证MODULE_LICENSE
    模块许可证使用MODULE_LICENSE设置:

    //include/linux/module.h
    #define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
    

    在这里我们更感兴趣的是内核划分许可证类型的方式,哪些类型是GPL兼容的

    • GPL和表示GPL第二版的GPLv2。根据GPL的定义,该许可证的任何后续版本(可能尚不存在)也都可以使用
    • 如果有其他条款(必须兼容自由软件的定义)添加到GPL,那么必须使用GPL and additional rights
    • 如果模块的源代码以双许可证形式发布,则需要使用BSD/GPL、MIT/GPL或MPL/GPL(即GPL与Berkeley、MIT、Mozilla3种许可证之一同时使用)。
    • 专有模块(或许可证不兼容GPL的模块)必须使用Proprietary。
    • 如果没有指定明确的许可证,则使用unspecified。
  • 开发者和描述MODULE_AUTHOR,MODULE_DESCRIPTION
    每个模块都应该包含有关开发者的简短信息(如有可能,应包括电子邮件地址)和对模块用途的描述。

    //include/linux/module.h
    #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
    #define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)
    
  • 备选名称(別名)MODULE_ALIAS
    MODULE_ALIAS(alias)用于给模块指定备选名称(alias),在用户空间中可据此访问模块。该机制可用于区分备选驱动程序,例如,可能有几个驱动程序实现了同样的功能,但实际上只能使用其中一个。这对于构建系统化的名称也是必要的。例如,这使得能够向一个模块分配一个或多个别名。这些别名分别指定该模块支持的所有PCI设备的ID号。如果在系统中找到这样的设备,内核可以(在用户空间帮助下)自动插入对应的模块。

  • 基本的版本控制MODULE_INFO
    .modinfo段中总是会存储某些必不可少的版本控制信息,无论内核的版本控制特性是否启用。这使得可以从各种内核配置中区分出特别影响整个内核源代码的那些配置,这些可能需要一个单独的模块集合。在模块编译的第二阶段期间,下列代码会链接到每个模块中:

    //module.mod.c 
    MODULE_INFO(vermagic, VERMAGIC_STRING);
    

    VERMAGIC_STRING是一个字符串,表示内核配置的关键特性:

    //include/linux/vermagic.h
    #define VERMAGIC_STRING 						\
        UTS_RELEASE " "							\
        MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT 			\
        MODULE_VERMAGIC_MODULE_UNLOAD MODULE_ARCH_VERMAGIC
    

    内核自身和每个模块中都会存储VERMAGIC_STRING的一份副本。只有内核与模块存储的两个字符串匹配时,模块才能加载。这意味着模块和内核在以下配置方面必须是一致的:

    1. SMP配置(是否启用)
    2. 抢占配置(是否启用);
    3. 使用的编译器版本;
    4. 特定于体系结构的常数。

    在IA-32系统上,处理器类型用作特定于体系结构的常数,因为不同处理器可用的特性可能相去甚远。例如,如果模块编译时特意对Pentium 4处理器进行优化,那么可能无法插入为Athlon处理器编译的内核中

    内核版本也会存储,但在比较时会忽略。内核版本不同的模块,只要剩余的版本字符串匹配,仍然可以装载,不会有问题。例如,2.6.0版本的模块可以载入2.6.10版本的内核

7.3.4插入模块,sys_init_module系统调用

init_module系统调用是用户空间和内核之间用于装载新模块的接口

//kernel/module.c
asmlinkage long
sys_init_module(void __user *umod,
		unsigned long len,
		const char __user *uargs)

从用户空间的角度来看,插入一个模块非常简单,只需读入模块的二进制代码,并发出一个系统调用

  1. 系统调用的实现init_module
    在这里插入图片描述

  2. 加载模块load_module
    load_module函数做的工作:

    1. 从用户空间复制模块数据(和参数)到内核地址空间中的一个临时内存位置。各ELF段的相对地址替换为该临时映像的绝对地址。
    2. 查找各个(可选)段的位置。
    3. 确保内核和模块中版本控制字符串和struct module的定义匹配。
    4. 将存在的各个段分配到其在内存中的最终位置。
    5. 重定位符号并解决引用。链接到模块符号的任何版本控制信息都会被注意到。
    6. 处理模块的参数
  3. 解决引用resolve_symbol

    • sys_init_module 加载模块系统调用
      • load_module 加载解析模块
        • simplify_symbols 处理模块中的符号
          • resolve_symbol 解决未定义符号

    在这里插入图片描述

7.3.5移除模块sys_delete_module

系统调用sys_delete_module
在这里插入图片描述

//kernel/module.c
//从内核移除模块
asmlinkage long
sys_delete_module(const char __user *name_user, unsigned int flags)

7.4自动化与热插拔

模块不仅可以根据用户指令或自动化脚本装载,还可以由内核自身请求装载。这种装载机制,在下面两种情况下很有用处。
(1) 内核确认一个需要的功能当前不可用。例如,需要装载一个文件系统,但内核不支持。内核可以尝试加载所需的模块,然后重试装载文件系统。
(2) 一个新设备连接到可热插拔的总线(USB、FireWire、PCI等)。内核检测到新设备并自动装载包含适当驱动程序的模块。

在这两种情况下,内核都依赖用户空间的实用程序。根据内核提供的信息,实用程序找到适当的模块并按惯例将其插入内核

7.4.1kmod实现的自动加载request_module

在内核发起的模块自动装载特性中,kernel/kmod.c中的request_module是主要的函数。模块的名称(或一般占位符,这是一个服务名称,并不与特定硬件相关。例如,内核确定需要一个网络模块支持某种协议族,但却无法链接到内核中。因为它只知道该协议族的编号,而不是支持该协议族编号的模块名称,因此内核使用net-pf-X作为模块名,其中X表示协议族编号。modules.alias文件为特定协议族分配适当的模块名。例如net-pf-24对应到pppoe。)需要传递给该函数

请求模块的操作必须显式建立在内核中,逻辑上一般出现在以下场合:内核因为没有可用的驱动程序而导致分配特定的资源失败。目前,内核中此类场景大约有100处左右。例如,IDE驱动程序在探测现存的设备时会尝试加载设备所需的驱动程序。为此必须直接指定所需驱动程序的模块名:

//drivers/ide/ide-probe.c
if (drive->media == ide_disk)
	request_module("ide-disk");
...
if (drive->media == ide_floppy)
	request_module("ide-floppy");

如果特定的协议族不可用,则内核必须设法发出一个一般性的请求:

//net/socket.c
if (net_families[family] == NULL)
	request_module("net-pf-%d", family);

尽管在较早的内核版本(2.0及以前)中自动装载模块是由一个独立的守护进程负责,该守护进程必需在用户空间中显式启动,而现在该特性由内核实现,当然还需要用户空间中的实用程序插入模块。默认情况下会使用/sbin/modprobe。读者可以参阅与系统管理方面与此相关的大量文档

request_module的代码流程图:
在这里插入图片描述

//kernel/kmod.c
//请求加载应用层的ko模块文件
int request_module(const char *fmt, ...)

工作队列内核线程khelper,函数为worker_thread

7.4.2热插拔

在新设备连接到可热插拔的总线(或移除)时,内核再次借助用户空间应用程序来确保装载正确的驱动程序。与通常插入模块的过程相比,这里有必要执行几个额外的任务(例如,必须根据设备标识字符串,找到正确的驱动程序,或必须进行一些配置工作)。因此,这里用另一个工具(通常是/sbin/udevd)代替了modprobe,向用户空间提供消息

在内核以前的版本中,/sbin/hotplug是唯一的热插拔工具程序。随着设备模型的引入和该模型在内核版本2.6开发期间逐渐成熟,udevd现在是大多数发行版首选的方法。尽管如此,内核确实提供的是一般性的消息,并不绑定到用户空间中的特定机制。在某些情况下,内核仍然调用uevent_helper中注册的程序,它可以设置为/sbin/hotplug,该设置可以通过/proc/sys/kernel/hotplug访问。将该值设置为空串,则停用该机制。由于它只在启动期间或某些非常特殊的配置下(主要是完全停用了网络的系统)有用,我不会过多考虑它

要注意,内核不仅在设备插入与移除时向用户空间提供消息,实际上内核在很多一般事件发生时,都会发送消息。例如,在一个新硬盘连接到系统时,内核不仅提供有关该事件的信息,还发送通知,提供该设备上已经找到的分区信息。设备模型的每部分都可以向用户层发送注册和撤销注册事件。因此,实际上内核可能发送的消息,组成了一个相当庞大和广泛的集合,我没有详细地讲述。

举个例子,来说明基本的机制。考虑将一个USB存储设备附接到系统,但此时提供USB 海量存储(mass storage)支持的模块尚未载入内核。此外,该发行版想要自动地将该设备装载到文件系统中,以便用户可以立即访问它。为此,需要执行以下步骤

  1. USB宿主机控制器在总线上检测到一个新设备并报告给其设备驱动程序。宿主机控制器分配一个新的device实例并调用usb_new_device注册它
  2. usb_new_device触发对kobject_uevent的调用。该函数对所述对象kobject实例,调用其中注册的特定于子系统的事件通知程序
  3. 对USB设备对象,usb_uevent用作通知函数。该函数准备一个消息,其中包含了所有必要的信息,使得udevd能够对新的USB海量存储设备的插入,作出适当反应
  • usb_new_device
    • device_add
      • kobject_uevent

用户空间的udevd守护进程可以检查来源于内核的所有消息。注意以下通信日志,其内容取自一个新的USB存储棒插入系统时

root@meitner # udevmonitor --environment 
... 
UEVENT[1201129806.368892] add /devices/pci0000:00/0000:00:1a.7/usb7/7-4/7-4:1.0 
(usb) 
ACTION=add 
DEVPATH=/devices/pci0000:00/0000:00:1a.7/usb7/7-4/7-4:1.0 
SUBSYSTEM=usb 
DEVTYPE=usb_interface 
DEVICE=/proc/bus/usb/007/005 
PRODUCT=951/1600/100 
TYPE=0/0/0 
INTERFACE=8/6/80 
MODALIAS=usb:v0951p1600d0100dc00dsc00dp00ic08isc06ip50 
SEQNUM=1830

第一个消息由上述的usb_uevent函数产生。每个消息都由一些标识符/值对组成,这些描述了内核内部所进行的操作。由于一个新的设备添加到系统,ACTION的值是addDEVICE表示该设备在USB文件系统中的位置,可以据此查找该设备的有关信息,而PRODUCT提供一些有关厂商和设备的信息。这里最重要的字段是INTERFACE,它确定了新设备所属接口的类别USB标准对海量存储设备分配的类别是8

//include/linux/usb/ch9.h
#define USB_CLASS_MASS_STORAGE		8

MODALIAS字段包含有关该设备的所有一般信息。这些信息编码在一个字符串中,其设计显然不适于人眼阅读,但计算机很容易解析。其生成过程如下(add_uevent_var是一个辅助函数,用于向热插拔消息添加一个新的标识符/值对)。

//drivers/usb/core/message.c
if (add_uevent_var(env,
		   "MODALIAS=usb:v%04Xp%04Xd%04Xdc%02Xdsc%02Xdp%02Xic%02Xisc%02Xip%02X",
		   le16_to_cpu(usb_dev->descriptor.idVendor),
		   le16_to_cpu(usb_dev->descriptor.idProduct),
		   le16_to_cpu(usb_dev->descriptor.bcdDevice),
		   usb_dev->descriptor.bDeviceClass,
		   usb_dev->descriptor.bDeviceSubClass,
		   usb_dev->descriptor.bDeviceProtocol,
		   alt->desc.bInterfaceClass,
		   alt->desc.bInterfaceSubClass,
		   alt->desc.bInterfaceProtocol))

通过比较MODALIAS值和各个模块提供的别名udevd可以找到需要插入的模块。在这里,应该插入usb-storage模块,因为以下别名与需求匹配:

wolfgang@meitner> /sbin/modinfo usb-storage 
... 
alias: usb:v*p*d*dc*dsc*dp*ic08isc06ip50* 
...

类似于普通的正则表达式,星号是占位符,表示任意值,而该别名最后的一部分(ic08isc06ip50*)与MODALIAS值相同(ic%02Xisc%02Xip%02X)。因而别名是匹配的,udevd可以将usb-storage模块插入到内核中。udevd如何了解一个给定模块有哪些别名呢?它依赖depmod程序,该程序扫描所有可用的模块,提取别名信息,并存储到文本文件/lib/modules/2.6.x/modules.alias中。

但故事到这里尚未结束。在USB 海量存储模块已经插入内核之后,块设备层识别出该设备和其中包含的分区。 这又产生了另一个通知

root@meitner # udevmonitor
... 
UDEV [1201129811.890376] add /block/sdc/sdc1 (block) 
UDEV_LOG=3 
ACTION=add 
DEVPATH=/block/sdc/sdc1 
SUBSYSTEM=block 
MINOR=33 
MAJOR=8 
PHYSDEVPATH=/devices/pci0000:00/0000:00:1a.7/usb7/7-4/7-4:1.0/host7/target7:0:0/7:0:0:0 
PHYSDEVBUS=scsi 
SEQNUM=1837 
UDEVD_EVENT=1 
DEVTYPE=partition 
ID_VENDOR=Kingston 
ID_MODEL=DataTraveler_II 
ID_REVISION=PMAP 
ID_SERIAL=Kingston_DataTraveler_II_5B67040095EB-0:0 
ID_SERIAL_SHORT=5B67040095EB 
ID_TYPE=disk 
ID_INSTANCE=0:0 
ID_BUS=usb 
ID_PATH=pci-0000:00:1a.7-usb-0:4:1.0-scsi-0:0:0:0 
ID_FS_USAGE=filesystem 
ID_FS_TYPE=vfat 
ID_FS_VERSION=FAT16 
ID_FS_UUID=0920-E14D 
ID_FS_UUID_ENC=0920-E14D 
ID_FS_LABEL=KINGSTON 
ID_FS_LABEL_ENC=KINGSTON 
ID_FS_LABEL_SAFE=KINGSTON 
DEVNAME=/dev/sdc1 
DEVLINKS=/dev/disk/by-id/usb-Kingston_DataTraveler_II_5B67040095EB-0:0-part1 
/dev/disk/by-path/pci-0000:00:1a.7-usb-0:4:1.0-scsi-0:0:0:0-part1 
/dev/disk/by-uuid/0920-E14D /dev/disk/by-label/KINGSTON

该消息提供了新检测到的分区名称(/dev/sdc1)和分区上找到的文件系统(vfat)的有关信息。对于udevd来说,该信息已经足够用于自动装载文件系统,这样USB存储棒就可以使用了。

7.5版本控制

不断改变的内核源代码当然对驱动程序和模块的程序设计有一些影响,特别是只提供二进制代码的专有的驱动程序,本节将讨论这种影响

在实现新特性或修订总体设计时,通常必须修改内核各个部分之间的接口,以处理新的情况或支持性能和设计方面的改进。当然,开发者会尽可能将改动限制到驱动程序不直接使用的那些内部函数上。但这并不排除偶尔修改“公开的”接口。很自然,模块接口也会受到此类修改的影响。

在驱动程序以源代码形式提供时,这不成问题,只要找到一个勤勉的内核黑客将代码改编为适应新的结构即可。对大多数驱动程序来说,相关的工作量都可以按天计算(即使不能按小时计算)。由于不再有明确的“开发版内核”的概念,在内核的两个稳定修订版之间引入接口改变是不可避免的。但由于in-tree代码更新很容易,所以这不会产生问题。

但对于由厂商发布、只提供二进制代码的驱动程序来说,情况会有所不同。用户不得不依赖厂商的信誉,等待新驱动程序的开发和发布。这种情况引起了一整套问题,其中有两个是技术上的,我们对此比较感兴趣。

  • 如果模块使用了一个废弃的接口,不仅会损害模块的正常功能,而且系统很可能崩溃
  • SMP和单处理器系统的接口不同,需要两个二进制版本。如果装载了错误的版本,同样可能导致系统崩溃

因此,显然需要引入模块版本控制功能。最简单的解决方案是引入一个常数,并且内核和模块都保存该常数。每次改变一个接口,常数的值都加1。除非模块和内核中接口的版本号相同,否则内核不会接受模块。这可以解决版本问题。理论上该方法是可行的,但还不够全面。如果改变的是模块未使用的接口,尽管模块完全可以运转,却不能再装载。

因此,引入了一种细粒度的方法,从而考虑到内核中各个例程的改变。具体地,我们无需考虑实际的模块和内核实现。需要考虑的问题是,如果模块要在不同的内核版本下运作,那么其调用的接口不能改变。所用的方法极其简单,但却能很好地解决版本控制问题

7.5.1校验和方法

基本思想是使用函数或过程的参数,生成一个CRC校验和。该校验和是一个4字节数字,十六进制记数法需要8个字母。如果函数接口修改了,校验和也会发生变化。这使得内核能够推断出新版本已经不再兼容旧版本。

校验和在数学上不是唯一的,不同过程可能映射到同一个校验和,因为过程参数的组合(实际上,是一个无穷大数)比可用的校验和(即,232个)要多很多。实际上这不会成为问题,因为函数接口的几个参数改变前后校验和相同的可能性很小。

  1. 生成校验和
    内核源代码附带的genksym工具在编译时自动创建,用于生成函数的校验和。为说明其工作方式,我们使用以下头文件,其中包含一个导出函数的定义

    #include<linux/sched.h> 
    #include<linux/module.h> 
    #include<linux/types.h> 
    int accelerate_task(long speedup, struct task_struct *task); 
    EXPORT_SYMBOL(accelerate_task);
    

    该函数定义包含了一个复合结构指针作为参数,这使得genksyms的工作更为困难。当该结构的定义改变时,函数的校验和也会改变。为分析该结构的内容,假定其内容必须是已知的。因此,genksyms的输入只能是预处理器已经处理过的文件,其中已经包括了相关定义所在的头文件。生成导出函数的校验和,需要以下调用:

    wolfgang@meitner> gcc -E test.h -D__GENKSYMS__ -D__KERNEL__ | genksyms > test.ver
    

    test.ver的内容如下:

    wolfgang@meitner> cat test.ver
    __crc_accelerate_task = 0x3341f339;
    

    如果accelerate_task的定义改变,那么校验和也会改变,例如将第一个参数改为整数。在这种情况下,genksym计算的校验和是0xbb29f607
    如果一个文件中定义了几个符号,genksyms生成同样数目的校验和。以下是对vfat模块生成的结果文件的内容示例:

    wolfgang@meitner> cat .tmp_vfat.ver 
    __crc_vfat_create = 0x50fed954 ; 
    __crc_vfat_unlink = 0xe8acaa66 ; 
    __crc_vfat_mkdir = 0x66923cde ; 
    __crc_vfat_rmdir = 0xd3bf328b ; 
    __crc_vfat_rename = 0xc2cd0db3 ; 
    __crc_vfat_lookup = 0x61b29e32 ; 
    

    这是一个脚本,用于链接器ld,其在编译过程中的重要性将在下文解释

  2. 将校验和编译到模块和内核中
    内核必须将genksym提供的信息合并到模块的二进制代码中,供后续使用,后面将讨论具体的做法。

    • 导出函数
      __EXPORT_SYMBOL内部调用了__CRC_SYMBOL宏,这在7.3.3节讨论过。在启用版本控制时,后者定义如下:

      //在对导出函数启用内核版本控制特性时,会使用__CRC_SYMBOL;否则该宏定义为空串
      #define __CRC_SYMBOL(sym, sec)					\
          extern void *__crc_##sym __attribute__((weak));		\
          static const unsigned long __kcrctab_##sym		\
          __attribute_used__					\
          __attribute__((section("__kcrctab" sec), unused))	\
          = (unsigned long) &__crc_##sym;
      

      在调用EXPORT_SYMBOL(get_shorty)时,其内部调用了__CRC_SYMBOL宏,如下所示:

      extern void *__crc_get_richard __attribute__((weak)); 
      static const unsigned long __kcrctab_get_shorty 
          __attribute_used__ 
          __attribute__((section("__kcrctab" ""), unused)) = 
          (unsigned long) &__crc_get_shorty;
      

      因此,内核在模块的二进制文件中建立了两个对象:

      1. 未定义的void指针__crc_function,位于模块的普通符号表中
      2. 指向刚定义的变量的一个指针krcrtab_function,存储在文件的__kcrctab段

      weak属性创建一个(弱)链接的变量。如果该变量未指定值,就不会报错(普通变量会报错)。如果未指定值,该变量会被忽略。这是必要的,因为genksyms对某些符号并不生成校验和

      在模块链接时(模块编译的第一个阶段),链接器使用genksyms生成的.ver文件作为一个脚本。这将脚本中的值,提供给__crc_function符号。内核稍后会读入这些值。如果有另一个模块引用了其中某个符号,内核会使用此处给出的信息,来确保两个模块都引用同一版本的符号。

    • 未解决的引用
      当然只存储模块导出函数的校验和是不够的。更重要的是,要注意到所有使用的符号的校验和,因为在模块插入内核时,必须要将用到的符号的校验和与当前内核中可用的版本相比。

      编译的第一阶段,所有模块源文件都编译为.o目标文件,其中包含导出符号的版本信息,但不包括引用的符号

      模块编译的第二阶段,需要执行以下步骤,将所有引用到的符号的版本信息,插入到模块的二进制文件中。

      (1) 如下调用modpost

      wolfgang@meitner> scripts/modpost vmlinux module1 module2 module3 ... modulen 
      

      这里不仅指定了内核映像的名称,还包括所有先前生成的.o模块二进制文件的名称。modpost是内核源代码附带的一个实用程序。它生成两种列表:一个是全局列表,包含所有可用的符号(无论由内核或模块提供);另一种列表特定于具体模块,每个模块都有一个,包含所有未解决的引用。
      (2) modprobe接下来遍历所有模块,试图在所有符号的列表中找到未解决的引用。如果符号由内核自身或另一个模块定义,那么会成功查找到。
      对每个模块创建一个新的module.mod.c文件。其内容如下所示(对vfat模块生成的文件):

      wolfgang@meitner> cat vfat.mod.c 
      #include <linux/module.h> 
      #include <linux/vermagic.h> 
      #include <linux/compiler.h> 
      MODULE_INFO(vermagic, VERMAGIC_STRING); 
      struct module __this_module 
      __attribute__((section(".gnu.linkonce.this_module")))= {
          .name = KBUILD_MODNAME, 
          .init = init_module, 
          #ifdef CONFIG_MODULE_UNLOAD 
          .exit = cleanup_module, 
          #endif 
          .arch = MODULE_ARCH_INIT, 
      }; 
      static const struct modversion_info ____versions[] 
      __attribute_used__
      __attribute__((section("__versions"))) = 
      {
          0x8533a6dd, "struct_module" , 
          0x21ab58c2, "fat_detach" , 
          0xd8ec2862, "__mark_inode_dirty" , 
          ... 
          0x3c15a491, "fat_dir_empty" , 
          0x9a290a43, "d_instantiate" , 
      }; 
      static const char __module_depends[] 
      __attribute_used__ 
      __attribute__((section(".modinfo"))) = 
      {
          "depends=fat";
      }
      

      在该文件中定义了两个变量,分别位于二进制文件的两个不同段

      (a) 模块引用的所有符号连同对应的校验和,从内核或另一个模块的符号定义中复制过来,保存在__modversions段的modversions_info数组中。在插入模块时,该信息用于检查当前运行内核是否有所需的正确版本的符号。
      (b) 当前模块所依赖的所有模块的列表,位于.modinfo段的module_depends数组中。在我们的例子中,VFAT模块依赖FAT模块。modprobe建立依赖列表是很简单的。如果模块A引用的符号在内核自身中没有定义,而是由另一个模块B定义,那么将模块B的名称添加到模块A的依赖列表中。
      (3) 最后一步,内核将结果module.mod.c文件编译为目标文件,并使用ld将其与模块现存的.o目标文件链接起来。结果文件命名为module.ko,这是最终的内核模块,可以使用insmod装载。

7.5.2版本控制函数

内核使用辅助函数check_version确定模块所需版本的符号是否与内核中可用符号的版本匹配。
该函数需要几个参数:指向模块段头的一个指针(sechdrs),__version段的索引,将要处理符号的名称(symname),指向模块数据结构的一个指针(mod),指向内核提供的对应符号校验和的一个指针(crc),该校验和在解决该符号时由__find_symbol提供

//kernel/module.c
//确定模块所需版本的符号是否与内核中可用符号的版本匹配
static int check_version(Elf_Shdr *sechdrs,//指向模块段头的指针
			 unsigned int versindex,//__version段的索引号
			 const char *symname,//将要处理的符号名称
			 struct module *mod, //指向模块数据结构的一个指针
			 const unsigned long *crc)//指向内核提供的对应符号校验和的一个指针,该校验和在解决该符号时由__find_symbol提供

总结

系统启动或新模块安装后或运行 depmod 工具生成模块依赖关系保存到 /lib/modules/version/modules.dep 中
加载模块时使用 modprobe 会读入依赖文件的内容,搜索描述目标模块的行,搜集并建立所有依赖模块的列表,插入到内核的实际任务,则委托给insmod工具

depmod 在模块安装时产生了文件 /lib/modules/version/System.map,该文件列出了内核导出的所有符号。如果其中包含了某个模块中未解决的引用,在模块装载时引用将自动解决.。如果未解决的引用无法在该文件或其他模块中找到,则模块不能添加到内核中

modinfo 工具能查看模块二进制文件中的信息(在 .modinfo 段中)

modprobe 读入 /lib/modules/version/modules.dep 搜索目标依赖关系,确认后调用insmod插入模块

mount 命令触发系统调用 sys_init_module,如果模块有其他依赖的模块,会使用 request_module 函数,启动kmod进程通过 modprobe 工具加载依赖的模块.

模块版本管理通过 genksym 工具,使用函数参数,生成一个CRC校验和.如果函数接口修改了,校验和也会发生变化.校验和信息保存在二进制模块和内核中

usb设备动态加载过程:
usb设备插入后,usb主控制器检测到新设备,报告给其设备驱动程序,控制器分配新的device实例并调用usb_new_device注册它,usb_new_device通过kobject_uevent调用特定的通知函数usb_uevent通过netlink,创建消息(包含了设备别名),用户空间的udevd收到消息通过depmod程序扫描可用的模块提取别名并加载对应的驱动

插入模块:
模块编译后生成module.mod.c文件,其中定义了module结构体,并将 module->init 回调函数与模块中 MODULE_INIT(xxx_init) 指定的 xxx_init 函数绑定

用户空间读入模块代码,并发出 init_module 系统调用,该系统调用从用户空间复制模块数据到内核空间处理符号和重定位,将module结构体加入链表,并调用 module->init 回调函数

驱动的该回调函数将设备结构体插入对应总线的设备链表中,然后在另一条链表中通过总线提供的匹配函数进行比较,成功后调用驱动的probe回调函数

内核主动加载模块调用 request_module 函数,它调用了用户空间的 modprobe 程序加载模块

热插拔事件
内核将在什么时候产生热插拔事件呢?当驱动程序将kobject注册到设备驱动模型时, 会产生这些事件。也就是当内核调用kobject_add()和kobject_del()函数时,会产生热插拔事件。热插拔事件产生时,内核会根据kobject的kset指针找到所属的kset结构体,执行kset结构体中uevent_ops包含的热插拔函数

不管是平台总线还是IIC总线都都有这样的调用路线:

当系统发现了新设备或者新驱动就会掉用相应总线的Match()进行匹配,当找到后就会掉用相对应的总线的Probe函数,最后Probe函数再调用驱动自己的Probe函数

虽然平台总线和IIC总线的实现有些不同,但是大体使一样的

扩展

=========================================

涉及的命令和配置:

nm工具可用于产生模块(或任意目标文件)中所有外部函数的列表
file命令

modprobe在识别出模块所依赖的模块之后,在内核也会使用insmod
insmod加载模块到内核中
depmod用于计算系统的各个模块之间的依赖关系。每次系统启动时或新模块安装后,通常都会运行该程序。找到的依赖关系保存在一个列表中。默认情况下,写入文件/lib/modules/version/modules.dep
modinfo查看模块的额外信息

内核编译选项KALLSYMS_ALL,让nm命令能看到未解决的变量引用,否则只能看到函数符号

/proc/kallsyms 内核函数的内存地址和对应函数名

内核kmod进程,kmod并不是一个永久性的守护进程,内核会按需启动它来使用modprobe工具加载所需的用户空间的ko文件

全局变量 modules 链表头,保存所有模块

CONFIG_KALLSYMS 是一个配置选项(但只用于嵌入式系统,在普通计算机上总是启用的),启用该选项后,将在内存中建立一个列表,保存内核自身和加载模块中定义的所有符号(否则只存储导出的函数)。如果oops消息(如果内核检测到与背离常规的行为,例如反引用NULL指针)不仅输出十六进制数字(地址),还要输出涉及函数的名称,那么该选项就很有用

CONFIG_MODULE_UNLOAD,没有这个选项或者这个选项配置为“n”就不能卸载任何模块
CONFIG_MODVERSIONS启用版本控制。该选项防止将接口定义与当前版本不再匹配的废弃模块载入内核。7.5节会更详细地讨论该选项。
CONFIG_MODULE_FORCE_UNLOAD允许模块从内核强制移除,即使仍然有引用该模块的地方,或其他模块正在使用其代码,也是如此。在系统正常运转时,绝不需要这种蛮干法,但它可能在开发系统期间用到
CONFIG_KMOD选项使内核在需要模块时自动加载。这需要与用户空间的一些交互

readelf -S module.ko可以列出模块中的所有段

GNU C编译器的attribute指令,用于将数据放置到指定的段中。该指令的其他用法,在附录C中讲述

/proc/sys/kernel/modprobe request_module函数调用的应用层加载ko文件命令,默认为/sbin/modprobe
全局变量modprobe_path保存默认的应用层加载ko文件命令,为/sbin/modprobe

/proc/sys/kernel/hotplug 内核向用户空间传递消息的程序,以前版本为/sbin/hotplug,现在通常是/sbin/udevd,如usb插入时,内核通知用户空间程序,将该值设置为空串,可以停用该机制

udevmonitor,udevinfo是RHEL5下的命令,在RHEL6(udev 147)中统一用udevadm [option]来管理。udevm monitor.查看内核的uevent消息

  1. udevinfo命令可以查询udev数据库的设备信息,udevadm info -a -p /block/sda
  2. udevadm test会针对一个设备,在不需要 uevent 触发的情况下模拟一次udev的运行,并输出查询规则文件的过程、所执行的行为、规则文件的执行结果。通常使用来调试规则文件(udevadm test /class/net/eth0)
  3. udevadmmonitor用于调试,监控udev的事件过程。(udevadm monitor)

source-tree 驱动程序源码KCONFIG树
in-tree 在源码KCONFIG树中的驱动程序
out-of-tree 不在源码KCONFIG树中的驱动程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值