1.Hello World 模块
#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);
moudle.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指定你的初始化和清理函数, 如我们在上面的 "hello world" 例子里见到的, 这个我们在下一节中再讲. 大部分模块还包含 moudleparam.h, 使得可以在模块加载时传递参数给模块. 我们将很快遇到.
不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE:
MODULE_LICENSE("GPL");
内核认识的特定许可有, "GPL"( 适用 GNU 通用公共许可的任何版本 ), "GPL v2"( 只适用 GPL 版本 2 ), "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", 和 "Proprietary". 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了. 象我们在第 1 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块后遇到问题的用户.
可以在模块中包含的其他描述性定义有 MODULE_AUTHOR ( 声明谁编写了模块 ), MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ), MODULE_VERSION ( 一个代码修订版本号; 看 <linux/module.h> 的注释以便知道创建版本字串使用的惯例), MODULE_ALIAS ( 模块为人所知的另一个名子 ), 以及 MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 ).
各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是, 一个内核代码中相对近期的惯例是把这些声明放在文件末尾.
这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证; 没有这样的说明, 在模块加载时内核会抱怨.
printk 函数在 Linux 内核中定义并且对模块可用; 它与标准 C 库函数 printf 的行为相似. 内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用 printk 是因为, 在 insmod 加载了它之后, 模块被连接到内核并且可存取内核的公用符号.
你可以用 insmod 和 rmmod 工具来测试这个模块. 注意只有超级用户可以加载和卸载模块.
2.编译和加载
关键文件:在内核源码的 Document/kbuild
工具相关:Documentation/Changes 一直列出了需要的工具版本
创建一个 makefile
obj-m := hello.o
在从目标文件建立后结果模块命名为 hello.ko。如果你有一个模块名为 module.ko, 是来自 2 个源文件( 姑且称之为, file1.c 和 file2.c ), 正确的书写应当是:
obj-m := module.o
module-objs := file1.o file2.o
对于一个象上面展示的要工作的 makefile, 它必须在更大的内核建立系统的上下文被调用. 如果你的内核源码数位于, 假设, 你的 ~/kernel-2.6 目录, 用来建立你的模块的 make 命令( 在包含模块源码和 makefile 的目录下键入 )会是:
make -C ~/kernel-2.6 M=`pwd` modules
这个命令开始是改变它的目录到用 -C 选项提供的目录下( 就是说, 你的内核源码目录 ). 它在那里会发现内核的顶层 makefile. 这个 M= 选项使 makefile 在试图建立模块目标前, 回到你的模块源码目录. 这个目标, 依次地, 是指在 obj-m 变量中发现的模块列表, 在我们的例子里设成了 module.o.
键入前面的 make 命令一会儿之后就会感觉烦, 所以内核开发者就开发了一种 makefile 方式, 使得生活容易些对于那些在内核树之外建立模块的人. 这个窍门是如下书写你的 makefile:
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
再一次, 我们看到了扩展的 GNU make 语法在起作用. 这个 makefile 在一次典型的建立中要被读 2 次. 当从命令行中调用这个 makefile , 它注意到 KERNELRELEASE 变量没有设置. 它利用这样一个事实来定位内核源码目录, 即已安装模块目录中的符号连接指回内核建立树. 如果你实际上没有运行你在为其而建立的内核, 你可以在命令行提供一个 KERNELDIR= 选项, 设置 KERNELDIR 环境变量, 或者重写 makefile 中设置 KERNELDIR 的那一行. 一旦发现内核源码树, makefile 调用 default: 目标, 来运行第 2 个 make 命令( 在 makefile 里参数化成 $(MAKE))象前面描述过的一样来调用内核建立系统. 在第 2 次读, makefile 设置 obj-m, 并且内核的 makefile 文件完成实际的建立模块工作.
这种建立模块的机制你可能感觉笨拙模糊. 一旦你习惯了它, 但是, 你很可能会欣赏这种已经编排进内核建立系统的能力. 注意, 上面的不是一个完整的 makefile; 一个真正的 makefile 包含通常的目标类型来清除不要的文件, 安装模块等等. 一个完整的例子可以参考例子代码目录的 makefile.
模块建立之后, 下一步是加载到内核. 如我们已指出的, insmod 为你完成这个工作. 这个程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中任何未解决的符号连接到内核的符号表上. 但是不象连接器, 内核不修改模块的磁盘文件, 而是内存内的拷贝. insmod 接收许多命令行选项(详情见 manpage), 它能够安排值给你模块中的参数, 在连接到当前内核之前. 因此, 如果一个模块正确设计了, 它能够在加载时配置; 加载时配置比编译时配置给了用户更多的灵活性, 有时仍然在用.
modprobe 工具值得快速提及一下. modprobe, 如同 insmod, 加载一个模块到内核. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块. 当 modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核. 如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下一条 " unresolved symbols "消息.
如前面提到, 模块可以用 rmmod 工具从内核去除. 注意, 如果内核认为模块还在用( 就是说, 一个程序仍然有一个打开文件对应模块输出的设备 ), 或者内核被配置成不允许模块去除, 模块去除会失败. 可以配置内核允许"强行"去除模块, 甚至在它们看来是忙的. 如果你到了需要这选项的地步, 但是, 事情可能已经错的太严重以至于最好的动作就是重启了.
lsmod 程序生成一个内核中当前加载的模块的列表. 一些其他信息, 例如使用了一个特定模块的其他模块, 也提供了. lsmod 通过读取 /proc/modules 虚拟文件工作. 当前加载的模块的信息也可在位于 /sys/module 的 sysfs 虚拟文件系统找到.
在编译的时候还需要注意内核的版本和平台,即版本依赖和平台依赖性。
3.内核符号表
使用堆叠来划分模块成不同层, 这有助于通过简化每一层来缩短开发时间。
linux 内核头文件提供了方便来管理你的符号的可见性, 因此减少了命名空间的污染( 将与在内核别处已定义的符号冲突的名子填入命名空间), 并促使了正确的信息隐藏. 如果你的模块需要输出符号给其他模块使用, 应当使用下面的宏定义:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
上面宏定义的任一个使得给定的符号在模块外可用. _GPL 版本的宏定义只能使符号对 GPL 许可的模块可用. 符号必须在模块文件的全局部分输出, 在任何函数之外, 因为宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明. 这个变量存储于模块的一个特殊的可执行部分( 一个 "ELF 段" ), 内核用这个部分在加载时找到模块输出的变量.
4.模块参数
驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号到驱动应当任何操作的几个方面. 例如, SCSI 适配器的驱动常常有选项控制标记命令队列的使用, IDE 驱动允许用户控制 DMA 操作. 如果你的驱动控制老的硬件, 还需要被明确告知哪里去找硬件的 I/O 端口或者 I/O 内存地址. 内核通过在加载驱动的模块时指定可变参数的值, 支持这些要求.
这些参数的值可由 insmod 或者 modprobe 在加载时指定; 后者也可以从它的配置文件(/etc/modprobe.conf)读取参数的值. 这些命令在命令行里接受几类规格的值.
在 insmod 可以修改模块参数前, 模块必须使它们可用. 参数用 moudle_param 宏定义来声明, 它定义在 moduleparam.h
模块参数支持许多类型:
bool
invbool
一个布尔型( true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.
charp
一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.
int
long
short
uint
ulong
ushort
基本的变长整型值. 以 u 开头的是无符号值.
数组参数, 用逗号间隔的列表提供的值, 模块加载者也支持. 声明一个数组参数, 使用:
module_param_array(name,type,num,perm);
这里 name 是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一个整型变量, perm 是通常的权限值.
如果数组参数在加载时设置, num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.最后的 module_param 字段是一个权限值;
你应当使用 <linux/stat.h> 中定义的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示.
如果 perm 被设为 0, 就根本没有 sysfs 项. 否则, 它出现在 /sys/module 下面, 带有给定的权限. 使用 S_IRUGO 作为参数可以被所有人读取, 但是不能改变;
S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs 修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.
5.内核模块相比于应用程序
不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数的任务是为以后调用模块的函数做准备; 好像是模块说, " 我在这里, 这是我能做的."模块的退出函数( 例子里是 hello_exit )就在模块被卸载时调用. 它好像告诉内核, "我不再在那里了, 不要要求我做任何事了."这种编程的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.
应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调用时间内动态分配.
常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."
内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.
总结
-
insmod
modprobe
rmmod
-
用户空间工具, 加载模块到运行中的内核以及去除它们.
#include <linux/init.h>
module_init(init_function);
module_exit(cleanup_function);
-
指定模块的初始化和清理函数的宏定义.
__init
__initdata
__exit
__exitdata
-
函数( __init 和 __exit )和数据 (__initdata 和 __exitdata)的标记, 只用在模块初始化或者清理时间. 为初始化所标识的项可能会在初始化完成后丢弃; 退出的项可能被丢弃如果内核没有配置模块卸载. 这些标记通过使相关的目标在可执行文件的特定的 ELF 节里被替换来工作.
#include <linux/sched.h>
-
最重要的头文件中的一个. 这个文件包含很多驱动使用的内核 API 的定义, 包括睡眠函数和许多变量声明.
struct task_struct *current;
-
当前进程.
current->pid
current->comm
-
进程 ID 和 当前进程的命令名.
obj-m
-
一个 makefile 符号, 内核建立系统用来决定当前目录下的哪个模块应当被建立.
/sys/module
/proc/modules
-
/sys/module 是一个 sysfs 目录层次, 包含当前加载模块的信息. /proc/moudles 是旧式的, 那种信息的单个文件版本. 其中的条目包含了模块名, 每个模块占用的内存数量, 以及使用计数. 另外的字串追加到每行的末尾来指定标志, 对这个模块当前是活动的.
vermagic.o
-
来自内核源码目录的目标文件, 描述一个模块为之建立的环境.
#include <linux/module.h>
-
必需的头文件. 它必须在一个模块源码中包含.
#include <linux/version.h>
-
头文件, 包含在建立的内核版本信息.
LINUX_VERSION_CODE
-
整型宏定义, 对 #ifdef 版本依赖有用.
EXPORT_SYMBOL (symbol);
EXPORT_SYMBOL_GPL (symbol);
-
宏定义, 用来输出一个符号给内核. 第 2 种形式输出没有版本信息, 第 3 种限制输出给 GPL 许可的模块.
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
-
放置文档在目标文件的模块中.
module_init(init_function);
module_exit(exit_function);
-
宏定义, 声明一个模块的初始化和清理函数.
#include <linux/moduleparam.h>
module_param(variable, type, perm);
-
宏定义, 创建模块参数, 可以被用户在模块加载时调整( 或者在启动时间, 对于内嵌代码). 类型可以是 bool, charp, int, invbool, short, ushort, uint, ulong, 或者 intarray.
#include <linux/kernel.h>
int printk(const char * fmt, ...);
-
内核代码的 printf 类似物.