第二章 构造和运行模块
一、Hello World模块
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);
hello.c
Makefile
# if KERNELRELEASE is defined, we\'ve benn 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
Makefile
moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核
printk函数和print函数类似,字串KERN_ALERT是消息优先级
与应用程序的不同
应用程序:
- 应用程序从头执行到尾。
- 可以不管理资源的释放或者其他的清理工作。
- 可以调用它并未定义的函数,通过解析外部引用从而使用适当的函数库。
- 开发过程中,段错误是无害的。并且可以使用调试器跟踪到源代码中的问题所在。
内核:
- 模块初始化的函数任务就是为以后调用模块函数预先做准备。
- 模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.。
- 模块能调用的函数仅仅是由内核导出的哪些函数,
- 一个内核的段错误即使不影响整个系统,也至少会杀死当前的进程。
用户空间和内核空间
模块在内核空间运行,应用程序在用户空间运行。
系统至少有两种级别,内核(超级模式),最低级模式(用户模式),以此控制对硬件的直接存取以及对内存的非法访问。
内核空间和用户空间,都有自己对应的内存映射(自己的地址空间)
内核的并发
即使最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生。
当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用.
如果不注意并发问题,可能导致出现很难调试的灾难性错误。
内核的版本检查
UTS_RELEASE
这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, “2.6.10”.
LINUX_VERSION_CODE
这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [4]4有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.
KERNEL_VERSION(major,minor,release)
这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如,KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.
二、模块编译和装载
首先,读者应该确保具备了正确的版本编译器、模块工具和其他必要的工具。
只有系统调用的名字前带有sys_前缀,而其他函数都没有这个前缀。这种命名在源码中grep系统调用时非常方便。
- insmod:可以接收一些命令行选项(参见手册),并且可以在装载时给模块变量。
- rmmod:如果内核认为模块依然使用,或者内核被配置为禁止移除,则无法移除模块。
- lsmod:通过读取/proc/modules虚拟文件来获得这些信息。有关已装载的模块信息在/sys/module下找到
- 查看系统日志文件(/var/log/messages或者系统配置使用的文件),将看到导致模块装载失败的具体原因
模块的堆叠
可以使用modeprobe,类似于insmod。
如果你的模块需要输出符号给其他模块使用,应当使用下面的宏定义:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
几乎所有模块都有
#include <linux/module.h>
#include <linux/init.h>
- MODULE_LICENSE(“GPL”)
- MODULE_AUTHOR(声明谁编写了模块)
- MODULE_DESCRIPION(一个人说明的关于模块做什么的声明)
- MODULE_VERSION(一个diamante修订版本号)
- MODULE_ALIAS(模块为人所知的另一个名字)
- MODULE_DEVICE_TABLE(来告知用户空间,模块支持哪些设备)
模块的初始化和关停
模块的初始化通常是:
static int __init initialization_function(void) { /* Initialization code here */ } module_init(initializatioon_function);
module_init
__init标志是给内核的一个暗示,给定的函数只是在初始化使用,加载后会丢掉这个初始化函数。
__initdata给只在初始化时用的数据,后面可能还会有__devinit和__devinitdata
清理函数
static void __exit cleanup_function(void) { /* Cleanup code here */ } module_exit(cleanup_function);
module_exit
清理函数没有返回值,__exit修饰符用于模块卸载
初始化汇总的错误处理
在之策内核设施时,注册可能失败,几遍最简单的动作常常需要内存分配,分配的内存可能不可用。
因此模块代码必须一致检查返回值,并且确认要求的操作实际上已经成功。
如果注册时发生任何错误,首先第一的事情是决定模块是否能够五路你如何继续初始化它自己。
任何时候,你的模块应当尽力向前,并提供事情失败后具备的能力。
如何模块证实失败,不能完全加载,必须取消失败前注册动作。因此初始化某个点失败,模块必须自己退回所有东西。否则内核就处于不稳定状态。
- 使用goto是处理错误回复的最好情况
- 使用错误编码是一个好习惯,诸如-ENODEV、-ENOMEM
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 */ }
错误处理
err是错误码,在<linux/errno.h>中。显然在模块清理函数中,是按照注册时相反的顺序注销设施。
void __exit my_cleanup_function(void) { unregister_those(ptr3, "skull"); unregister_that(ptr2, "skull"); unregister_this(ptr1, "skull"); return; }
模块清理处理
如果goto内容太多难以管理,清理函数必须撤销注册前的每一项。
struct something *item1; struct somethingelse *item2; int stuff_ok; void my_cleanup(void) { if(item1) release_thing(item1); if(item2) release_thing2(item2); if(stuff_ok) unregister_stuff(); return; } int __init my_init(void) { int err = -ENOMEM; item1 = allocate_thing(arguments); item2 = allocate_thing2(arguments2); if(!item2 || !item2) goto fail; err = register_stuff(item1, item2); if(!err) stuff_ok = 1; else goto fail; return 0; fail: my_cleanup(); return err; }
goto清理
模块加载竞争
内核的某些别的部分会在注册完成之后马上使用任何你注册的设施。换句话说,内核将调用进你的模块,在你初始化函数任然在运行时,所以你的代码必须准备好被调用,一旦完成注册。
模块参数
insmod hellop howmany=10 whom="Mom"
模块参数
在模块中参数用module_param宏定义来声明,它定义在moduleparam.h module_param使用了3个参数:变量名,类型,以及一个权掩码用来做一个辅助的sysfs入口。
static char *whom = "world"; static int howmany = 1; mdoule_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO);
模块参数用例
模块参数支持许多类型:
bool、invbool 一个布尔型,invbool颠倒了值
charp 字符指针
int、long、short、uint、ulong、ushort 整型,u开头是无符号值
数组参数,用逗号间隔的列表提供的值,模块加载者也支持。
module_param_array(name, type, num, perm);
name:数组名
type:数组元素类型
num:整型变量
perm:通常的权限值