Linux内核模块编程指南(一)

翻译来自:
http://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
本系列文章还有:
Linux内核模块编程指南(一)
Linux内核模块编程指南(二)
Linux内核模块编程指南(三)
Linux内核模块编程指南(四)

Peter Jay Salzman
Michael Burian
Ori Pomerantz
Copyright © 2001 Peter Jay Salzman
2007-05-18 ver 2.6.4

Linux内核模块编程指南是一本免费的书; 您可以根据开放软件许可1.1版的条款复制和/或修改它。 您可以在http://opensource.org/licenses/osl.php上获取此许可证的副本。
本书的发行是希望它有用,但没有任何保证,甚至没有适销性或适用于特定用途的默示保证。

作者鼓励广泛分发本书用于个人或商业用途,前提是上述版权声明保持不变,且该方法符合开放软件许可的规定。 总之,您可以免费复制和分发本书或获取利润。 作者不得以任何媒介,物理或电子形式复制本书,不需要明确许可。

本文档的衍生作品和翻译必须放在开放软件许可下,原始版权声明必须保持不变。 如果您为本书提供了新材料,则必须使材料和源代码可用于您的修订。 请直接向文档维护人员Peter Jay Salzman < [email protected] >提供修订和更新。 这将允许合并更新并为Linux社区提供一致的修订。

第1章简介

什么是内核模块?

所以,你想编写一个内核模块。 你知道C,你已经编写了一些正常的程序作为进程运行,现在你想要到达实际操作的位置,一个狂野指针可以消灭你的文件系统,核心转储意味着重启。

什么是内核模块? 模块是可以根据需要加载和卸载到内核中的代码片段。 它们扩展了内核的功能,而无需重启系统。 例如,一种类型的模块是设备驱动程序,它允许内核访问连接到系统的硬件。 没有模块,我们必须构建单片内核并将新功能直接添加到内核映像中。 除了拥有更大的内核之外,这还有一个缺点,即每次我们想要新功能时都需要我们重建和重启内核。

模块如何进入内核?

您可以通过运行lsmod来查看已经加载到内核中的模块, lsmod通过读取文件/proc/modules来获取其信息。

这些模块如何进入内核? 当内核需要一个不驻留在内核中的特性时,内核模块守护进程kmod [1]执行modprobe来加载模块.modprobe以两种形式之一传递一个字符串:

模块名称,如softdog或ppp 。

一个更通用的标识符,如char-major-10-30 。

如果modprobe被赋予通用标识符,它首先在文件/etc/modprobe.conf中查找该字符串。 [2]如果找到如下的别名行:

alias char-major-10-30 softdog

它知道通用标识符引用模块softdog.ko 。

接下来,modprobe查看文件 /lib/modules/version/modules.dep ,以查看是否必须加载其他模块才能加载所请求的模块。 该文件由depmod -a创建,包含模块依赖项。 例如, msdos.ko要求fat.ko模块已加载到内核中。 如果另一个模块定义了所请求模块使用的符号(变量或函数),则请求的模块依赖于另一个模块。

最后,modprobe使用insmod首先将任何必备模块加载到内核中,然后加载所请求的模块。 modprobe将insmod指向 /lib/modules/version /[3] ,模块的标准目录。 insmod对于模块的位置是相当愚蠢的,而modprobe知道模块的默认位置,知道如何找出依赖关系并以正确的顺序加载模块。 例如,如果要加载msdos模块,则必须运行

insmod /lib/modules/2.6.11/kernel/fs/fat/fat.ko 
insmod /lib/modules/2.6.11/kernel/fs/msdos/msdos.ko 

或者

modprobe msdos

我们在这里看到的是: insmod要求你传递完整的路径名并以正确的顺序插入模块,而modprobe只取名字,没有任何扩展名,并通过解析/ lib找出它需要知道的所有内容/modules/version/modules.dep 。

Linux发行版提供modprobe,insmod和depmod作为名为module-init-tools的包。 在以前的版本中,该包称为modutils。 一些发行版还设置了一些包装器,允许两个包并行安装并做正确的事情,以便能够处理2.4和2.6内核。 用户不应该关心细节,只要他们运行这些工具的最新版本。

现在你知道模块如何进入内核了。 如果您想编写依赖于其他模块的自己的模块(我们称之为“堆叠模块”),那么故事还有更多内容。 但这将不得不等待未来的一章。 在解决这个相对高级别的问题之前,我们需要做很多工作。

在我们开始之前

在我们深入研究代码之前,我们需要解决一些问题。 每个人的系统都不同,每个人都有自己的沟槽。 让你的第一个“hello world”程序正确编译和加载有时候会成为一种技巧。 请放心,在您第一次克服了最初的障碍后,此后将顺利进行。

Modversioning

除非在内核中启用CONFIG_MODVERSIONS ,否则如果引导其他内核,则不会加载为一个内核编译的模块。 我们不会在本指南的后面部分进行模块版本控制。 在我们介绍modversions之前,如果您运行的是启用了modversion的内核,则指南中的示例可能无效。 但是,大多数现有的Linux发行版内核随之打开。 如果由于版本控制错误而无法加载模块,请在关闭modversion的情况下编译内核。

