11. 编写您自己的可加载内核模块

Linux 内核模块编程指南,由 Peter J Salzman、Michael Burian 和 Ori Pomerantz 编写,是对编写您自己的 LKM 的完整解释。本书也有印刷版。它有两个版本:一个用于 Linux 2.4,另一个用于 2.6。

曾经,本文档的 Linux 2.4 版本已经相当过时,并且包含一两个错误。

以下是关于编写 LKM 的一些事项,至少曾经不在那里。请告知 LKM HOWTO 的作者,看看这是否仍然属实。如果不是,他可以从 LKM HOWTO 中删除此部分。

11.1. 更简单的 hello.c

Lkmpg 给出了世界上最简单的 LKM 示例,hello-1.c。但它并没有尽可能简单,并且依赖于您的系统上以某种方式设置的内核消息传递才能看到它工作。最后,该程序要求您在编译命令中包含-D选项才能工作,因为它没有在源代码中定义一些宏,而这些宏的定义应该在那里。

这是一个改进的世界上最简单的 LKM,hello.c.

/* hello.c 
 * 
 * "Hello, world" - the loadable kernel module version. 
 *
 * Compile this with 
 *
 *          gcc -c hello.c -Wall
 */

/* Declare what kind of code we want from the header files */
#define __KERNEL__         /* We're part of the kernel */
#define MODULE             /* Not a permanent part, though. */

/* Standard headers for LKMs */
#include <linux/modversions.h> 
#include <linux/module.h>  

#include <linux/tty.h>      /* console_print() interface */

/* Initialize the LKM */
int init_module()
{
  console_print("Hello, world - this is the kernel speaking\n");
  /* More normal is printk(), but there's less that can go wrong with 
     console_print(), so let's start simple.
  */

  /* If we return a non zero value, it means that 
   * init_module failed and the LKM can't be loaded 
   */
  return 0;
}


/* Cleanup - undo whatever init_module did */
void cleanup_module()
{
  console_print("Short is the life of an LKM\n");
}

使用简单的命令编译它

$ gcc -c -Wall -nostdinc -I /usr/src/linux/include hello.c

The-I以上假设您拥有构建基础内核(您希望将模块加载到其中的内核的基础内核)的源代码,并且位于传统位置hello.c)位于传统位置,/usr/src/linux。如果你的确够执着到在你的基础内核中使用符号版本控制,那么你最好也对该内核源代码运行 'make dep',因为那会构建 .ver 文件,这些文件会更改所有符号的名称。

但请注意,通常情况下, 会在那里安装内核头文件,而且通常,安装在那里的头文件是错误的。当您使用从发行版 CD 加载的内核时,您通常需要单独加载它的头文件。为了安全起见,如果您正在玩编译 LKM,您真的应该编译您自己的内核,这样您才能确切地知道您正在使用什么,并且可以绝对确定您正在使用匹配的头文件。

The-nostdinc选项并非绝对必要,但这样做是正确的。它将使您免于麻烦,并且还会提醒您,您可能在脑海中与 C 本身融合在一起的标准 C 库的服务,内核代码是不可用的。-nostdinc表示不要在包含文件搜索路径中包含“标准”目录。这意味着,最值得注意的是,/usr/include.

The-c选项表示您只想创建一个目标 (.o) 文件,而不是 gcc 的默认行为,后者是创建目标文件,然后将其与一些其他标准目标文件链接,以创建一个适合在用户进程中执行的内容。由于您不会执行此模块,而是将其添加到内核,因此链接阶段将完全不合适。

-Wall(这使得编译器警告您许多种类的可疑代码)显然不是必要的,但此程序不应生成任何警告。如果它生成了警告,您需要修复一些东西。

11.2. 使用内核构建系统

Lkmpg 包含构建(编译)LKM 的精细说明(除了 __KERNEL__ 宏和通常的 MODULE 宏应该在源代码中定义,而不是像 Lkmpg 建议的那样使用-D编译器选项)。但值得一提的是,一些 Linux 内核程序员认为,构建 LKM 的唯一正确方法是将其添加到完整 Linux 源代码树的副本中,并使用现有的 Linux make 文件构建它,就像 Linux 本身包含的 LKM 一样。

这样做有优点。最大的优点是,当 Linux 程序员以影响您构建 LKM 方式的方式更改 LKM 与内核其余部分的接口时,您会得到保护。

另一方面,从代码管理角度来看,您可能会发现您确实需要将自己的代码和 Linux 分开,并且从编码角度来看,您确实需要了解您的代码如何被编译的所有复杂性,尤其是在它发生变化时。

