热门关键字:  ubuntu  分区  Fedora  linux系统进程  函数

当前位置 :| 主页>Linux教程>内核研究>

开发简单的 Linux2.6 内核模块

来源: 作者: 时间:2008-12-03 Tag: 点击:
05年本科毕业设计做的是Linux下驱动的剖析,当时就买了一本《Linux设备驱动程序(第二版)》,但是没有实现将最简单的helloworld程序编译成模块,加载到kernel里。不过,现在自己确实打算做一款芯片的Linux的驱动,因此,又开始看了《Linux设备驱动程序》这本书,不过已经是第三版了。第二版讲的是2.4的内核,第三版讲的是2.6的内核。两个内核版本之间关于编译内核以及加载模块的方法都有所变化。

本文是基于2.6的内核,也建议各位可以先看一下《Linux内核设计与实现(第二版)》作为一个基础知识的铺垫。当然,从实践角度来看,只要按着以下的步骤去做也应该可以实现成功编译内核及加载模块。

个人用的Linux版本为:Debian GNU/Linux,内核版本为:2.6.20-1-686.

第一步,下载Linux内核的源代码,即构建LDD3(Linux Device Drivers 3rd)上面所说的内核树。
如过安装的Linux系统中已经自带了源代码的话,应该在/usr/src目录下。如果该目录为空的话,则需要自己手动下载源代码。下载代码的方法和链接很多,也可以在CU上通过http://download.chinaunix.net/search/?key=&;q=kernel&frmid=53去下载。不过,下载的内核版本最好和所运行的Linux系统的内核版本一致。当然,也可以比Linux系统内核的版本低,但高的话应该不行(个人尚未实践)。
Debian下可以很方便的通过Debian源下载:
首先查找一下可下载的内核源代码:
# apt-cache search linux-source
其中显示的有:linux-source-2.6.20,没有和我的内核版本完全匹配,不过也没关系,直接下载就可以了:
# apt-get install linux-source-2.6.20
下载完成后,安装在/usr/src下,文件名为:linux-source-2.6.20.tar.bz2,是一个压缩包,解压缩既可以得到整个内核的源代码:
# tar jxvf linux-source-2.6.20.tar.bz2
解压后生成一个新的目录/usr/src/linux--source-2.6.20,所有的源代码都在该目录下。
注:该目录会因内核版本的不同而不同,各位动手实践的朋友只需知道自己的源代码所在的具体位置即可。

第二步:配置及编译内核。
进入/usr/src/linux--source-2.6.20目录下,可以看到Makefile文件,它包含了整个内核树编译信息。该文件最上面四行是关于内核版本的信息。对于整个Makefile可以不用做修改,采用默认的就可以了。
一般情况下,需要先用命令诸如"make menuconfig", "make xconfig"或者"make oldcofig"对内核进行配置,这几个都是对内核进行配置的命令,只是它们运行的环境不一样,执行一下这几个命令中的任何一个即可对内核进行配置:
make menuconfig是基于界面的内核配置方法,make xconfig应该是基于QT库的,还有make gcofig也是基于图形的配置方法,应该是需要GTK的环境,make oldcofig就是对内核树原有的.config文件进行配置一下即可。
其实内核的配置部分,主要是保证内核启动模块可动态加载的配置,默认配置里面应该已经包含了这样的内容,因此,我用的是make oldconfig.

内核的详细配置请见另外一位网友的帖子,这里给出链接:
http://linux.chinaunix.net/bbs/viewthread.php?tid=885597&extra=page%3D1%26amp%3Bfilter%3Ddigest

在内核源码的目录下执行:
# make
# make bzImage
其中,第一个make也可以不执行,直接make bzImage。这个过程可能要持续一个小时左右,因此是对整个内核重新编译了。执行结束后,可以看到在当前目录下生成了一个新的文件: vmlinux, 其属性为-rwxr-xr-x。
然后执行:
# make modules
# make modules_install
对内核的所有模块进行编译和安装。
执行结束之后,会在/lib/modules下生成新的目录/lib/modules/2.6.20/。 在随后的编译模块文件时,要用到这个路径下的build目录。至此,内核编译完成。可以重启一下系统。

第三步:编写模块文件及Makefile
以LDD3上的hello.c为例:
//hello.c
#include <linux/init.h>
#include <linux/module.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);