使用X.

强烈建议您输入,编译和加载本指南讨论的所有示例。 强烈建议您从控制台执行此操作。 你不应该在X中处理这些东西。

模块不能像printf()那样打印到屏幕上,但它们可以记录信息和警告,最终会在屏幕上打印,但只能在控制台上打印。 如果从xterm中修改模块,将记录信息和警告,但仅记录日志文件。 除非查看日志文件,否则不会看到它。 要立即访问此信息,请从控制台执行所有工作。

编译问题和内核版本

通常情况下,Linux发行版将分发以各种非标准方式修补的内核源代码,这可能会带来麻烦。

一个更常见的问题是某些Linux发行版分发不完整的内核头文件。 您需要使用Linux内核中的各种头文件编译代码。 Murphy定律指出缺少的标题正是模块工作所需的标题。

为了避免这两个问题,我强烈建议您下载,编译并启动到可以从任何Linux内核镜像站点下载的全新Linux内核。 有关更多详细信息,请参阅Linux Kernel HOWTO。

具有讽刺意味的是,这也可能导致问题。 默认情况下,系统上的gcc可能会在默认位置查找内核头文件,而不是安装内核新副本的位置(通常在/ usr / src /中 。这可以通过使用gcc的-I开关来修复。

第2章Hello World

Hello,World(第1部分):最简单的模块

当第一个穴居人程序员在第一个洞穴计算机的墙壁上凿出第一个程序时,它是一个在Antelope图片中绘制字符串“Hello,world”的程序。 罗马编程教科书以“Salut,Mundi”计划开始。 我不知道那些打破这种传统的人会发生什么,但我认为不发现更安全。 我们将从一系列hello world程序开始,这些程序演示了编写内核模块的基本知识的不同方面。

这是最简单的模块。 不要编译它; 我们将在下一节中介绍模块编译。

例2-1。 HELLO-1.C

/*  
 *  hello-1.c - The simplest kernel module.
 */
#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */

int init_module(void)
{
    printk(KERN_INFO "Hello world 1.\n");

    /* 
     * A non 0 return means init_module failed; module can't be loaded. 
     */
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_INFO "Goodbye world 1.\n");
}

内核模块必须至少有两个函数:一个名为init_module()的“start”(初始化)函数,当模块被编入内核时调用,以及一个名为cleanup_module()的“end”(清理)函数,只调用在它被破坏之前。 实际上,从内核2.3.13开始,情况发生了变化。 您现在可以使用您喜欢的任何名称作为模块的开始和结束功能,您将在第2.3节中学习如何执行此操作。 实际上,新方法是首选方法。 但是,许多人仍然使用init_module()和cleanup_module()作为其开始和结束函数。

通常, init_module()要么为内核注册一个处理程序,要么用自己的代码替换其中一个内核函数(通常代码执行某些操作然后调用原始函数)。 cleanup_module()函数应该撤消init_module()所做的任何操作,因此可以安全地卸载模块。

最后,每个内核模块都需要包含linux / module.h 。 我们需要包含linux / kernel.h,仅用于printk()日志级别的KERN_ALERT的宏扩展,您将在第2.1.1节中了解它。

介绍printk()

尽管你可能会想到, printk()并不是要向用户传达信息,即使我们在hello-1中将它用于此目的! 它恰好是内核的日志记录机制,用于记录信息或发出警告。 因此,每个printk()语句都带有一个优先级,即您看到的<1>和KERN_ALERT 。 有8个优先级,内核有宏,所以你不必使用神秘的数字,你可以在linux / kernel.h中查看它们(及其含义)。 如果未指定优先级,则将使用默认优先级DEFAULT_MESSAGE_LOGLEVEL 。

花点时间阅读优先级宏。 头文件还描述了每个优先级的含义。 在实践中,不要使用数字,如<4> 。 始终使用宏,如KERN_WARNING 。

如果优先级小于int console_loglevel ,则会在当前终端上打印该消息。 如果syslogd和klogd都在运行,那么该消息也会附加到/ var / log / messages ,无论它是否打印到控制台。 我们使用高优先级(如KERN_ALERT )来确保将printk()消息打印到控制台而不是仅记录到日志文件中。 编写实际模块时,您需要使用对当前情况有意义的优先级。

编译内核模块

内核模块的编译需要与常规用户空间应用程序略有不同。 以前的内核版本要求我们关注这些设置,这些设置通常存储在Makefile中。 虽然按层次结构组织,但许多冗余设置在次级Makefile中累积并使它们变大并且难以维护。 幸运的是,有一种新方法可以做这些事情,称为kbuild,外部可加载模块的构建过程现在完全集成到标准内核构建机制中。 要了解有关如何编译不属于官方内核的模块的更多信息(例如本指南中的所有示例),请参阅文件linux / Documentation / kbuild / modules.txt 。

那么,让我们看一个简单的Makefile来编译一个名为hello-1.c的模块:

例2-2。 Makefile用于基本内核模块

obj-m += hello-1.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

从技术角度来看,第一行确实是必要的,为了方便起见,添加了“全部”和“清洁”目标。

现在您可以通过发出命令make来编译模块。 您应该获得类似于以下内容的输出:

