现在的位置: 首页 > 综合 > 正文

The Linux Kernel Module Programming Guide

2013年05月02日 ⁄ 综合 ⁄ 共 23790字 ⁄ 字号 评论关闭

The Linux Kernel Module Programming Guide

Peter Jay Salzman
Michael Burian
Ori Pomerantz

The Linux Kernel Module Programming Guide is a free book; you may reproduce and/or modify it under the terms of the Open Software License, version 1.1. You can obtain a copy of this license at http://opensource.org/licenses/osl.php.

This book is distributed in the hope it will be useful, but without any warranty, without even the implied warranty of merchantability or fitness for a particular purpose.

The author encourages wide distribution of this book for personal or commercial use, provided the above copyright notice remains intact and the method adheres to the provisions of the Open Software License. In summary, you may copy and distribute this book
free of charge or for a profit. No explicit permission is required from the author for reproduction of this book in any medium, physical or electronic.

Derivative works and translations of this document must be placed under the Open Software License, and the original copyright notice must remain intact. If you have contributed new material to this book, you must make the material and source code available
for your revisions. Please make revisions and updates available directly to the document maintainer, Peter Jay Salzman . This will allow for the merging of updates and provide consistent
revisions to the Linux community.

If you publish or distribute this book commercially, donations, royalties, and/or printed copies are greatly appreciated by the author and the Linux Documentation Project (LDP). Contributing in this way shows your support for
free software and the LDP. If you have questions or comments, please contact the address above.


Table of Contents
Foreword
1. 作者声明
2. 版本和注意
3. 感谢
4. 译者注
1. Introduction
1.1. 什么是内核模块?
1.2. 内核模块是如何被调入内核工作的?
1.2.1. 在开始前
1.2.1.1. 内核模块和内核的版本问题
1.2.1.2. 使用 X带来的问题
1.2.1.3. 编译相关和内核版本相关的问题
2. Hello World
2.1. Hello, World (part 1): 最简单的内核模块
2.1.1. 介绍printk()
2.2. 编译内核模块
2.3. Hello World (part 2)
2.4. Hello World (part 3): 关于__init__exit
2.5. Hello World (part 4): 内核模块证书和内核模块文档说明
2.6. 从命令行传递参数给内核模块
2.7. 由多个文件构成的内核模块
2.8. 为已编译的内核编译模块
3. Preliminaries
3.1. 内核模块对比用户程序
3.1.1. 内核模块是如何开始和结束的
3.1.2. 模块可调用的函数
3.1.3. 用户空间和内核空间
3.1.4. 命名空间
3.1.5. 代码空间
3.1.6. 设备驱动
3.1.6.1. Major and Minor Numbers
4. Character Device Files
4.1. 字符设备文件
4.1.1. 关于file_operations结构体
4.1.2. 关于file结构体
4.1.3. 注册一个设备
4.1.4. 注销一个设备
4.1.5. chardev.c
4.1.6. 为多个版本的内核编写内核模块
5. The /proc File System
5.1. 关于 /proc 文件系统
5.2. 读写 /proc 文件
5.3. 用标准文件系统管理 /proc 文件
5.4. 使用seq_file管理 /proc 文件
6. Using /proc For Input
6.1. TODO:编写关于sysfs的一章
7. Talking To Device Files
7.1. 与设备文件对话 (writes and IOCTLs)
8. System Calls
8.1. 系统调用
9. Blocking Processes
9.1. 阻塞进程
10. Replacing Printks
10.1. 替换printk
10.2. 让你的键盘指示灯闪起来
11. Scheduling Tasks
11.1. 任务调度
12. Interrupt Handlers
12.1. 中断处理程序
12.1.1. Interrupt Handlers
12.1.2. Intel架构中的键盘
13. Symmetric Multi Processing
13.1. 对称多处理
14. Common Pitfalls
14.1. 常见陷阱
A. Changes: 2.4 To 2.6
A.1. 从2.4到2.6的变化
A.1.1. 从2.4到2.6的变化
B. Where To Go From Here
B.1. 从这里如何起步?
Index
List of Examples
2-1. hello-1.c
2-2. 一个基本的Makefile
2-3. hello-2.c
2-4. 两个内核模块使用的Makefile
2-5. hello-3.c
2-6. hello-4.c
2-7. hello-5.c
2-8. start.c
2-9. stop.c
2-10. Makefile
4-1. chardev.c
5-1. procfs1.c
5-2. procfs2.c
5-3. procfs3.c
5-4. procfs4.c
7-1. chardev.c
7-2. chardev.h
7-3. ioctl.c
8-1. syscall.c
9-1. sleep.c
10-1. print_string.c
10-2. kbleds.c
11-1. sched.c
12-1. intrpt.c