Makefile文件的内容为:

obj-m := hello.o
KERNELDIR := /lib/modules/2.6.20/build
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

其中,hello.c和Makefile文件应该位于同一个目录下,可以放在/home下,我的两个文件都位于/home/david/.

第四步:编译和装载模块

在文件所处的目录下,执行:
debian:/home/david # make

然后查看该目录下有哪些文件生成:
debian:/home/david # ls -l
总计 28
drwxr-xr-x 2 david david 4096 2007-02-07 17:49 Desktop
-rw-r--r-- 1 david david 462 2007-07-20 13:42 hello.c
-rw-r--r-- 1 root root 2432 2007-07-20 13:55 hello.ko
-rw-r--r-- 1 root root 607 2007-07-20 13:55 hello.mod.c
-rw-r--r-- 1 root root 1968 2007-07-20 13:55 hello.mod.o
-rw-r--r-- 1 root root 1140 2007-07-20 13:55 hello.o
-rw-r--r-- 1 david david 267 2007-07-20 13:48 Makefile
-rw-r--r-- 1 root root 0 2007-07-05 14:11 Module.symvers

可见,已经生成模块文件hello.ko.
然后,就可以加载该模块:
debian:/home/david # insmod hello.ko

查看模块是否加载进内核:
debian:/home/david # lsmod
Module Size Used by
hello 1344 0
nfs 219468 0
nfsd 202224 17
... ...

其中Module名为hello的即为我们所加载的模块.

卸载模块:

debian:/home/david # rmmod hello

同样可以通过lsmod来查看该模块是否被卸载.

这里有两个问题,其一就是printk()输出的问题.LDD3上也说,在加载和卸载模块的时候都会有信息输出在屏幕上,如果通过终端仿真器(,则在屏幕上看不到任何输出.我同时在虚拟机和和物理机都运行了该模块,均未看到有"Hello, world"(加载模块时printk的输出)或"Goodby, cruel world"(卸载模块时printk的输出). 这个不知道是我操作系统发行版的原因还是系统配置的问题,请了解这个问题的朋友指点一下.


其二,书上讲到如果屏幕上看不到信息,可能输出在某个日志文件里面了,并说可能在/var/log/messages文件中.并且看到网上很多网友也说是输出到这个文件里面.我不知道有没有发现输出在其他日志文件里的,不过我的这个信息输出在/var/log/syslog里面.在加载和卸载完该模块后, 执行命令:
debian:/home/david # cat /var/log/syslog | grep world
可以看到有两行内容.当然,也可以不用grep world, 应该会出现在最后两行.

Jul 20 14:15:29 localhost kernel: Hello, world
Jul 20 14:15:34 localhost kernel: Goodbye, cruel world

这就是printk应该输出的信息.

这里有另外一个方法,可以实现printk的信息输出在屏幕上,即更改printk输出的优先级.例子中的优先级为:KERN_ALERT,优先级为<1>,如果将优先级改为KERN_EMERG即<0>,则可以看到屏幕的输出信息.
修改的方法只是修改一下hello.c中两句printk()的内容,修改后的hello.c如下:

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_EMERG "Hello, world\n"); /*改动部分*/
return 0;
}

static void hello_exit(void)
{
printk(KERN_EMERG"Goodbye, cruel world\n"); /*改动部分*/
}

module_init(hello_init);
module_exit(hello_exit);

同样的方法编译生成模块,再次用insmod和rmmod,则在屏幕上看到的输出信息为:

debian:/home/david# insmod hello.ko
debian:/home/david#
Message from syslogd@localhost at Fri Jul 20 14:27:32 2007 ...
localhost kernel: Hello, world

debian:/home/david# rmmod hello
debian:/home/david#
Message from syslogd@localhost at Fri Jul 20 14:27:42 2007 ...
localhost kernel: Goodbye, cruel world

debian:/home/david
但是,是否能够将printk()的优先级改为KERN_EMERG值得商榷.因为在Linux Kernel Development中,对该优先级的描述为: An emergency condition; the system is probably dead.

以上就是整个2.6内核编译步骤以及模块动态加载的方法.理解和解释有误的地方,也请各位浏览本文的朋友指点,也希望能和对内核和驱动感兴趣的朋友交流.

