Linux内核模块简要介绍

本文的主要内容是阅读LDD3书籍的第二章笔记;
测试开发板是JZ2440V3,内核版本是Linux version 2.6.22.6。
要想为内核构造模块,还必须在自己的系统中配置并构造好内核数。因为内核的模块要和内核源码树中的目标文件进行连接,通过这种方式,可得到一个更加健壮的模块装载器,但也需要这些目标文件存在于内核目录树中。这样,我们就要首先准备好一个内核源代码树,构造一个新内核,然后安装到开发板中。因为后面讲到的原因,如果在构造内核时运行的恰好是目标内核,则开发工作就好非常轻松。

首先编译百问科技提供的内核(linux-2.6.22.6);

1.将内核源码解压;
tar -xvf linux-2.6.22.6.tar
2.进入内核的根目录;
cd linux-2.6.22.6
3.配置内核;
cp config_ok .config
4.编译内核;
make uImage -j2

5.将编译好的内核uImage烧写到开发板中。

Hello World模块:

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
	printk(KERN_ALERT "Hello,World\n");
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_ALERT "Goodbye cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

这个模块定义了两个函数,其中一个在模块被装载到内核时调用(hello_init),而另一个则在模块被移除时调用(hello_exit)。module_init和module_exit行使用了内核的特殊宏来表示上述两个函数所扮演的角色。另一个特殊宏MODULE_LICENSE用来告诉内核,该模块采用自由许可证;如果没有这样的声明,内核在装载该模块时会产生抱怨。printk函数在Linux内核中定义,功能
和标准C库中的printf类似。内核需要自己单独的打印输出函数,这是因为它在运行时不能依赖于C库。

如下所示,读者可以通过调用insmod和rmmod工具来测试这个模块,值得注意的是,只有超级用户才有权加载和卸载模块。

# insmod hello_world.ko
Hello,World
# rmmod hello_world
Goodbye cruel world
我们已经看到,编译一个模块并没有想象的那么困难,至少当模块不需要完成什么有价值的工作时。真正的困难在于理解设备并最大化其性能。

核心模块和应用程序对比:

大多数小规模及中规模应用程序是从头到尾执行单个任务,而模块却只是预先注册自己以便服务于将来的某个请求,然后它的初始化函数就立即结束。换句话说,模块初始化函数的任务就是为以后调用模块函数预先做准备;这就像模块在说"我在这儿,并且我能做这些工作。"模块退出函数将在模块被卸载之前调用。它告诉内核:"我要离开啦,不要再让我做任何事情了。"这种编程方式和事件驱动的编程有点类似,但并不是所有的应用程序都是事件驱动的,而每个内核模块都是这样的。事件驱动的应用程序和内核代码之间的另一个主要不同是:应用程序在退出时,可以不管资源的释放或者其他的清除工作,但模块的退出函数却必须仔细撤销初始化函数所做的一切,否则,在系统重新引导之前某些东西就会残留在系统中。

内核编程和应用程序编程的另一点重要不同之处在于各环境下处理错误的方式不同:应用程序开发过程中的段错误是无害的,并且总是可以使用调试器追踪到源码中的问题所在,而一个内核错误即使不影响整个系统,也至少会杀死当前进程。

用户空间和内核空间:

模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间中。这个概念是操作系统理论的基础之一。实际上,操作系统的作用是为应用程序提供一个操作计算机硬件的一致试图。我们通常将运行模式称作为内核空间和用户空间。这两个术语不仅说明两种模式具有不同的优先权等级,而且还说明每个模式都有自己的内核映射,也即自己的地址空间。

模块化代码在内核空间中运行,用于扩展内核的功能。通常来讲,一个驱动程序要执行先前讲述过的两个任务:模块中的某些函数作为系统调用的一部分而执行,而其他函数则负责中断处理。

当前进程:

虽然内核模块不像应用程序那样顺序的执行,然而内核执行的大多数操作还是和某个特定的进程相关。内核代码可通过访问全局向current来获得当前进程。current在<asm.current.h>中定义,是一个指向struct task_struct的指针,而task_struct结果在<linux/sched.h>中定义,current指针指向当前正在运行的进程。在open、read等系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。如果需要,内核代码可以通过current获得与当前进程相关的信息。

例如,下面的语句通过访问struct task_struct的某些成员来打印当前进程的进程ID和命令名:

printk(KERN_ALERT "current process pid is %d\n",current->pid);
printk(KERN_ALERT "current process name is %s\n",current->comm);

存储在current->comm成员中的命令名是当前进程所执行的程序文件的基本名称,如果必要,会剪裁到15个字符以内。

编译模块:

首先,我们要简单看看模块是如何构造的。模块的构造过程和用户空间应用程序的构造过程有很大的不同。内核是一个大的、独立的程序,为了将它的各个片断放在一起,要满足很多详细明确的要求,和先前的内核版本相比,构造过程也有所不同;新的构造系统用起来更加简单,并可产生更加正确的结果,但看起来和先前的方法有大的不同。内核的构造系统是个复杂的野兽,我们看到的只是其中之一小部分。如果读者希望理解这些表象之下的所有细节,则应该阅读内核源代码中Documentation\kbuild\目录下的文件。

在构造内核模块之前,有一些先决条件首先应该得到满足。首先,读者应确保具备了正确版本的编译器、模块工具和其他必要的工具。内核文档目录中的Documentation\Changes文件列出了需要的工具版本;在开始构造模块之前,读者需要查看该文件并确保已安装了正确的工具。

在准备好这些东西之后,为自己的模块创建makefile文件则非常简单。实际上,对本章先前给出的"hello world"示例来说,下面一行就足以了:

obj-m	:= hello_world.o

如果读者熟悉make但对2.6内核构造系统还不熟悉的话,则可能会对此makefile的工作方式感到疑惑。毕竟上面这行并不是makefile文件的常见形式。问题的答案当然是内核构造系统处理了其余的问题。上面的赋值语句说明了有一个模块需要从目标文件hello_world.o中构造,而从该文件中构造的模块名称为hello_world.ko。

如果我们要构造的模块名称为module.ko,并有两个源文件生产(比如file1和file2),则正确的makefile可如下编写:

obj-m	:= module.o
module-objs := file1.o file2.o

为了让上面这种类型的makefile文件正常工作,必须在大的内核构造系统环境中调用它们。如果读者的内核源代码树保存在/work/jz2440/linux-2.6.22.6目录中,则用来构造模块的make命令应该是:

KERN_DIR = /work/jz2440/linux-2.6.22.6
make -C $(KERN_DIR) M=`pwd` modules 

上述命令首先改变目录到-C指定的位置(即内核源代码目录),其中保存有内核的顶层makefile文件。M=选项让该makefile在构造内核modules目标之前返回到模块源代码目录。然后,modules目标指向obj-m变量中设定的模块;在上面的例子中,我们将该变量设置成module.o。

其makefile原文是:

KERN_DIR = /work/jz2440/linux-2.6.22.6
all:
    make -C $(KERN_DIR) M=`pwd` modules 
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf Module.symvers

obj-m	+= hello_world.o
装载模块:

在构造模块之后,下一步就是将模块装入内核。如前所述,insmod为我们完成这项工作。insmod程序和ld有些类似,它将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符合。前面提到,我们可以使用rmmod工具从内核中移除模块。注意,如果内核认为模块仍然在使用状态,或者内核被配置为禁止移除模块,则无法移除该模块。

使用lsmod程序列出当前装载到内核中的所有模块,还提供了其他一些信息,比如其他模块是不是在使用某个特定模块等。lsmod通过读取/proc/modules虚拟文件来获得这些信息。有关当前已装载模块的信息也可以在sysfs虚拟文件系统的/sys/module下找到。

内核符号表:

上面的讨论中,我们了解到insmod使用公共内核符号表来解析模块中未定义的符合。公共内核符号表中包含了所有的全局内核项的地址,这是实现模块化驱动程序所必须的。当模块被装载入内核后,它所导出的任何符号都会变成内核符号表的一部分。通常情况下,模块只需实现自己的功能,而无需导出任何符号。但是,如果其他模块需要从某个模块中获得好处时,我们也可以导出符号。

Linux内核头文件提供了一个方便的方法来管理符号对模块外部的可见性,从而减少了可能造成的名字空间污染,并且适当隐藏信息。如果一个模块需要向其他模块导出符号,则应该使用下面的宏。

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

这两个宏均用于给定的符合导出到模块外部。_GPL版本使得要导出的模块只能被GPL许可证下的模块使用。符合必须在模块文件的全局部分导出,不能再函数中导出,这是因为上面这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。

预备知识:

大部分内核代码中都要包含相等数量的头文件,以便获得函数、数据类型和变量的定义。我们将在用到这些文件时向读者介绍,但有几个头文件是专门用于模块的,因此必须出现在每个可装载的模块中。故而,所有的模块代码中都包含下面两行代码:

#include <linux/init.h>
#include <linux/module.h>
module.h包含有可装载模块需要的大量符合和函数的定义。包含init.h的目的是指定初始化和清除函数。

尽管不是严格要求的,但模块应该指定代码所使用的许可证。因此,我们只需要包含MODULE_LICENSE行:

MODULE_LICENSE("GPL");

如果一个模块没有显示地标记为内核可识别的许可证,则会被假定是专有的,而内核装载这种模块就会被"污染"。

可在模块中包含的其他描述性定义包括MODULE_AUTHOR("描述模块作者")、MODULE_DESCRIPTION("模块简短描述")和MODULE_ALIAS("模块的别名")。

上述MODULE_声明可出现在源文件中源代码函数以外的任何地方。但新的内核编码习惯是将这些声明放在文件的最后。

初始化函数:

前面已经提到,模块的初始化函数负责注册模块所提供的任何设施。这里的设施指的是一个可以被应用程序访问的新功能,它可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。初始化函数的实际定义通常如下:

static int __init initialization_init(void){
	/* 这里是初始化代码 */
	return 0;
}
module_init(initialization_init);

初始化函数应该被声明为static,因为这种函数在特定文件之外没有其他意义。因为一个模块函数如果要对内核其他部分可见,则必须被显示导出,因此这并不是什么强制性规则。上述定义中的__init标记看起来似乎有点陌生,它对内核来讲是一种暗示,表明该函数仅在初始化期间使用。在模块被装载之后,模块装载器就会将初始化函数扔掉,这样可将该函数占用的内存释放出来,以作他用。__init和__initdata的使用时可选的,虽然有点繁琐,但是很值得使用。注意,不要在结束初始化之后仍要使用的函数上使用这两个标记。在内核源代码中可能还会遇到__devinit和__devinitdata,只有在内核未被配置为支持热插拔设备的情况下,这两个标记才会被翻译为__init和__initdata。

module_init的使用是强制性的。这个宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。没有这个定义,初始化函数永远不会被调用。

清除函数:

每个重要的模块都要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源,该函数定义如下:

static void __exit cleanup_exit(void){
	printk(KERN_ALERT "Goodbye cruel world\n");
}
module_exit(cleanup_exit);
清除函数没有返回值,因此被声明为void。__exit修饰词标记该代码仅用于模块卸载。如果模块被直接内嵌到内核中,或者内核的配置不允许卸载模块,则被标记为__exit的函数将被简单地丢弃。处于以上原因,被标记为__exit的函数只能在模块被卸载或者系统关闭时被调用,其他的任何用法是错误的。和前面类似,module_exit声明对于帮助内核找到模块的清除函数是必需的。

如果一个模块未定义清除函数,则内核不允许卸载该模块。

猜你喜欢

转载自blog.csdn.net/caihaitao2000/article/details/80464437