Foreword

1. 作者声明

《Linux内核驱动模块编程指南》最初是由Ori Pomerantz为2.2版本的内核编写的 ,后来,Ori将文档维护的任务交给了Peter Jay Salzman,Peter完成了2.4内核版本文档 的编写,毕竟Linux内核驱动模块是一个更新很快的内容。现在,Peter也无法腾出足够的 时间来完成2.6内核版本文档的编写,目前该2.6内核版本的文档由合作者Michael Burian 完成。


2. 版本和注意

Linux内核模块是一块不断更新进步的内容,在LKMPG上总有关于是否保留还是历史 版本的争论。Michael和我最终是决定为每个新的稳定版本内核建立一个新的文档分支。也 就是说LKMPG 2.4.x专注于2.4的内核,而LKMPG 2.6.x将专注于2.6的内核。我们不会在一 篇文档中提供对旧版本内核的支持,对此感兴趣的读者应该寻找相关版本的文档分支。

在文档中的绝大部分源代码和讨论都应该适用于其它平台,但我无法提供任何保证。其中的一个例外就是 Chapter 12, 中断处理该章的源代码和讨论就只适用于x86平台。


3. 感谢

感谢下列人士为此文档提供了他们宝贵的意见。他们是:Ignacio Martin, David Porter, Daniele Paolo,Scarpazza 和 Dimo Velev。


4. 译者注

我,译者,名叫田竞,目前是一名在北京邮电大学就读的通信专业的大学生。 自高中起我就是Linux的爱好者并追随至今。我喜欢Linux给我带来的自由,我想大家也一样。没有人不向往自由。

我学习内核模块编写时最初阅读的是Orelly出版社的使用2.0版本的内核的书籍,但如同我预料的一样, 书中的许多事例由于内核代码的变化而无法使用。这让想亲自体验内核模块的神秘的我非常苦恼。 我在Linux文档计划在上海的镜像站ldp.linuxforum.net上找到了这篇文档。

受原作者Ori的鼓励,基于上次完成的LKMPG 2.4的,内容有稍许的改变和扩充。应该是目前最新的了。 翻译的方式有所改变,在基于LDP认可的docbook格式上翻译,通过docbook2html转换为附件中的html文档。 由于对docbook不是很熟悉,其中的一些标题尚未翻译,而且可能破坏了原有的tag,导致html出现一些错误显示, 但总体来说很少。修改了很多2.4中的错别字。

学习并分享学习的过程是我翻译的最终目的。

补注:不知为何,原译者田竞未能及时更新此文档。我发电子邮件询问并请求做一些维护工作,但他至今仍未给我答复。我冒 昧地做一下更新和维护工作,为大家尽微薄之力,希望原译者看此文到后能联系我。译文的版权完全属于原译者田竞,我不想保留任何版权, 但我会对翻译质量负责。我的邮箱是xiyou [dot] wangcong [at] gmail [dot] com。总的来说,我对原文中的 一些技术错误做了订正(我已经通知原文作者),对原译文中的一些用词和错字做了更改,也使本文与最新的LKMPG版本2.6.3版一致。我也会继续
做维护工作,如发现有任何错误可以通过上面的邮箱联系我,我会及时修订。