本文也参考了一位网友博客上一篇关于编译2.6内核的文章,这里给出链接:
http://blog.csdn.net/wooin/archive/2007/05/21/1619141.aspx

【08-10】补充:本文最后所提到的模块加载和卸载时没有看到屏幕的输出信息,即printk输出的问题。经过实践之后,本人发现了个人的原因,和大家交流一下。LDD3书中所说的看不到输出指的是在终端仿真器,就是我们平时在X-Window下用的终端,譬如我用的时Gnome-Terminal,在这种情况下看不到屏幕的输出。而我上面的例程正是在终端下运行的。当时没想到切换到控制台Console运行一下试试。今天在控制台运行了,发现无论是在虚拟机还是物理机下,只要在Console下面运行,是可以看到屏幕输出的。

















开发简单的 Linux2.6 内核模块



您的内核必须已经启用这些选项进行了编译:

Loadable module support --->

[*] Enable loadable module support
[*] Module unloading
[ ] Module versioning support (EXPERIMENTAL)
[*] Automatic kernel module loading

如果按照第一篇教程中的说明编译内核,那么就已经正确地设置了这些选项。否则,修改这些选项,重新编译内核,并引导到新内核。

一个简单的模块骨架

首先,找到编译当前 Linux 内核的源代码。将目录切换到 Linux 源代码目录中的 drivers/misc/。现在,拷贝下面的代码并将其粘贴到一个名为 mymodule.c 的文件:

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

static int __init mymodule_init(void)
{
printk ("My module worked!\n");
return 0;
}

static void __exit mymodule_exit(void)
{
printk ("Unloading my module.\n");
return;
}

module_init(mymodule_init);
module_exit(mymodule_exit);

MODULE_LICENSE("GPL");

保存这个文件,并在同一目录下编辑 Makefile 文件。添加这一行:

obj-m += mymodule.o

编译模块:

# make -C <top directory of your kernel source> SUBDIRS=$PWD modules

使用 insmod ./mymodule.ko 加载这个模块,并查看是否打印了您的消息: dmesg | tail。应该会在输出的结束处看到:

My module worked!

现在删除内核模块:rmmod mymodule。再次查看 dmesg;应该会看到:

Unloading my module.

这样您就已经编写并运行了一个新的内核模块!

模块/内核接口

现在,我们来做一些与您的模块有关的更有趣的事情。要了解的一个关键内容是,模块只能“看到”内核故意让它访问的函数和变量。首先,我们以错误的方式来进行尝试。

编辑文件 kernel/printk.c,在所有包含文件之后其他全局变量声明附近(但要在所有函数之外)添加下面一行:

int my_variable = 0;

现在重新编译内核并引导到新内核。然后,将下面的内容添加到模块的 mymodule_init 函数起始处,置于其他代码之前。

extern int my_variable;
printk ("my_variable is %d\n", my_variable);
my_variable++;

保存修改并重新编译模块:

# make -C <top directory of your kernel source> SUBDIRS=$PWD modules

加载模块(这将失败):insmod ./mymodule.ko。模块的加载会失败,并给出消息:

insmod: error inserting './mymodule.ko': -1 Unknown symbol in module

这说明内核不允许模块访问那个变量。当模块加载时,它必须解析所有外部引用,比如函数名或者变量名。如果它不能找到内核导出的符号列表中所有未解析的名称,那么模块就不能写入那个变量或者调用那个函数。在内核中某个地方有为变量 my_variable 分配的空间,但模块不知道是哪里。

为解决此问题,我们将把 my_variable 添加到内核导出的符号列表中。在很多内核目录中,都有一个特定的文件,用于导出在那个目录中定义的符号。再次打开 kernel/printk.c 文件,在变量声明之后添加下面一行:

EXPORT_SYMBOL(my_variable);

重新编译并重新引导到新内核。现在再一次尝试加载模块:insmod ./mymodule.ko。这一次,当查看 dmesg 时,应该看到:

my_variable is 0
My module worked!

重新加载模块:

# rmmod mymodule && insmod ./mymodule.ko

现在应该看到:

Unloading my module.
my_variable is 1
My module worked!

