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

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

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

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

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

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

第1章简介

什么是内核模块?

因此,你想编写一个内核模块。 你知道C,你已经编写了一些正常的程序做为进程运行,如今你想要到达实际操做的位置,一个狂野指针能够消灭你的文件系统,核心转储意味着重启。shell

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

模块如何进入内核?

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

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

模块名称,如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 <p@dirac.org>"
#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存储空间的相同软盘驱动器,而且对应于某些人称之为“超格式化”的磁盘。 比标准格式化软盘拥有更多数据的数据。 因此这里有两个具备不一样次要编号的设备文件实际上表明同一块物理硬件的状况。 因此请注意,咱们讨论中的“硬件”这个词可能意味着很是抽象的东西。