Chapter 1. Introduction

1.1. 什么是内核模块?

现在,你是不是想编写内核模块。你应该懂得C语言,写过一些用户程序, 那么现在你将要见识一些真实的东西。在这里,你会看到一个野蛮的指针是如何 毁掉你的文件系统的,一次内核崩溃意味着重启动。

什么是内核模块?内核模块是一些可以让操作系统内核在需要时载入和执 行的代码,这同样意味着它可以在不需要时由操作系统卸载。它们扩展了操作系 统内核的功能却不需要重新启动系统。举例子来说,其中一种内核模块时设备驱 动程序模块,它们用来让操作系统正确识别,使用安装在系统上的硬件设备。如 果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜 像来加入新的功能。这还意味着一个臃肿的内核。


1.2. 内核模块是如何被调入内核工作的?

你可以通过执行lsmod命令来查看内核已经加载了哪 些内核模块, 该命令通过读取/proc/modules文件的内容 来获得所需信息。

这些内核模块是如何被调入内核的?当操作系统内核需要的扩展功能不存 在时,内核模块管理守护进程kmod[1]执行modprobe去加载内核模 块。两种类型的参数被传递给modprobe:

  • 一个内核模块的名字像softdog或是ppp

  • 通用识别符像char-major-10-30

当传递给modprobe是通用识别符时,modprobe首先在文件 /etc/modules.conf查找该字符串。如果它发现的一行别名像:

alias char-major-10-30 softdog
	

它就明白通用识别符是指向内核模块softdog.o

然后,modprobe遍历文件/lib/modules/version/modules.dep 来判断是否有其它内核模块需要在该模块加载前被加载。该文件是由命令depmod -a 建立,保存着内核模块的依赖关系。举例来说,msdos.o依赖于模块fat.o 内核模块已经被内核载入。当要加载的内核模块需要使用别的模块提供的符号链接时(多半是变量或函数),
那么那些提供这些所需符号链接的内核模块就被该模块所依赖。

最终,modprobe调用insmod先加载被依赖的模块,然后加载该被内核要求的模块。modprobe将insmod指向 /lib/modules/version/[2]目录,该目录为默认标准存放内核模块的目录。insmod对内核模块存放位置
的处理相当呆板,所以modprobe应该很清楚的知道默认标准的内核模块存放的位置。所以,当你想要载入一个内 核模块时,你可以执行:

insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
	

或只是执行"modprobe -a msdos"。

Linux提供modprobe, insmod and depmod在一个名为modutils 或 mod-utils的工具包内。

在结束本章前,让我们来看一个 /etc/modules.conf文件:

#This file is automatically generated by update-modules
path[misc]=/lib/modules/2.4.?/local
keep
path[net]=~p/mymodules
options mydriver irq=10
alias eth0 eepro
	

用'#'起始的行为注释。空白行被忽略。

以 path[misc]起始的行告诉modprobe用 /lib/modules/2.4.?/local替代搜寻 misc内核模块的路径。正如你看到的,命令解释器shell的元字符也可以使用。

path[net]起始的行告诉modprobe 在目录 ~p/mymodules搜索网络方面的内核模块。但是,在path[net] 指令之前使用的"keep" 指令告诉modprobe只是将该路径添加到标准搜索路径中,而不是像对待 misc前面那样进行替换。

以alias 起始的的行使modprobe加载eepro.o当kmod 以通用识别符'eth0' 要求加载相应内核模块时。

你不会发现像"alias block-major-2 floppy"这样的别名行在文件/etc/modules.conf 因为modprobe已经知道在绝大多数系统上安装的标准的设备的驱动模块。

现在你已经知道内核模块是如何被调入的了。当你想写你自己的依赖于其它模块的内核模块时, 还有一些内容没有提供。这个相对高级的问题将在以后的章节中介绍,当我们已经完成前面的学习后。