每次重新加载那个模块,my_variable 都会增 1。您正在读写一个在主内核中定义的变量。只要被 EXPORT_SYMBOL() 显式地声明,模块就可以访问主内核中的任何变量。例如,函数 printk() 是在内核中定义的,并且在文件 kernel/printk.c 中被导出。

简单的可引导内核模块是用来研究内核的一个有趣的途径。例如,可以使用一个模块来打开或关闭 printk,方法是在内核中定义一个变量 do_print(它初始化为 0)。然后,让所有 printk 都依赖于“do_print”:

if (do_print) {
printk ("Big long obnoxious message\n");
}

然后,只有当您的模块被加载时才打开它。

模块参数

引导模块时,可以向它传递参数。要使用模块参数加载模块,这样写:

insmod module.ko [param1=value param2=value ...]

为了使用这些参数的值,要在模块中声明变量来保存它们,并在所有函数之外的某个地方使用宏 MODULE_PARM(variable, type) 和 MODULE_PARM_DESC(variable, description) 来接收它们。type 参数应该是一个格式为 [min[-max]]{b,h,i,l,s} 字符串,其中 min 和 max 是数组的长度限度。如果两者都忽略了,则默认为 1。最后一个字符是类型说明符:

b byte
h short
i int
l long
s string

可以在 MODULE_PARM_DESC 的 description 域中添加任何需要的说明符。

编写使用中断的模块

现在我们将编写一个模块,其中有一个函数,当内核接收到某个 IRQ 上的一个中断时会调用它。首先,将文件 mymodule.c 拷贝到 myirqtest.c,然后删除函数的内容,只保留返回语句。在编辑器中打开 myirqtest.c,并使用“myirqtest”替换所出现的“mymodule”来修改函数名。另外删除 printk。为了能够使用中断,将下面一行:

#include <linux/interrupt.h>

加入到文件的顶部。

使用 cat /proc/interrupts 找出正在使用的中断。第一列显示出正在使用的中断号,第二列是机器自最后一次引导后在那个 IRQ 上发行了多少次中断,第三列是使用这个 IRQ 的设备。在这个示例中,我们将研究来自网络接口的中断,并使用两个模块参数 interface 和 irq 来指明我们要使用的接口和 IRQ 行。

为了使用模块参数,要声明两个变量来存放它们,并使用 MODULE_PARM 和 MODULE_PARM_DESC 来捕获参数。此代码应该放置在所有函数之外的某个地方:

static int irq;
static char *interface;

MODULE_PARM(interface, "s");
MODULE_PARM_DESC(interface, "A network interface");
MODULE_PARM(irq, "i");
MODULE_PARM_DESC(irq, "The IRQ of the network interface");

函数 request_irq() 将您的函数添加到选定的 IRQ 行的处理程序列表,每当接收到那个行上的一个中断时,可以使用它打印一条消息。现在,我们需要在函数 myirqtest_init 中请求网络设备的 IRQ。 request_irq 的定义如下:

int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char *devname,
void *dev_id);

irq 是中断号。我们将使用从模块参数获得的值。handler 是一个指针,指向处理中断的函数。我们将使用 SA_SHIRQ 作为 irqflags 的值,表明我们的处理程序支持与其他处理程序共享 IRQ。 devname 是设备的简称,显示在 /proc/interrupts 列表中。我们将使用 interface 变量中的值,它是作为模块参数接收到的。

dev_id 参数是设备 ID。这个参数通常设置为 NULL,但是,如果需要共享 IRQ,以使得稍后那个 IRQ 被 free_irq() 释放时,正确的设备会被放开,那么它需要是 non-NULL 的。由于它是 void *,所以它可以指向任何内容,不过,通常的做法是传递驱动程序的设备结构体。在此,我们将使用一个指向 irq 变量的指针。

如果成功,request_irq() 将返回 0。

编写完代码后,myirqtest_init() 应该类似如下:

static int __init myirqtest_init(void)
{
if(request_irq(irq, &myinterrupt, SA_SHIRQ, interface, &irq)) {
printk(KERN_ERR "myirqtest: cannot register IRQ %d\n", irq);
return -EIO;
}
printk("Request on IRQ %d succeeded\n", irq);

return 0;
}

