第二章 建立和运行模块
2.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); 这个模块定义了两个函数,这两个函数是最基础的也是必须的:
一个在模块加载到内核时被调用(hello_init)以及一个在模 块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色.
另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该 模块带有一个自由的许可证;
printk函数在 Linux 内核中定义并且对模块可用; 它与标准 C 库函数 printf 的行为相似.字串 KERN_ALERT 是消息的优先级.
2.2 内核模块相比于应用程序
1 不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数 的任务是为以后调用模块的函数做准备;
2 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.
3 一个应用程序可以调用它没有定义的函数: 连接阶段使用合适的函数库解决了外部引用.一个模 块, 在另一方面, 只连接到内核, 它能够调用的唯一的函数是内核输出的那些; 没有库来连接.
因为没有库连接到模块中, 源文件不应当包含通常的头文件, 和非常特殊的情 况是仅有的例外. 只有实际上是内核的一部分的函数才可以在内核模块里使用. 内核相关 的任何东西都在头文件里声明, 这些头文件在你已建立和配置的内核源码树里; 大部分相 关的头文件位于 include/linux 和 include/asm,但是别的 include 的子目录已经添加 到关联特定内核子系统的材料里了
4 用户空间和内核空间 一个模块在内核空间运行, 而应用程序在用户空间运行.
5 内核编程与传统应用程序编程方式很大不同的是并发问题. 大部分应用程序, 多线程的应用程序是一个明显的例外, 典型地是顺序运行的, 从头至尾, 不必要担心其他事情会发生 而改变它们的环境. 内核代码没有运行在这样的简单世界中, 即便最简单的内核模块必须 在这样的概念下编写, 很多事情可能马上发生.
6 应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用 历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈; 它可能小到一个, 4096 字节的页.
PS:
常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名 通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调 用这个函数, 确信你知道你在做什么."内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮 点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮 点, 额外的负担不值得.
2.3 预备知识
#include <linux/module.h>
#include <linux/init.h>
这两个头文件在编写任何模块时都是必须的,module.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指定你的初始 化和清理函数,
不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包 含一行 MODULE_LICENSE:
MODULE_LICENSE("GPL");
可以在模块中包含的其他描述性定义有:
MODULE_AUTHOR ( 声明谁编写了模块 ),
MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ),
MODULE_VERSION ( 一个代 码修订版本号);
各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是, 一个内核代码 中相对近期的惯例是把这些声明放在文件末尾.
2.4 初始化与关停
2.4.1 初始化
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function); 初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见; 没有硬性规定这个, 然而, 因为没有函数能输出给内核其他部分, 除非明确请求.
声明中的 __init 标志是一个给内核的暗示, 给定的函数只是在初始化使用. 模块加载者在模块加载后会丢掉这个初始化函数, 使它的内存可做其他用途。
模块可以注册许多的不同设施, 包括不同类型的设备, 文件系统, 加密转换, 以及更多. 对每一个设施, 有一个特定的内核函数来完成这个注册。大部分注册函数以 register_ 做前缀, 因此找到它们的另外一个方法是在内核源码里查找 register_ .
2.4.2 关停
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);清理函数没有返回值, 因此它被声明为 void.
__exit 修饰符标识这个代码是只用于模块 卸载( 通过使编译器把它放在特殊的 ELF 段). 如果你的模块直接建立在内核里, 或者如果你的内核配置成不允许模块卸载, 标识为 __exit 的函数被简单地丢弃. 因为这个原因, 一个标识 __exit 的函数只在模块卸载或者系统停止时调用; 任何别的使用是错的.
2.4.3 错误处理
如果证实你的模块在一个特别类型的失败后完全不能加载, 你必须取消任何在失败前注册 的动作. 内核不保留已经注册的设施的每模块注册, 因此如果初始化在某个点失败, 模块 必须能自己退回所有东西.
错误恢复有时用 goto 语句处理是最好的.
int __init my_init_function(void)
{
int err;
err = register_this(ptr1, "skull"); /* registration takes a pointer and a name */
if (err)
goto fail_this;
err = register_that(ptr2, "skull");
if (err)
goto fail_that;
err = register_those(ptr3, "skull");
if (err)
goto fail_those;
return 0; /* success */
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_this(ptr1, "skull");
fail_this:
return err; /* propagate the error */
}my_init_function 的返回值, err, 是一个错误码.
在 Linux 内核里, 错误码是负数, 属 于定义于 <linux/errno.h>的集合. 如果你需要产生你自己的错误码代替你从其他函数得 到的返回值, 你应当包含<linux/errno.h> 以便使用符号式的返回值, 例如 -ENODEV, - ENOMEM, 等等
PS:
显然, 模块清理函数必须撤销任何由初始化函数进行的注册, 并且惯例(但常常不是要求的) 是按照注册时相反的顺序注销设施.
void __exit my_cleanup_function(void)
{
unregister_those(ptr3, "skull");
unregister_that(ptr2, "skull");
unregister_this(ptr1, "skull");
return;
} 2.5 模块参数
驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号( 如我们在下一章见到的 ) 到驱动应当任何操作的几个方面.这些参数的值可由 insmod 或者 modprobe 在加载时指定; 后者也可以从它的配置文件 (/etc/modprobe.conf)读取参数的值.
对HELLO WORLD 模块,我们增加 2 个参数: 一个整型值, 称为 howmany, 一个字符串称为 whom. 我们 的特别多功能的模块就在加载时, 欢迎 whom 不止一次, 而是 howmany 次. 这样一个模块 可以用这样的命令行加载:
insmod hellop howmany=10 whom="Mom" 一旦以那样的方式加载, hellop 会说 "hello, Mom" 10 次. 但是, 在 insmod 可以修改模块参数前, 模块必须使它们可用.
参数用moudle_param 宏 定义来声明, 它定义在 moduleparam.h. module_param 使用了 3 个参数: 变量名, 它的类型, 以及一个权限掩码用来做一个辅助的 sysfs 入口. 这个宏定义应当放在任何函数之 外, 典型地是出现在源文件的前面.
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, char, S_IRUGO);
声明一个数组参数, 使用: module_param_array(name,type,num,perm);
这里 name 是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一个整型变 量, perm 是通常的权限值. 如果数组参数在加载时设置, num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.