1.2.1. 在开始前

在我们介绍源代码前,有一些事需要注意。系统彼此之间的不同会导致许多困难。 顺利的编译并且加载你的第一个"hello world"模块有时就会比较困难。但是当你跨过 这道坎时,后面会顺利的多。


1.2.1.1. 内核模块和内核的版本问题

为某个版本编译的模块将不能被另一个版本的内核加载如果内核中打开了 CONFIG_MODVERSIONS选项。我们暂时不会讨论与此相关的 内容。在我们进入相关内容前,本文档中的范例可能在该选项打开的情况下无法 工作。但是,目前绝大多数的发行版是将该选项打开的。所以如果你遇到和版本 相关的错误时,最好,重新编译一个关闭该选项的内核。


1.2.1.2. 使用 X带来的问题

强烈建议你在控制台下输入文档中的范例代码,编译然后加载模块,而不是在X下。

模块不能像printf()那样输出到屏幕,但它们可以 记录信息和警告,当且仅当你在使用控制台时这些信息才能最终显示在屏幕上。 如果你从xterm中insmod一个模块,这些日志信息只会记录在你的日志文件中。 除了查看日志文件你将无法 得到输出信息。想要及时的获得这些日志信息,建议 所有的工作都在控制台下进行。


1.2.1.3. 编译相关和内核版本相关的问题

Linux的发行版经常给内核打一些非标准的补丁,这种情况回导致一些问题的发生。

一个更普遍的问题是一些Linux发行版提供的头文件不完整。编译模块时你将需要非常多 的内核头文件。墨菲法则之一就是那些缺少的头文件恰恰是你最需要的。

我强烈建议从Linux镜像站点下载源代码包,编译新内核并用新内核启动系统来避免以上 的问题。参阅"Linux Kernel HOWTO"获得详细内容。

具有讽刺意味的是,这也会导致一些问题。gcc倾向于在缺省的内核源文件路径(通常是/usr/src/)下寻找源代码文件。这可以通过gcc的-I 选项来切换。


Chapter 2. Hello World

2.1. 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(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() 还有一个“结束” (干一些收尾清理的工作)的函数被称为cleanup_module() ,当内核模块被rmmod卸载时被执行。实际上,从内核版本2.3.13开始这种情况有些改变。 你可以为你的开始和结束函数起任意的名字。 你将在以后学习如何实现这一点Section
2.3
。 实际上,这个新方法时推荐的实现方法。但是,许多人仍然使init_module()和 cleanup_module()作为他们的开始和结束函数。

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

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


2.1.1. 介绍printk()

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

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

当优先级低于int console_loglevel,信息将直接打印在你的终端上。如果同时 syslogdklogd都在运行,信息也同时添加在文件 /var/log/messages,而不管是否显示在控制台上与否。我们使用像 KERN_ALERT这样的高优先级,来确保printk()将信息输出到
控制台而不是只是添加到日志文件中。 当你编写真正的实用的模块时,你应该针对可能遇到的情况使用合 适的优先级。


2.2. 编译内核模块

内核模块在用gcc编译时需要使用特定的参数。另外,一些宏同样需要定义。 这是因为在编译成可执行文件和内核模块时, 内核头文件起的作用是不同的。 以往的内核版本需要我们去在Makefile中手动设置这些设定。尽管这些Makefile是按目录分层次 安排的,但是这其中有许多多余的重复并导致代码树大而难以维护。 幸运的是,一种称为kbuild的新方法被引入,现在外部的可加载内核模块的编译的方法已经同内核编译统一起来。想了解更多的编 译非内核代码树中的模块(就像我们将要编写的)请参考帮助文件linux/Documentation/kbuild/modules.txt

现在让我们看一个编译名为hello-1.c的模块的简单的Makefile:

Example 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#

这里有很多有用的信息去看。报告错误的作者信息,许可证信息,甚至对它接受参数的简短描述。

更详细的文档请参考 linux/Documentation/kbuild/makefiles.txt。在研究Makefile之前请确认你已经参考了这些文档。它很可能会节省你很多工作。

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

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

这儿是另一个练习。看到了在声明 init_module()上的注释吗? 改变返回值非零,重新编译再加载,发生了什么?


2.3. 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_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);

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