hostname:~/lkmpg-examples/02-HelloWorld# make
make -C /lib/modules/2.6.11/build M=/root/lkmpg-examples/02-HelloWorld modules
make[1]: Entering directory `/usr/src/linux-2.6.11'
  CC [M]  /root/lkmpg-examples/02-HelloWorld/hello-1.o
 Building modules, stage 2.
  MODPOST
  CC      /root/lkmpg-examples/02-HelloWorld/hello-1.mod.o
  LD [M]  /root/lkmpg-examples/02-HelloWorld/hello-1.ko
make[1]: Leaving directory `/usr/src/linux-2.6.11'
hostname:~/lkmpg-examples/02-HelloWorld#

请注意,内核2.6引入了一种新的文件命名约定:内核模块现在具有.ko扩展名(代替旧的.o扩展名),可以轻松地将它们与传统的目标文件区分开来。 这样做的原因是它们包含一个额外的.modinfo部分,其中保留了有关该模块的其他信息。 我们很快就会看到这些信息有什么用处。

使用modinfo hello - * .ko来查看它是什么类型的信息。

hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-1.ko
filename:       hello-1.ko
vermagic:       2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3
depends:

到目前为止,没什么了不起的。 一旦我们在后面的一个示例hello-5.ko上使用modinfo,这就会改变。

hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-5.ko
filename:       hello-5.ko
license:        GPL
author:         Peter Jay Salzman
vermagic:       2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3
depends:
parm:           myintArray:An array of integers (array of int)
parm:           mystring:A character string (charp)
parm:           mylong:A long integer (long)
parm:           myint:An integer (int)
parm:           myshort:A short integer (short)
hostname:~/lkmpg-examples/02-HelloWorld# 

很多有用的信息可以在这里看到。 bug报告的作者字符串,许可证信息,甚至是它接受的参数的简短描述。

有关内核模块的Makefile的更多详细信息,请参见linux / Documentation / kbuild / makefiles.txt 。 在开始破解Makefile之前,请务必阅读此文件和相关文件。 它可能会为你节省大量的工作。

现在是时候用insmod ./hello-1.ko将新编译的模块插入到内核中(忽略任何你看到的污染内核;我们很快就会介绍它)。

加载到内核中的所有模块都列在/ proc / modules中 。 来吧,抓住那个文件,看看你的模块真的是内核的一部分。 恭喜,您现在是Linux内核代码的作者! 当新颖性消失时,使用rmmod hello-1从内核中删除模块。 查看/ var / log / messages只是为了看到它已记录到您的系统日志文件中。

这是读者的另一个练习。 请参阅init_module()中 return语句上方的注释? 将返回值更改为负值,重新编译并再次加载模块。 怎么了?

Hello World(第2部分)

从Linux 2.4开始,您可以重命名模块的init和cleanup功能; 它们不再需要分别被称为init_module()和cleanup_module() 。 这是通过module_init()和module_exit()宏完成的。 这些宏在linux / init.h中定义。 唯一需要注意的是,必须在调用宏之前定义init和cleanup函数,否则会出现编译错误。 以下是此技术的示例:

例2-3。 HELLO-2.C

/*  
 *  hello-2.c - Demonstrating the module_init() and module_exit() macros.
 *  This is preferred over using init_module() and cleanup_module().
 */
#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/init.h>     /* Needed for the macros */

static int __init hello_2_init(void)
{
    printk(KERN_INFO "Hello, world 2\n");
    return 0;
}

static void __exit hello_2_exit(void)
{
    printk(KERN_INFO "Goodbye, world 2\n");
}

module_init(hello_2_init);
module_exit(hello_2_exit);

所以现在我们有两个真正的内核模块。 添加另一个模块就像这样简单:

例2-4。 我们的模块的Makefile

obj-m += hello-1.o
obj-m += hello-2.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

现在看一下linux / drivers / char / Makefile的真实示例。 正如你所看到的,有些东西被硬件连接到内核(obj-y)但是那些obj-m去了哪里? 熟悉shell脚本的人很容易发现它们。 对于那些没有的,你看到的obj - $(CONFIG_FOO)条目会扩展为obj-y或obj-m,具体取决于CONFIG_FOO变量是否设置为y或m。 虽然我们在这里,但那些正是你在linux / .config文件中设置的那种变量,上次你说make menuconfig之类的东西。

Hello World(第3部分): __ init和__exit宏

这演示了内核2.2及更高版本的功能。 注意init和cleanup函数定义的变化。 __init宏导致init函数被丢弃,一旦init函数完成内置驱动程序而不是可加载模块,它的内存就会被释放。 如果你考虑调用init函数的时候,这是完全合理的。

还有一个__initdata与__init类似,但是对于init变量而不是函数。

当模块内置到内核中时, __ exit宏会导致省略函数,而像__exit一样,对可加载模块没有影响。 同样,如果你考虑清理功能何时运行,这就完全合情合理; 内置驱动程序不需要清理功能,而可加载模块则需要清理功能。

这些宏在linux / init.h中定义,用于释放内核内存。 当您启动内核并看到释放未使用的内核内存时:释放236k ,这正是内核正在释放的内容。

