linux内核模块编程2

本文详细介绍了一个简单的内核模块HelloWorld的实现过程,包括模块的基本结构、编译方法、宏使用等核心内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Chapter 2. Hello World
Table of Contents
Hello, World (part 1): 最简单的内核模块
编译内核模块
Hello World (part 2)
Hello World (part 3): 关于__init和__exit宏
Hello World (part 4): 内核模块证书和内核模块文档说明
从命令行传递参数给内核模块
由多个文件构成的内核模块
为已编译的内核编译模块
Hello, World (part 1): 最简单的内核模块
当第一个洞穴程序员在第一台洞穴计算机的墙上上凿写第一个程序时, 这是一个在羚羊皮上输出`Hello, world'的字符串。罗马的编程书籍上是以 `Salut, Mundi'这样的程序开始的。 我不明白人们为什么要破坏这个传统, 但我认为还是不明白为好。我们将从编写一系列的`Hello, world'模块开始, 一步步展示编写内核模块的基础的方方面面。

这可能是一个最简单的模块了。先别急着编译它。我们将在下章模块编译的章节介绍相关内容。

Example 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_ALERT */

int init_module(void)
{
        printk("<1>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_ALERT "Goodbye world 1.\n" );
}
一个内核模块应该至少包含两个函数。一个“开始”(初始化)的函数被称为init_module() 还有一个“结束” (干一些收尾清理的工作)的函数被称为cleanup_module() ,当内核模块被rmmod卸载时被执行。实际上,从内核版本2.3.13开始这种情况有些改变。 你可以为你的开始和结束函数起任意的名字。 你将在以后学习如何实现这一点the Section called Hello World (part 2)。 实际上,这个新方法时推荐的实现方法。但是,许多人仍然使init_module()和 cleanup_module()作为他们的开始和结束函数。

一般,init_module()要么向内核注册它可以处理的事物,要么用自己的代码 替代某个内核函数(代码通常这样做然后再去调用原先的函数代码)。函数 cleanup_module()应该撤消任何init_module()做的事,从而 内核模块可以被安全的卸载。

最后,任一个内核模块需要包含linux/module.h。 我们仅仅需要包含 linux/kernel.h当需要使用 printk()记录级别的宏扩展时KERN_ALERT,相关内容将在the Section called 介绍printk()中介绍。

介绍printk()
不管你可能怎么想,printk()并不是设计用来同用户交互的,虽然我们在 hello-1就是出于这样的目的使用它!它实际上是为内核提供日志功能, 记录内核信息或用来给出警告。因此,每个printk() 声明都会带一个优先级,就像你看到的<1>和KERN_ALERT 那样。内核总共定义了八个优先级的宏, 所以你不必使用晦涩的数字代码,并且你可以从文件 linux/kernel.h查看这些宏和它们的意义。如果你 不指明优先级,默认的优先级DEFAULT_MESSAGE_LOGLEVEL将被采用。

阅读一下这些优先级的宏。头文件同时也描述了每个优先级的意义。在实际中, 使用宏而不要使用数字,就像<4>。总是使用宏,就像 KERN_WARNING。

当优先级低于int console_loglevel,信息将直接打印在你的终端上。如果同时 syslogd和klogd都在运行,信息也同时添加在文件 /var/log/messages,而不管是否显示在控制台上与否。我们使用像 KERN_ALERT这样的高优先级,来确保printk()将信息输出到 控制台而不是只是添加到日志文件中。 当你编写真正的实用的模块时,你应该针对可能遇到的情况使用合 适的优先级。
编译内核模块
内核模块在用gcc编译时需要使用特定的参数。另外,一些宏同样需要定义。 这是因为在编译成可执行文件和内核模块时, 内核头文件起的作用是不同的。 以往的内核版本需要我们去在Makefile中手动设置这些设定。尽管这些Makefile是按目录分层次 安排的,但是这其中有许多多余的重复并导致代码树大而难以维护。 幸运的是,一种称为kbuild的新方法被引入,现在外部的可加载内核模块的编译的方法已经同内核编译统一起来。想了解更多的编 译非内核代码树中的模块(就像我们将要编写的)请参考帮助文件linux/Documentation/kbuild/modules.txt。 

现在让我们看一个编译名字叫做hello-1.c的模块的简单的Makefile文件:

Example 2-2. 一个基本的Makefile

obj-m += hello-1.o

现在你可以通过执行命令 make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules 编译模块。 你应该得到同下面类似的屏幕输出:

[root@pcsenonsrv test_module]# make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules
make: Entering directory `/usr/src/linux-2.6.x
  CC [M]  /root/test_module/hello-1.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test_module/hello-1.mod.o
  LD [M]  /root/test_module/hello-1.ko
make: Leaving directory `/usr/src/linux-2.6.x
        
请注意2.6的内核现在引入一种新的内核模块命名规范:内核模块现在使用.ko的文件后缀(代替 以往的.o后缀),这样内核模块就可以同普通的目标文件区别开。更详细的文档请参考 linux/Documentation/kbuild/makefiles.txt。在研究Makefile之前请确认你已经参考了这些文档。 

现在是使用insmod ./hello-1.ko命令加载该模块的时候了(忽略任何你看到的关于内核污染的输出 显示,我们将在以后介绍相关内容)。

所有已经被加载的内核模块都罗列在文件/proc/modules中。cat一下这个文件看一下你的模块是否真的 成为内核的一部分了。如果是,祝贺你!你现在已经是内核模块的作者了。当你的新鲜劲过去后,使用命令 rmmod hello-1.卸载模块。再看一下/var/log/messages文件的内容是否有相关的日志内容。 

这儿是另一个练习。看到了在声明 init_module()上的注释吗? 改变返回值非零,重新编译再加载,发生了什么?
Hello World (part 2)
在内核Linux 2.4中,你可以为你的模块的“开始”和“结束”函数起任意的名字。它们不再必须使用 init_module()和cleanup_module()的名字。这可以通过宏 module_init()和module_exit()实现。这些宏在头文件linux/init.h定义。唯一需要注意的地方是函数必须在宏的使用前定义,否则会有编译 错误。下面就是一个例子。

Example 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_ALERT */
#include <linux/init.h>                /* Needed for the macros */

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

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

module_init(hello_2_init);
module_exit(hello_2_exit);
现在我们已经写过两个真正的模块了。添加编译另一个模块的选项十分简单,如下:

Example 2-4. 两个内核模块使用的Makefile

obj-m += hello-1.o
obj-m += hello-2.o
现在让我们来研究一下linux/drivers/char/Makefile这个实际中的例子。就如同你看到的, 一些被编译进内核 (obj-y),但是这些obj-m哪里去了呢?对于熟悉shell脚本的人这不难理解。这些在Makefile中随处可见 的obj-$(CONFIG_FOO)的指令将会在CONFIG_FOO被设置后扩展为你熟悉的obj-y或obj-m。这其实就是你在使用 make menuconfig编译内核时生成的linux/.config中设置的东西。
Hello World (part 3): 关于__init和__exit宏
这里展示了内核2.2以后引入的一个新特性。注意在负责“初始化”和“清理收尾”的函数定义处的变化。宏 __init的使用会在初始化完成后丢弃该函数并收回所占内存,如果该模块被编译进内核,而不是动态加载。 

宏__initdata同__init 类似,只不过对变量有效。

宏__exit将忽略“清理收尾”的函数如果该模块被编译进内核。同宏 __exit一样,对动态加载模块是无效的。这很容易理解。编译进内核的模块 是没有清理收尾工作的, 而动态加载的却需要自己完成这些工作。

这些宏在头文件linux/init.h定义,用来释放内核占用的内存。 当你在启动时看到这样的Freeing unused kernel memory: 236k freed内核输出,上面的 那些正是内核所释放的。

Example 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_ALERT */
#include <linux/init.h>                /* Needed for the macros */

static int hello3_data __initdata = 3;

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

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

module_init(hello_3_init);
module_exit(hello_3_exit);
Hello World (part 4): 内核模块证书和内核模块文档说明
如果你在使用2.4或更新的内核,当你加载你的模块时,你也许注意到了这些输出信息:

# insmod hello-3.o
Warning: loading hello-3.o will taint the kernel: no license
  See  http://www.tux.org/lkml/#export-tainted  for information about tainted modules
Hello, world 3
Module hello-3 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 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查看。 作为一个练习,使用grep从目录linux/drivers看一看这些模块的作者是如何 为他们的模块提供识别信息和档案的。

Example 2-6. hello-4.c

/*  
*  hello-4.c - Demonstrates module documentation.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR " Peter Jay Salzman < p@dirac.org >"
#define DRIVER_DESC   "A sample driver"

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

static void __exit cleanup_hello_4(void)
{
        printk(KERN_ALERT "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_PARM()(在头文件linux/module.h)。运行时,insmod将给变量赋予命令行的参数,如同 ./insmod mymodule.o myvariable=5。为使代码清晰,变量的声明和宏都应该放在 模块代码的开始部分。以下的代码范例也许将比我公认差劲的解说更好。

宏MODULE_PARM()需要两个参数,变量的名字和其类型。支持的类型有" b": 比特型,"h": 短整型, "i": 整数型," l: 长整型和 "s": 字符串型,其中正数型既可为signed也可为unsigned。 字符串类型应该声明为"char *"这样insmod就可以为它们分配内存空间。你应该总是为你的变量赋初值。 这是内核编程,代码要编写的十分谨慎。举个例子:

int myint = 3;
char *mystr;

MODULE_PARM(myint, "i" );
MODULE_PARM(mystr, "s" );
        
数组同样被支持。在宏MODULE_PARM中在类型符号前面的整型值意味着一个指定了最大长度的数组。 用'-'隔开的两个数字则分别意味着最小和最大长度。下面的例子中,就声明了一个最小长度为2,最大长度为4的整形数组。

int myshortArray[4];
MODULE_PARM (myintArray, "3-9i" );
        
将初始值设为缺省使用的IO端口或IO内存是一个不错的作法。如果这些变量有缺省值,则可以进行自动设备检测, 否则保持当前设置的值。我们将在后续章节解释清楚相关内容。在这里我只是演示如何向一个模块传递参数。

最后,还有这样一个宏,MODULE_PARM_DESC()被用来注解该模块可以接收的参数。该宏 两个参数:变量名和一个格式自由的对该变量的描述。

Example 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";

/* 
* 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" );

static int __init hello_5_init(void)
{
        printk(KERN_ALERT "Hello, world 5\n=============\n" );
        printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort);
        printk(KERN_ALERT "myint is an integer: %d\n", myint);
        printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong);
        printk(KERN_ALERT "mystring is a string: %s\n", mystring);
        return 0;
}

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

module_init(hello_5_init);
module_exit(hello_5_exit);
我建议用下面的方法实验你的模块:

satan# insmod hello-5.o 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.o 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.o mylong=hello
hello-5.o: invalid argument syntax for mylong: 'h'
由多个文件构成的内核模块
有时将模块的源代码分为几个文件是一个明智的选择。在这种情况下,你需要:


只要在一个源文件中添加#define __NO_VERSION__预处理命令。 这很重要因为module.h通常包含 kernel_version的定义,此时一个存储着内核版本的全局变量将会被编译。但如果此时你又要包含头文件 version.h,你必须手动包含它,因为 module.h不会再包含它如果打开预处理选项__NO_VERSION__。

像通常一样编译。

将所有的目标文件连接为一个文件。在x86平台下,使用命令ld -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>。

此时Makefile一如既往会帮我们完成编译和连接的脏活。

这里是这样的一个模块范例。

Example 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("Hello, world - this is the kernel speaking\n" );
        return 0;
}
另一个文件:

Example 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("<1>Short is the life of a kernel module\n" );
}
最后是该模块的Makefile:

Example 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
为已编译的内核编译模块
很显然,我们强烈推荐你编译一个新的内核,这样你就可以打开内核中一些有用的排错功能,像强制卸载模块(MODULE_FORCE_UNLOAD): 当该选项被打开时,你可以rmmod -f module强制内核卸载一个模块,即使内核认为这是不安全的。该选项可以为你节省不少开发时间。 

但是,你仍然有许多使用一个正在运行中的已编译的内核的理由。例如,你没有编译和安装新内核的权限,或者你不希望重启你的机器来运行新内核。 如果你可以毫无阻碍的编译和使用一个新的内核,你可以跳过剩下的内容,权当是一个脚注。 

如果你仅仅是安装了一个新的内核代码树并用它来编译你的模块,当你加载你的模块时,你很可能会得到下面的错误提示: 

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。

让我们再次注意一下先前的错误信息:仔细看的话你会发现,即使使用完全相同的配置文件,版本印戳还是有细小的差异的,但这足以导致 模块加载的失败。这其中的差异就是在模块中出现却不在内核中出现的custom字符串,是由某些发行版提供的修改过的 makefile导致的。检查/usr/src/linux/Makefile,确保下面这些特定的版本信息同你使用的内核完全一致:

VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 5
EXTRAVERSION = -1.358custom
...
        
像上面的情况你就需要将EXTRAVERSION一项改为-1.358。我们的建议是将原始的makefile备份在 /lib/modules/2.6.5-1.358/build下。 一个简单的命令cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname -r`即可。 另外,如果你已经在运行一个由上面的错误的Makefile编译的内核,你应该重新执行 make,或直接对应/lib/modules/2.6.x/build/include/linux/version.h从文件 /usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆盖后者的。 

现在,请执行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中止编译过程。因为此时你需要的文件 已经就绪了。现在你可以返回你的模块目录然后编译加载它:此时模块将完全针对你的当前内核编译,加载时也不会由任何错误提示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值