第二章 建立和运行模块
part1
-------------------------------------------------------------------------
2.2 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);
module_init内核宏 来指出hello_init()的角色是:模块加载到内核时被调用。
module_exit内核宏 来指出hello_exit()的角色是:模块从内核卸载时被调用。
MODULE_LICENSE)是用来告知内核:该模块带有一个自由的许可证,否则 在模块加载时内核会抱怨.
printk函数在Linux内核中定义并且对模块可用。 是因为在insmod加载了它之后, 模块被连接到内核并且可存取内核的公用符号。
KERN_ALERT 是消息的优先级
对上述模块的操作,需要root权限。
%make 要有Makefile文件
...
%insmod hello.ko
Hello, world
%rmmod hello.ko
Goodbye cruel world ;若消息没有显示,消息进一个系统日志文件中,例如/var/log/messages。
2.3内核模块和应用程序之间不同:
1 内核可以被加载/卸载模块
2 模块的退出函数 --必须小心恢复每个由初始化函数建立的东西,否则会保留一些东西直到系统重启.
应用程序的终止 可以在释放资源方面懒惰
3 模块 --只连接到内核. 是内核的一部分的函数才可以在内核模块里使用。
应用程序 可以连接到库函数。
4 内核模块 --只注册自己以便来服务将来的请求,并且它的初始化函数会立刻终止.
应用程序 从头至尾处理一个单个任务
5 模块缺乏浮点函数支持。
6 出现在内核的错误至少会杀掉当前进程。
在应用程序开发中段错误是无害的, 可以用调试器来追踪到错误。
7 模块在内核空间运行, 应用程序在用户空间运行。
8 模块程序是并发的。
在同一时间, 不止一个进程能够试图使用你的驱动.在多核上,模块可能被多个CPU执行。
用户程序,典型是顺序执行的,多线程例外。
9 当前进程
全局变量current指针,指向当前正在运行的进程.在<asm/current.h>中定义。它指向结构task_struct(在<linux/sched.h> 定义)
内核代码可以通过使用 current 来获得进程特定的信息。
10 内核堆栈很小,可能小到4096字节.模块函数会共享这个堆栈. 因此,不要声明一个巨大的自动变量; 需要大的结构时,应当在调用时间内动态分配.
应用程序存在于虚拟内存中, 有一个非常大的堆栈区.
11 有些以双下划线(__)开始的API函数名.标志它一个低层的接口组件, 应当小心使用.
2.4编译模块
编译前的准备工作:
1 Document/kbuild 目录下的文件,想理解表面之下的真实情况的人都要阅读一下.
2 Documentation/Changes 列出了需要的工具版本,在向前走之前参考之。
3 需要一个内核树,或者配置和建立内核,现在是时候去做了。 否则你无法为2.6内核建立可加载的模块(不是必要的).
当建立起所有东西后,对于前面展示的" hello world" 例子,在Makefile中有如下一行就能够编译了:
obj-m := hello.o
1 表明有一个模块要从目标文件 hello.o 建立. 在从目标文件建立后结果模块命名为 hello.ko.
2 如果建立的模块名为 module.ko,来自2个源文件 file1.c和file2.c 应如下写:
obj-m := module.o
module-objs := file1.o file2.o
4 如果你的内核源码 ~/kernel-2.6目录, 则make命令添加如下命令:
make -C ~/kernel-2.6 M=`pwd` modules
-C 选项提供你的内核源码目录,它在那里会发现内核的顶层 makefile.
M= 选项使 makefile 在试图建立模块目标前,回到你的模块源码目录. 其目标,是指在obj-m变量中查找模块列表,在此设成了module.o
5 若上面的太麻烦,使用下面的 Makefile。 在命令行输入make 可直接编译出 .ko文件
# 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
这个makefile 在一次典型的建立中要被读2次.工作过程如下
1 开始调用 makefile 时,KERNELRELEASE 变量没有设置.则使用 KERNELDIR= 选项:
KERNELDIR ?= /lib/modules/$(shell uname -r)/build 设置KERNELDIR为内核源代码的路径。
2 一旦发现内核源码树, makefile 调用 default下的语句:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules 调用内核建立系统
与 make -C ~/kernel-2.6 M=`pwd` modules 语句相同了。
3 makefile 设置 obj-m, 并且内核的 makefile 文件完成实际的建立模块工作.
6 下面是另外一个可以正确make的Makefile
obj-m := hello.o #模块编译的目标必须以obj-m 这样的形式指出
KERNELDIR ?= /lib/modules/$(shell uname -r)/build #模块的编译必须指定内核源代码的路径
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules #本行开头必须使用 Tab 键
2.4.2 加载和卸载模块
加载:
insmod 将成这个工作,这个程序加载模块的代码段和数据段到内核,然后连接模块中未连接的符号到内核符号表上.
1 内核不修改模块的磁盘文件, 而是拷贝到内存.
2 insmod 接收许多命令行选项 (详见manpage), 它在连接到当前内核之前,能够赋值给你模块中的参数.
注:insmod: 它依赖一个在kernel/module.c 中定义的系统调用.函数 sys_init_module 使用vmalloc 来分配内核内
存来存放模块,它接着拷贝模块代码到该区域, 在内核符号表查找引用,并调用模块的初始化函数来启动。
在内核代码,会发现系统调用的名子都以 sys_ 为前缀。
若出现 ‘unresolved symbols’ 时尝试下 modprobe工具。
卸载:
rmmod 工具从内核去除模块。
如果内核认为模块还在用或者内核被配置成不允许模块去除,模块去除会失败.
当然,可以配置内核允许"强行"去除模块,此时可能有严重的情况发生了。
查看模块信息:
lsmod 程序生成一个内核中当前加载的模块的列表.
vi /proc/modules
vi /sys/module 当前加载的模块的信息
版本依赖:
编译模块给一个特定的内核版本, 需要使用这个特定版本建立的系统和源码树。简单修改 KERNELDIR 变量, 就完成这个动作.
在 linux/version.h 定义了版本.
平台依赖:略。
2.5 内核符号表
1 内核符号表包含了所有的全局内核项(即全局函数和变量)
2 当使用堆叠的模块时, 使用modprobe工具是有帮助的.他与insmod相似。
3 如果本模块需要输出符号给其他模块使用,应当使用下面的宏定义: 符号必须在模块文件的全局部分输出
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
2.6 预备知识
几乎所有的模块都要包含如下两行:
#include <linux/module.h> 包含了大量加载模块需要的函数和符号的定义
#include <linux/init.h> 指定初始化和清理函数
其他可包含的内容:
moudleparam.h 使得可以在模块加载时传递参数给模块.
MODULE_LICENSE ("GPL"); 不包含则内核就假定它是私有的。 它可放于文件的任何位置。
MODULE_AUTHOR (声明谁编写了模块)
MODULE_DESCRIPION (一个人可读的关于模块做什么的声明)
MODULE_VERSION (一个代码修订版本号)
MODULE_ALIAS (模块为人所知的另一个名子)
MODULE_DEVICE_TABLE (告知用户空间模块支持那些设备)
初始化:
static int __init initialization_function(void) static:因为它们不会在特定文件之外可见,没有硬性规定这个。
{
/* Initialization code here */
}
module_init(initialization_function);
__init:
其 暗示,函数只在初始化使用.模块加载后会丢掉这个初始化函数,使它的内存可做其他用途.
一个类似的标签(__initdata)只在初始化时用的数据.
moudle_init
他是强制使用的.这个宏定义增加了特别的段到模块目标代码中,表明在哪里找到模块的初始化函数.
初始化错误处理
1 注册可能失败. 即便最简单的内存分配.必须一直检查返回值,并且确认要求的操作全部经成功.
2 若出现错误,要判断是否继续,若不继续,这要注销掉之前注册的动作。
3 错误恢复有时用goto语句。
4 <linux/errno.h> . 在Linux内核里包含了错误码,使用该错误码有好处,因为用户程序能够把它们转变为有意义的字串。
注销函数
static void __exit cleanup_function(void) 它注销接口, 在模块被去除之前返回所有资源给系统.
{
/* Cleanup code here */ 以相反的顺序注销注册的动作
}
module_exit(cleanup_function);
void. __exit
清理函数没被声明为void, __exit:标识这个代码是只用于模块卸载(
moudle_exit
声明对于使得内核能够找到你的清理函数是必要的.
如果你的模块没有定义一个清理函数, 内核不会允许它被卸载.
小技巧:
查找 EXPORT_SYMBOL 遍可找到由不同驱动提供的入口点.
查找 register_ 也可以,因为大部分注册函数以register_ 做前缀。
模块参数:
可以在命令行输入运行参数。应该包含文件moduleparam.h