例2-5。 HELLO-3.C


/*  
 *  hello-3.c - Illustrating the __init, __initdata and __exit macros.
 */
#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/init.h>     /* Needed for the macros */

static int hello3_data __initdata = 3;

static int __init hello_3_init(void)
{
    printk(KERN_INFO "Hello, world %d\n", hello3_data);
    return 0;
}

static void __exit hello_3_exit(void)
{
    printk(KERN_INFO "Goodbye, world 3\n");
}

module_init(hello_3_init);
module_exit(hello_3_exit);

Hello World(第4部分):许可和模块文档

如果您运行的是内核2.4或更高版本,则在加载专有模块时可能会注意到这样的内容:

# insmod xxxxxx.o
Warning: loading xxxxxx.ko will taint the kernel: no license
  See http://www.tux.org/lkml/#export-tainted for information about tainted modules
Module xxxxxx loaded, with warnings

在内核2.4及更高版本中,设计了一种机制来识别在GPL(和朋友)下许可的代码,以便可以警告人们代码是非开源的。 这是通过MODULE_LICENSE()宏实现的,该宏在下一段代码中进行了演示。 通过将许可证设置为GPL,您可以防止打印警告。 此许可证机制在linux / module.h中定义并记录:

/*
 * The following license idents are currently accepted as indicating free
 * software modules
 *
 *  "GPL"               [GNU Public License v2 or later]
 *  "GPL v2"            [GNU Public License v2]
 *  "GPL and additional rights" [GNU Public License v2 rights and more]
 *  "Dual BSD/GPL"          [GNU Public License v2
 *                   or BSD license choice]
 *  "Dual MIT/GPL"          [GNU Public License v2
 *                   or MIT license choice]
 *  "Dual MPL/GPL"          [GNU Public License v2
 *                   or Mozilla license choice]
 *
 * The following other idents are available
 *
 *  "Proprietary"           [Non free products]
 *
 * There are dual licensed components, but when running with Linux it is the
 * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL
 * is a GPL combined work.
 *
 * This exists for several reasons
 * 1.   So modinfo can show license info for users wanting to vet their setup 
 *  is free
 * 2.   So the community can ignore bug reports including proprietary modules
 * 3.   So vendors can do likewise based on their own policies
 */

类似地, MODULE_DESCRIPTION()用于描述模块的功能, MODULE_AUTHOR()声明模块的作者, MODULE_SUPPORTED_DEVICE()声明模块支持哪些类型的设备。

这些宏都在linux / module.h中定义,并且内核本身不使用它们。 它们只是用于文档,可以通过像objdump这样的工具查看。 作为读者的练习,尝试在linux / drivers中搜索这些宏,以了解模块作者如何使用这些宏来记录他们的模块。

我建议在/usr/src/linux-2.6.x/中使用像grep -inr MODULE_AUTHOR *这样的东西。 不熟悉命令行工具的人可能会喜欢一些基于Web的解决方案,搜索提供使用LXR索引的内核树的站点。 (或在本地计算机上设置)。

传统Unix编辑器的用户,如emacs或vi,也会发现标签文件很有用。 它们可以通过make标签生成,也可以在/usr/src/linux-2.6.x/中生成TAGS 。 一旦你在kerneltree中有了这样的标记文件,就可以将光标放在一些函数调用上,并使用一些组合键直接跳转到定义函数。

例2-6。 hello-4.c

/*  
 *  hello-4.c - Demonstrates module documentation.
 */
#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/init.h>     /* Needed for the macros */
#define DRIVER_AUTHOR "Peter Jay Salzman <[email protected]>"
#define DRIVER_DESC   "A sample driver"

static int __init init_hello_4(void)
{
    printk(KERN_INFO "Hello, world 4\n");
    return 0;
}

static void __exit cleanup_hello_4(void)
{
    printk(KERN_INFO "Goodbye, world 4\n");
}

module_init(init_hello_4);
module_exit(cleanup_hello_4);

/*  
 *  You can use strings, like this:
 */

/* 
 * Get rid of taint message by declaring code as GPL. 
 */
MODULE_LICENSE("GPL");

/*
 * Or with defines, like this:
 */
MODULE_AUTHOR(DRIVER_AUTHOR);   /* Who wrote this module? */
MODULE_DESCRIPTION(DRIVER_DESC);    /* What does this module do */

/*  
 *  This module uses /dev/testdevice.  The MODULE_SUPPORTED_DEVICE macro might
 *  be used in the future to help automatic configuration of modules, but is 
 *  currently unused other than for documentation purposes.
 */
MODULE_SUPPORTED_DEVICE("testdevice");

将命令行参数传递给模块

模块可以使用命令行参数,但不能使用您可能习惯使用的argc / argv 。

要允许将参数传递给模块,请声明将命令行参数的值作为全局变量的变量,然后使用module_param()宏(在linux / moduleparam.h中定义)来设置机制。 在运行时,insmod将使用给定的任何命令行参数填充变量,例如./insmod mymodule.ko myvariable = 5 。 为清楚起见,变量声明和宏应放在模块的开头。 示例代码应该清除我公认的糟糕解释。