Example 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脚本的人这不难理解。这些在Makefile中随处可见 的obj-$(CONFIG_FOO)的指令将会在CONFIG_FOO被设置后扩展为你熟悉的obj-y或obj-m。这其实就是你在使用 make
menuconfig
编译内核时生成的linux/.config中设置的东西。


2.4. 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_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);

2.5. Hello World (part 4): 内核模块许可证和内核模块文档说明

如果你在使用2.4或更新的内核,当你加载你的模块时,你也许注意到了这些输出信息:

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

我推荐在/usr/src/linux-2.6.x/目录下使用类似grep -inr MODULE_AUTHOR *的命令。不熟悉命令行工具的人可能喜欢网上那样的方法, 搜索提供LXR做索引的内核源代码树的网站(或在自己的本地机器上安装它)。

使用像emacs或vi那样传统的Unix编辑器的用户将会发现tag文件很有用。它们能够在/usr/src/linux-2.6.x/ 下用make tagsmake TAGS生成。 一旦你在内核目录树中得到了这种tag文件,你就能把鼠标放到某个函数调用上使用一些组合键直接跳 到函数的定义处。

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_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");

2.6. 从命令行传递参数给内核模块

模块也可以从命令行获取参数。但不是通过以前你习惯的argc/argv

要传递参数给模块,首先将获取参数值的变量声明为全局变量。然后使用宏MODULE_PARM()(在头文件linux/module.h)。运行时,insmod将给变量赋予命令行的参数,如同 ./insmod mymodule.ko myvariable=5。为使代码清晰,变量的声明和宏都应该放在
模块代码的开始部分。以下的代码范例也许将比我公认差劲的解说更好。

module_param()需要三个参数,变量的名字,其类型和在sysfs中关联文件的权限。 整数型既可为通常的signed也可为unsigned。 如果你想使用整数数组或者字符串,请看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, & count, 0); /* put count into "count" variable */

将初始值设为缺省使用的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";
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.7. 由多个文件构成的内核模块