如果 request_irq() 没有返回 0,则是出了一些错误, IRQ 不能被注册,所以我们打印一条错误消息并返回错误代码。

现在,当卸载那个模块时,我们还需要释放那个 IRQ。此任务由 free_irq 来完成,它使用中断号和设备 ID 作为参数。中断号保存在 irq 变量中,并且我们使用指向它的指针做为设备 ID,所以需要做的就是将下面的代码添加到 myirqtest_exit() 的开头:

free_irq(irq, &irq);
printk("Freeing IRQ %d\n", irq);

其余要做的全部事情就是编写 myinterrupt() 处理程序函数。它的声明已经间接通过 request_irq() 的参数说明了:void (*handler)(int, void *, struct pt_regs *)。第一个参数是中断号,第二个参数是在 request_irq 中所使用的设备 ID,第三个参数持有一个指向某个结构体的指针,结构体中容纳的是在服务那个中断之前的处理器寄存器和状态。

如果不去查看处理器寄存器,我们就不能知道中断是来自我们的设备还是来自共享同一 IRQ 的某些其他设备。在本例中,令人满意的是,中断发生在指定的 IRQ 上。当编写真正的驱动程序时,执行对此的检查很重要,如果处理程序发现中断由另一个设备所使用,那么它应该立即返回值 IRQ_NONE,而不去处理那个中断。如果中断来自我们的设备,而且处理程序被正确调用,那么应该返回 IRQ_HANDLED。这些操作是与硬件相关的,在此不再论述。

所以,每当在指定的 IRQ 上有一个中断时,myinterrupt() 函数都会被调用。发生此事件时我们会执行打印输出,但是希望限制输出的数量,所以将像先前建议的那样去做,只打印输出前 10 个中断。

还需要从这个函数返回某些内容。由于这不是一个真正的驱动程序,而只是研究中断,所以应该返回 IRQ_NONE。通过返回 IRQ_HANDLED,我们可以宣称这是设备的真正驱动程序,不需要任何其他驱动程序来处理这个中断(在本例中并不是这样)。

这里是 myinterrupt() 的最终代码:

static irqreturn_t myinterrupt(int irq, void *dev_id, struct pt_regs *regs)
{
static int mycount = 0;

if (mycount < 10) {
printk("Interrupt!\n");
mycount++;
}

return IRQ_NONE;
}

这样就完成了!将下面一行:

obj-m += myirqtest.o

添加到此目录中的 Makefile,并使用下面的命令编译模块:

# make -C <top directory of your kernel source> SUBDIRS=$PWD modules

现在插入模块(将参数值设置为在系统中可以生效的值,见 cat /proc/interrupts):

insmod myirqtest.ko interface=eth0 irq=9

查看 dmesg 的打印输出。它应该类似如下:

Request on IRQ 9 succeeded
Interrupt!
Interrupt!
Interrupt!
Interrupt!
Interrupt!

最多有 10 行“Interrupt!”,因为我们限制打印输出的数目最多那么多。现在,卸载模块:

rmmod myirqtest

IRQ 现在应该被我们的处理程序释放了。查看 dmesg 的输出。它应该类似如下:

Freeing IRQ 9

现在就已经完成了您自己的使用中断的内核模块!去研究您的新内核模块吧 —— 模块是非常有趣的!


相关文章:
精通initramfs构建step by step
Linux利用kexec迅速切换内核
进程上下文VS中断上下文
内核通知链 学习笔记
linux spi子系统驱动分析
menuconfig 配置选项
《Linux操作系统内核实习》之练习一
udev详解
什么叫微内核,宏内核?
Linux 信号signal处理机制
开发简单的 Linux2.6 内核模块
删除内核的perl脚本
Linux2.6内核usb gadget驱动移植
GCC hacks in the Linux kernel
iomem
kernel学习的想法
让自己的驱动支持udev
linux内核编译步骤
内核的等待队列
Linux内核wait_queue深入分析
升级和删除内核
SD卡驱动分析2
Linux Kernel VDSO本地权限提升漏洞
内核中的TCP的追踪分析-15-TCP(IPV4)的客户端与
linux 2.6内核可加载模块的编译
内核模块HelloWorld
在环回接口上发送一个数据报
ARP初始化
1分钟编译FreeBSD内核
linux设备模型之uart驱动架构分析