module_param()宏有3个参数:变量的名称,它在sysfs中对应文件的类型和权限。 整数类型可以照常签名或无符号签名。 如果您想使用整数或字符串数​​组,请参阅module_param_array()和module_param_string() 。

int myint = 3;
module_param(myint, int, 0);

也支持数组,但现在的情况与2.4中的情况有所不同。 要跟踪将指针作为第三个参数传递给计数变量所需的参数数量。 根据您的选择,您也可以忽略计数并传递NULL。 我们在这里展示两种可能性

int myintarray[2];
module_param_array(myintarray, int, NULL, 0); /* not interested in count */

int myshortarray[4];
int count;
module_parm_array(myshortarray, short, , 0); /* put count into "count" variable */

一个很好的用途是设置模块变量的默认值,如端口或IO地址。 如果变量包含默认值,则执行自动检测(在别处解释)。 否则,保持当前值。 这将在稍后阐明。

最后,有一个宏函数MODULE_PARM_DESC() ,用于记录模块可以采用的参数。 它需要两个参数:变量名称和描述该变量的自由格式字符串。

例2-7。 HELLO-5.C

/*
 *  hello-5.c - Demonstrates command line argument passing to a module.
 */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/stat.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Peter Jay Salzman");

static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";
static int myintArray[2] = { -1, -1 };
static int arr_argc = 0;

/* 
 * module_param(foo, int, 0000)
 * The first param is the parameters name
 * The second param is it's data type
 * The final argument is the permissions bits, 
 * for exposing parameters in sysfs (if non-zero) at a later stage.
 */

module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "A short integer");
module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(myint, "An integer");
module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "A long integer");
module_param(mystring, charp, 0000);
MODULE_PARM_DESC(mystring, "A character string");

/*
 * module_param_array(name, type, num, perm);
 * The first param is the parameter's (in this case the array's) name
 * The second param is the data type of the elements of the array
 * The third argument is a pointer to the variable that will store the number 
 * of elements of the array initialized by the user at module loading time
 * The fourth argument is the permission bits
 */
module_param_array(myintArray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintArray, "An array of integers");

static int __init hello_5_init(void)
{
    int i;
    printk(KERN_INFO "Hello, world 5\n=============\n");
    printk(KERN_INFO "myshort is a short integer: %hd\n", myshort);
    printk(KERN_INFO "myint is an integer: %d\n", myint);
    printk(KERN_INFO "mylong is a long integer: %ld\n", mylong);
    printk(KERN_INFO "mystring is a string: %s\n", mystring);
    for (i = 0; i < (sizeof myintArray / sizeof (int)); i++)
    {
        printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]);
    }
    printk(KERN_INFO "got %d arguments for myintArray.\n", arr_argc);
    return 0;
}

static void __exit hello_5_exit(void)
{
    printk(KERN_INFO "Goodbye, world 5\n");
}

module_init(hello_5_init);
module_exit(hello_5_exit);

我建议玩这个代码:

satan# insmod hello-5.ko mystring="bebop" mybyte=255 myintArray=-1
mybyte is an 8 bit integer: 255
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: bebop
myintArray is -1 and 420

satan# rmmod hello-5
Goodbye, world 5

satan# insmod hello-5.ko mystring="supercalifragilisticexpialidocious" \
> mybyte=256 myintArray=-1,-1
mybyte is an 8 bit integer: 0
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintArray is -1 and -1

satan# rmmod hello-5
Goodbye, world 5

satan# insmod hello-5.ko mylong=hello
hello-5.o: invalid argument syntax for mylong: 'h'

跨越多个文件的模块

有时在几个源文件之间划分内核模块是有意义的。

这是一个这样的内核模块的例子。

例2-8。 start.c

/*
 *  start.c - Illustration of multi filed modules
 */

#include <linux/kernel.h>   /* We're doing kernel work */
#include <linux/module.h>   /* Specifically, a module */

int init_module(void)
{
    printk(KERN_INFO "Hello, world - this is the kernel speaking\n");
    return 0;
}

下一个文件:

例2-9。 stop.c

/*
 *  stop.c - Illustration of multi filed modules
 */

#include <linux/kernel.h>   /* We're doing kernel work */
#include <linux/module.h>   /* Specifically, a module  */

void cleanup_module()
{
    printk(KERN_INFO "Short is the life of a kernel module\n");
}

最后,makefile:

例2-10。 Makefile文件

obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

这是我们到目前为止看到的所有示例的完整makefile。 前五行并不特别,但对于最后一个例子,我们需要两行。 首先,我们为组合模块创建一个对象名称,然后我们告诉make什么对象文件是该模块的一部分。

构建预编译内核的模块

显然,我们强烈建议您重新编译内核,以便启用许多有用的调试功能,例如强制模块卸载( MODULE_FORCE_UNLOAD ):启用此选项后,您可以强制内核卸载模块,即使它已经卸载认为它是不安全的,通过rmmod -f模块命令。 此选项可以在开发模块期间为您节省大量时间和大量重新启动。