有时将模块的源代码分为几个文件是一个明智的选择。

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

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(KERN_INFO "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(KERN_INFO "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

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什么目标文件是模块的一部分。


2.8. 为已编译的内核编译模块

很显然,我们强烈推荐你编译一个新的内核,这样你就可以打开内核中一些有用的排错功能,像强制卸载模块(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中止编译过程。因为此时你需要的文件 已经就绪了。现在你可以返回你的模块目录然后编译加载它:此时模块将完全针对你的当前内核编译,加载时也不会由任何错误提示。


Chapter 3. Preliminaries

3.1. 内核模块对比用户程序

3.1.1. 内核模块是如何开始和结束的

用户程序通常从函数main()开始,执行一系列的指令并且 当指令执行完成后结束程序。内核模块有一点不同。内核模块要么从函数init_module 或是你用宏module_init指定的函数调用开始。这就是内核模块 的入口函数。它告诉内核模块提供那些功能扩展并且让内核准备好在需要时调用它。 当它完成这些后,该函数就执行结束了。模块在被内核调用前也什么都不做。

所有的模块或是调用cleanup_module或是你用宏 module_exit指定的函数。这是模块的退出函数。它撤消入口函数所做的一切。 例如注销入口函数所注册的功能。

所有的模块都必须有入口函数和退出函数。既然我们有不只一种方法去定义这两个 函数,我将努力使用“入口函数”和“退出函数”来描述 它们。但是当我只用init_modulecleanup_module时,我希望你明白我指的是什么。


3.1.2. 模块可调用的函数

程序员并不总是自己写所有用到的函数。一个常见的基本的例子就是 printf()你使用这些C标准库,libc提供的库函数。 这些函数(像printf()) 实际上在连接之前并不进入你的程序。 在连接时这些函数调用才会指向 你调用的库,从而使你的代码最终可以执行。

内核模块有所不同。在hello world模块中你也许已经注意到了我们使用的函数 printk() 却没有包含标准I/O库。这是因为模块是在insmod加 载时才连接的目标文件。那些要用到的函数的符号链接是内核自己提供的。 也就是说, 你可以在内核模块中使用的函数只能来自内核本身。如果你对内核提供了哪些函数符号 链接感兴趣,看一看文件/proc/kallsyms

需要注意的一点是库函数和系统调用的区别。库函数是高层的,完全运行在用户空间, 为程序员提供调用真正的在幕后 完成实际事务的系统调用的更方便的接口。系统调用在内核 态运行并且由内核自己提供。标准C库函数printf()可以被看做是一 个通用的输出语句,但它实际做的是将数据转化为符合格式的字符串并且调用系统调用 write()输出这些字符串。

是否想看一看printf()究竟使用了哪些系统调用? 这很容易,编译下面的代码。

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

使用命令gcc -Wall -o hello hello.c编译。用命令 strace hello行该可执行文件。是否很惊讶? 每一行都和一个系统调用相对应。 strace[3] 是一个非常有用的程序,它可以告诉你程序使用了哪些系统调用和这些系统调用的参数,返回值。
这是一个极有价值的查看程序在干什么的工具。在输出的末尾,你应该看到这样类似的一行 write(1, "hello", 5hello)。这就是我们要找的。藏在面具printf() 的真实面目。既然绝大多数人使用库函数来对文件I/O进行操作(像 fopen, fputs, fclose)。 你可以查看man说明的第二部分使用命令man
2 write
. 。man说明的第二部分 专门介绍系统调用(像kill()read())。 man说明的第三部分则专门介绍你可能更熟悉的库函数, (像cosh()random())。

你甚至可以编写代码去覆盖系统调用,正如我们不久要做的。骇客常这样做来为系统安装后门或木马。 但你可以用它来完成一些更有益的事,像让内核在每次某人删除文件时输出 “ Tee hee, that tickles!” 的信息。


3.1.3. 用户空间和内核空间

内核全权负责对硬件资源的访问,不管被访问的是显示卡,硬盘,还是内存。 用户程序常为这些资源竞争。就如同我在保存这 份文档同时本地数据库正在更新。 我的编辑器vim进程和数据库更新进程同时要求访问硬盘。内核必须使这些请求有条不紊的进行, 而不是随用户的意愿提供计算机资源。 为方便实现这种机制, CPU 可以在不同的状态运行。不同的状态赋予不同的你对系统操作的自由。Intel 80836 架构有四种状态。 Unix只使用了其中 的两种,最高级的状态(操作状态0,即“超级状态”,可以执行任何操作)和最低级的状态
(即“用户状态”)。

回忆以下我们对库函数和系统调用的讨论,一般库函数在用户态执行。 库函数调用一个或几个系统调用,而这些系统调用为库函数完成工作,但是在超级状态。 一旦系统调用完成工作后系统调用就返回同时程序也返回用户态。


3.1.4. 命名空间

如果你只是写一些短小的C程序,你可为你的变量起一个方便的和易于理解的变量名。 但是,如果你写的代码只是 许多其它人写的代码的一部分,你的全局一些就会与其中的全局变量发生冲突。 另一个情况是一个程序中有太多的 难以理解的变量名,这又会导致变量命名空间污染 在大型项目中,必须努力记住保留的变量名,或为独一无二的命名使用一种统一的方法。

当编写内核代码时,即使是最小的模块也会同整个内核连接,所以这的确是个令人头痛的问题。 最好的解决方法是声明你的变量为static静态的并且为你的符号使用一个定

抱歉!评论已关闭.