11.3. Rubini 等人:《Linux 设备驱动程序》

关于编写设备驱动程序最受欢迎的书籍是 O'Reilly 出版的 Linux 设备驱动程序,作者是 Alessandro Rubini、Jonathan Corbet 和 Greg Kroah-Hartman。

即使您正在编写的 LKM 不是设备驱动程序,您也可以从本书中学到很多对您有帮助的东西。

本书的第一版涵盖了 Linux 2.0,并附有关于 2.2 中差异的注释。第二版(2001 年 6 月)涵盖了 Linux 2.4。第三版(2005 年 4 月)涵盖了 Linux 2.6。当然,如果您对 Linux 有任何了解,您就会知道像这样的书并不能完美地涵盖任何版本,因为 Linux 经常变化。第三版发布一个月后,当时的 Linux 2.6 与本书所写的 Linux 2.6 存在显着差异。

本书的第二版根据 FDL 发布。您可以在 http://www.xml.com/ldd/chapter/book/ 阅读它。第三版根据 Creative Commons Attribution-ShareAlike 许可条款发布,您可以在 http://lwn.net/Kernel/LDD3/ 找到它。

本书也有印刷版,在任何像样的技术书店都可以买到。

11.4. 模块使用计数

至关重要的是,内核在模块卸载后不要尝试引用模块的代码;即,您不得在模块正在使用时卸载它。正在使用的示例是设备驱动程序,为其打开了设备特殊文件。因为有一个为它打开的文件描述符,用户可能会读取设备,为了执行该读取,内核会想要调用设备驱动程序中的一个函数。您可以想象,如果在读取之前卸载该设备驱动程序模块,就会出现问题——内核会重用曾经包含读取子程序的内存,并且无法预测内核在认为它正在调用读取子程序时会分支到哪些指令。

在原始设计中,LKM 递增和递减其使用计数,以告知模块管理器是否可以卸载它。例如,如果它是一个文件系统驱动程序,它会在有人挂载它驱动的类型的文件系统时递增使用计数,并在卸载时递减它。

后来添加了一个更灵活的替代方案。您的 LKM 可以注册一个函数,每当模块管理器想知道是否可以卸载模块时,模块管理器都会调用该函数。如果该函数返回true值,则表示 LKM 正忙,无法卸载。如果它返回false值,则表示 LKM 处于空闲状态,可以卸载。模块管理器从调用模块繁忙函数之前一直持有大内核锁,直到其清理子程序返回或休眠之后,除非您做了一些奇怪的事情,否则这应该意味着您的 LKM 在您报告“不忙”的时间和您清理的时间之间不会变得繁忙。

那么如何注册模块繁忙函数呢?通过将其地址放在不幸命名的can_unload模块描述符(“struct module”)中的字段中。这个名字真的很不幸,因为它返回的布尔值与“可以卸载”的含义完全相反:如果模块管理器不能卸载 LKM,则为 true。

模块管理器确保在模块的初始化子程序返回或休眠之前,它不会尝试卸载模块,因此您可以安全地设置can_unload字段在初始化子程序中的任何位置,除了在休眠之后。

can_unload鲜为人知且很少使用。从 Linux 2.6 开始,它不再存在。

无论您使用传统的can_unload使用计数,在某些情况下,您都无法确定您的模块在仍在使用时不会被卸载。如果您的 LKM 创建了一个执行 LKM 代码的内核线程,则几乎不可能绝对确定该线程在 LKM 被卸载之前就消失了。您可以在 LKM 中提供地址的各种其他内核服务,这些服务不会正确地让您知道它们何时忘记了它们。

过去的问题比现在更严重。例如,过去,如果您的 LKM 创建了一个 proc 文件系统文件,您就无法阻止 LKM 在某些进程执行您的文件读取和写入例程时被卸载。这个问题和其他实例已通过让 LKM外部的代码理解它正在使用的地址可能位于 LKM 中来修复,因此必要时会递增和递减使用计数。在实现此功能的地方,您经常会看到一个名为“owner”的结构成员,它是 LKM 的句柄(即 struct module 地址)。

这些问题可能会在未来的 Linux 版本中得到修复。在此之前,您只能碰碰运气。有些人认为这些类型的问题太难修复,以至于 Linux 的正确设计就是让永远不可能卸载 LKM。从 Linux 2.6 开始,CONFIG_MODULE_UNLOAD 内核构建配置选项决定是否允许模块卸载。