然而,在许多情况下,您可能希望将模块加载到预编译的运行内核中,例如通用Linux发行版附带的内核,或者您过去编译的内核。 在某些情况下,您可能需要编译并将模块插入到不允许重新编译的正在运行的内核中,或者在您不希望重新启动的计算机上。 如果您不能想到会强制您使用预编译内核模块的情况,您可能希望跳过这一点,并将本章的其余部分作为一个重要注意事项处理。

现在,如果您只是安装内核源代码树,使用它来编译内核模块,并尝试将模块插入内核,在大多数情况下,您将获得如下错误:

insmod: error inserting 'poet_atkm.ko': -1 Invalid module format

将更少的密码信息记录到/ var / log / messages中 :

Jun  4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686 
REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'

换句话说,你的内核拒绝接受你的模块,因为版本字符串(更确切地说,版本魔法)不匹配。 顺便提一下,版本魔法以静态字符串的形式存储在模块对象中,以vermagic:开头。 当版本数据与init / vermagic.o文件链接时,会在模块中插入。 要检查存储在给定模块中的版本魔法和其他字符串,请发出modinfo module.ko命令

[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko 
license:        GPL
author:         Peter Jay Salzman <p@dirac.org>
description:    A sample driver
vermagic:       2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3
depends: 

为了克服这个问题,我们可以采用–force-vermagic选项,但这种解决方案可能不安全,而且在生产模块中无疑是不可接受的。 因此,我们希望在与构建预编译内核的环境相同的环境中编译模块。 如何做到这一点,是本章其余部分的主题。

首先,确保内核源代码树可用,与当前内核的版本完全相同。 然后,找到用于编译预编译内核的配置文件。 通常,这在您当前的/ boot目录中,在config-2.6.x之类的名称下可用。 您可能只想将其复制到内核源代码树: cp / boot / config-uname -r / usr / src / linux -uname -r / .config 。

让我们再次关注上一个错误消息:仔细查看版本魔术字符串表明,即使两个配置文件完全相同,版本魔法也可能略有不同,并且足以防止插入将模块放入内核。 这个细微的差别,即出现在模块版本魔术而不是内核版本中的自定义字符串,是由于在某些分发包含的makefile中对原始文件进行了修改。 然后,检查/ usr / src / linux / Makefile ,并确保指定的版本信息与用于当前内核的版本信息完全匹配。 例如,makefile可以如下开始:

VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 5
EXTRAVERSION = -1.358custom
...

在这种情况下,您需要将符号EXTRAVERSION的值恢复为-1.358 。 我们建议在/lib/modules/2.6.5-1.358/build中保留用于编译内核的makefile的备份副本。 一个简单的cp / lib / modules /uname -r / build / Makefile / usr / src / linux -uname -r就足够了。 另外,如果你已经使用之前的(错误的) Makefile启动了内核构建,你还应该重新运行make ,或者直接修改文件/usr/src/linux-2.6.x/include/linux/version.h中的符号UTS_RELEASE 。文件/lib/modules/2.6.x/build/include/linux/version.h的内容,或用第一个覆盖后者。

现在,请运行make来更新配置和版本标头和对象:

[root@pcsenonsrv linux-2.6.x]# make
CHK     include/linux/version.h
UPD     include/linux/version.h
SYMLINK include/asm -> include/asm-i386
SPLIT   include/linux/autoconf.h -> include/config/*
HOSTCC  scripts/basic/fixdep
HOSTCC  scripts/basic/split-include
HOSTCC  scripts/basic/docproc
HOSTCC  scripts/conmakehash
HOSTCC  scripts/kallsyms
CC      scripts/empty.o
...

如果您不希望实际编译内核,可以在SPLIT行之后中断构建过程( CTRL-C ),因为那时您需要的文件已准备就绪。 现在您可以返回到模块的目录并进行编译:它将完全根据您当前的内核设置构建,并且将加载到其中而不会出现任何错误。

第3章 Preliminaries

模块与程序

模块如何开始和结束

程序通常以main()函数开始,执行一堆指令并在完成这些指令后终止。 内核模块的工作方式略有不同。 模块始终以init_module或您通过module_init调用指定的函数开头。 这是模块的入口功能; 它告诉内核模块提供了哪些功能,并设置内核以在需要时运行模块的功能。 一旦它执行此操作,入口函数返回,并且模块不执行任何操作,直到内核想要对模块提供的代码执行某些操作。

所有模块都通过调用cleanup_module或您使用module_exit调用指定的函数来结束。 这是模块的退出功能; 它取消了任何输入功能。 它取消注册入口函数注册的功能。

每个模块都必须具有入口功能和退出功能。 由于指定入口和出口函数的方法不止一种,我会尽量使用“入口函数”和“退出函数”这两个术语,但如果我滑动并简单地将它们称为init_module和cleanup_module ,我想你我会明白我的意思。

模块可用的功能

程序员使用他们不会一直定义的函数。 一个主要的例子是printf() 。 您可以使用标准C库libc提供的这些库函数。 这些函数的定义实际上不会进入程序,直到链接阶段,这确保代码(例如printf() )可用,并修复调用指令以指向该代码。

内核模块也在这里不同。 在hello world示例中,您可能已经注意到我们使用了一个函数printk()但没有包含标准I / O库。 那是因为模块是目标文件,其符号在insmod’ing时得到解决。 符号的定义来自内核本身; 您可以使用的唯一外部函数是内核提供的函数。 如果您对内核导出的符号感到好奇,请查看/ proc / kallsyms 。

需要记住的一点是库函数和系统调用之间的区别。 库函数是更高级别的,完全在用户空间中运行,并为程序员提供了更方便的接口,使其能够执行真正的工作 - 系统调用。 系统调用代表用户以内核模式运行,由内核本身提供。 库函数printf()可能看起来像一个非常通用的打印函数,但它真正做的就是将数据格式化为字符串并使用低级系统调用write()写入字符串数据,然后将数据发送到标准输出。

您想查看printf()进行的系统调用吗? 这很容易! 编译以下程序:

#include <stdio.h>
int main(void)
{ printf("hello"); return 0; }

使用gcc -Wall -o hello hello.c 。 使用strace ./hello运行exectable 。 你印象深刻吗? 您看到的每一行都对应一个系统调用。 strace [4]是一个方便的程序,它为您提供有关程序正在进行的系统调用的详细信息,包括调用哪个调用,它的参数是什么。 它是一个非常宝贵的工具,用于确定程序试图访问的文件。 接近尾声,你会看到一条看起来像写的行(1,“你好”,5hello) 。 它就是。 printf()面具后面的面孔。 您可能不熟悉写入,因为大多数人使用库函数进行文件I / O(如fopen,fputs,fclose)。 如果是这种情况,请尝试查看man 2 。 第二个人部分专门用于系统调用(如kill()和read() 。第三个人部分专门用于库调用,你可能会更熟悉它们(如cosh()和random() )。

您甚至可以编写模块来替换内核的系统调用,我们很快就会这样做。 破解者经常使用这种东西来做后门或特洛伊木马,但是你可以编写自己的模块来做更多的良性事情,就像内核写的Tee嘻嘻,发痒! 每次有人试图删除系统上的文件。

用户空间与内核空间

内核就是对资源的访问,无论所讨论的资源是视频卡,硬盘还是内存。 程序通常竞争相同的资源。 在我刚刚保存此文档时,updatedb开始更新locate数据库。 我的vim会话和updatedb都同时使用硬盘驱动器。 内核需要保持整齐有序,并且不会让用户随时访问资源。 为此, CPU可以以不同的模式运行。 每种模式都提供了不同的自由度,可以在系统上执行您想要的操作。 英特尔80386架构有4种这样的模式,称为环。 Unix只使用两个环; 最高的环(环0,也称为“管理员模式”,允许一切都发生)和最低环,称为“用户模式”。

回想一下有关库函数与系统调用的讨论。 通常,您在用户模式下使用库函数。 库函数调用一个或多个系统调用,这些系统调用代表库函数执行,但是在管理员模式下执行,因为它们是内核本身的一部分。 一旦系统调用完成其任务,它将返回并执行转移回用户模式。

名称空间

当您编写一个小型C程序时,您可以使用方便且对读者有意义的变量。 另一方面,如果你正在编写将成为更大问题的一部分的例程,那么你拥有的任何全局变量都是其他人的全局变量社区的一部分; 一些变量名称可能会发生冲突。 当一个程序有许多全局变量,这些变量没有足够的意义可以区分时,就会产生命名空间污染 。 在大型项目中,必须努力记住保留名称,并找到开发用于命名唯一变量名称和符号的方案的方法。

在编写内核代码时,即使最小的模块也会链接到整个内核,所以这肯定是个问题。 处理此问题的最佳方法是将所有变量声明为静态变量,并为符号使用明确定义的前缀。 按照惯例,所有内核前缀都是小写的。 如果您不想将所有内容声明为静态 ,则另一个选项是声明符号表并将其注册到内核。 我们稍后会谈到这个。

文件/ proc / kallsyms包含内核知道的所有符号,因此它们可以访问模块,因为它们共享内核的代码空间。

代码空间

内存管理是一个非常复杂的主题— O’Reilly的大部分“理解Linux内核”仅仅是内存管理! 我们不打算成为内存管理方面的专家,但我们确实需要了解一些事实,甚至开始担心编写真正的模块。

如果您还没有想过段错误的真正含义,您可能会惊讶地发现指针实际上并未指向内存位置。 不管怎么说,不是真的。 当创建进程时,内核会留出一部分真实物理内存并将其交给进程,以用于执行代码,变量,堆栈,堆和计算机科学家所知道的其他事情[5] 。 该存储器以0x00000000开头,并扩展到它需要的任何内容。 由于任何两个进程的内存空间不重叠,因此每个可以访问内存地址的进程(例如0xbffff978 )都将访问实际物理内存中的不同位置! 这些进程将访问名为0xbffff978的索引,该索引指向为该特定进程留出的内存区域中的某种偏移量。 在大多数情况下,像我们的Hello,World程序这样的过程无法访问另一个进程的空间,尽管我们稍后会讨论一些方法。

内核也有自己的内存空间。 由于模块是可以在内核中动态插入和删除的代码(而不是半自治对象),因此它共享内核的代码空间而不是自己的代码空间。 因此,如果你的模块是segfaults,那么内核会出现段错误。 如果你因为一个错误的错误而开始写数据,那么你就是在践踏内核数据(或代码)。 这比听起来还要糟糕,所以尽量小心。

顺便提一下,我想指出上述讨论适用于任何使用单片内核的操作系统[6] 。 有一些称为微内核的东西,它们有模块可以获得自己的代码空间。 GNU Hurd和QNX Neutrino是微内核的两个例子。

设备驱动程序

一类模块是设备驱动程序,它为电视卡或串行端口等硬件提供功能。 在unix上,每个硬件都由位于/ dev中的文件表示,该文件命名为设备文件 ,该文件提供与硬件通信的方法。 设备驱动程序代表用户程序提供通信。 所以es1370.o声卡设备驱动程序可能会将/ dev / sound设备文件连接到Ensoniq IS1370声卡。 像mp3blaster这样的用户空间程序可以使用/ dev / sound而不知道安装了什么类型的声卡。

主要和次要号码

我们来看一些设备文件。 以下是代表主要主IDE硬盘驱动器上前三个分区的设备文件:

# ls -l /dev/hda[1-3]
brw-rw----  1 root  disk  3, 1 Jul  5  2000 /dev/hda1
brw-rw----  1 root  disk  3, 2 Jul  5  2000 /dev/hda2
brw-rw----  1 root  disk  3, 3 Jul  5  2000 /dev/hda3

注意用逗号分隔的数字列? 第一个数字称为设备的主要编号。 第二个数字是次要数字。 主要编号告诉您使用哪个驱动程序访问硬件。 为每个驱动程序分配一个唯一的主编号; 具有相同主要编号的所有设备文件由同一驱动程序控制。 所有上述主要数字均为3,因为它们都由同一个驱动程序控制。

驱动程序使用次要编号来区分它控制的各种硬件。 回到上面的例子,虽然所有三个设备都由相同的驱动程序处理,但它们具有唯一的次要编号,因为驱动程序将它们视为不同的硬件。

设备分为两种类型:字符设备和块设备。 区别在于块设备具有请求缓冲区,因此它们可以选择响应请求的最佳顺序。 这在存储设备的情况下是重要的,其中读取或写入彼此接近的扇区更快,而不是那些相距更远的扇区。 另一个区别是块设备只能接受块中的输入和返回输出(其大小可以根据设备而变化),而字符设备允许使用它们喜欢的尽可能多的字节。 世界上大多数设备都是字符,因为它们不需要这种类型的缓冲,并且它们不以固定的块大小运行。 您可以通过查看ls -l输出中的第一个字符来判断设备文件是用于块设备还是字符设备。 如果它是’b’那么它就是一个块设备,如果它是’c’那么它就是一个字符设备。 您在上面看到的设备是块设备。 以下是一些字符设备(串口):

crw-rw----  1 root  dial 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r-----  1 root  dial 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw----  1 root  dial 4, 66 Jul  5  2000 /dev/ttyS2
crw-rw----  1 root  dial 4, 67 Jul  5  2000 /dev/ttyS3

如果要查看已分配的主要编号,可以查看/usr/src/linux/Documentation/devices.txt 。

安装系统后,所有这些设备文件都是由mknod命令创建的。 要创建一个名为“coffee”且主要/次要编号为12和2的新char设备,只需执行mknod / dev / coffee c 12 2即可 。 您不必将设备文件放入/ dev ,但它是按惯例完成的。 Linus将他的设备文件放在/ dev中 ,所以你应该这样做。 但是,在创建用于测试目的的设备文件时,可以将它放在编译内核模块的工作目录中。 完成编写设备驱动程序后,请务必将其放在正确的位置。

我想提出一些隐含在上述讨论中的最后几点,但为了以防万一,我想明确一些。 访问设备文件时,内核使用文件的主编号来确定应使用哪个驱动程序来处理访问。 这意味着内核实际上并不需要使用甚至不知道次要编号。 司机本身是唯一关心次要号码的人。 它使用次要编号来区分不同的硬件。

顺便说一句,当我说“硬件”时,我的意思是比你手里拿着的PCI卡更抽象。 看看这两个设备文件:

% ls -l /dev/fd0 /dev/fd0u1680
brwxrwxrwx   1 root  floppy   2,  0 Jul  5  2000 /dev/fd0
brw-rw----   1 root  floppy   2, 44 Jul  5  2000 /dev/fd0u1680

到目前为止,您可以查看这两个设备文件并立即知道它们是块设备并由相同的驱动程序处理(块主要2 )。 您甚至可能知道这些都代表您的软盘驱动器,即使您只有一个软盘驱动器。 为什么两个文件? 一个代表具有1.44 MB存储空间的软盘驱动器。 另一个是具有1.68 MB存储空间的相同软盘驱动器,并且对应于某些人称之为“超格式化”的磁盘。 比标准格式化软盘拥有更多数据的数据。 所以这里有两个具有不同次要编号的设备文件实际上代表同一块物理硬件的情况。 所以请注意,我们讨论中的“硬件”这个词可能意味着非常抽象的东西。

猜你喜欢

转载自blog.csdn.net/yeshennet/article/details/82315604