本章介绍所有的关于模块和内核编程的关键概念,通过一个 hello world 模块来认识驱动加载的流程及相关细节。
我是在虚拟机上进行的开发,查看当前 Linux 系统的内核版本:
uname -r
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);
Makefile
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
这个 makefile 在一次典型的建立中要被读 2 次,当从命令行中调用这个 makefile,它注意到 KERNELRELEASE 变量没有设置,它利用这样一个事实来定位内核源码目录,即已安装模块目录中的符号连接指回内核建立树,如果你实际上没有运行你在为其而建立的内核,你可以在命令行提供一个 KERNELDIR= 选项,设置 KERNELDIR 环境变量,或者重写 makefile 中设置 KERNELDIR 的那一行。一旦发现内核源码树,makefile 调用 default: 目标,来运行第 2 个 make 命令( 在 makefile 里参数化成 $(MAKE)) 象前面描述过的一样来调用内核建立系统,在第 2 次读,makefile 设置 obj-m,并且内核的 makefile 文件完成实际的建立模块工作。
①、准备好 hello.c 和 Makefile
②、make 编译
make
查看当前目录下编译产物,其中 hello.ko 是我们需要用到的驱动模块
③、加载 hello.ko 模块
sudo insmod hello.ko
④、lsmod 显示已经加载到内核中的模块的状态信息
lsmod
⑤、查看加载时的打印信息
sudo dmesg -c
⑥、卸载 hello.ko 模块
⑦、查看卸载时的打印信息
sudo dmesg -c
常见引起并发原因:
struct task_struct *current;
current->id :当前进程的id
current->comm. :当前进程的命令名
上面已讲解,这里不再讲述。
如果你编写一个模块想用来在多个内核版本上工作(特别地是如果它必须跨大的发行版本)你可能只能使用宏定义和 #ifdef 来使你的代码正确建立,利用 linux/version.h 中发现的定义。这个头文件,自动包含在 linux/module.h,定义了下面的宏定义:
模块初始化函数注册模块提供的任何功能,实际的初始化函数定义常常如:
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
每个非试验性的模块也要求有一个清理函数,它注销接口,在模块被去除之前返回所有资源给系统。这个函数定义为:
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
清理函数没有返回值, 因此它被声明为 void,__exit 修饰符标识这个代码是只用于模块卸载(通过使编译器把它放在特殊的 ELF 段),如果你的模块直接建立在内核里,或者如果你的内核配置成不允许模块卸载,标识为 __exit 的函数被简单地丢弃。因为这个原因,一个标识 __exit 的函数只在模块卸载或者系统停止时调用;任何别的使用是错的。再一次,moudle_exit 声明对于使得内核能够找到你的清理函数是必要的。
你必须记住一件事,在注册内核设施时,注册可能失败。即便最简单的动作常常需要内存分配,分配的内存可能不可用。因此模块代码必须一直检查返回值,并且确认要求的操作实际上已经成功。
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 */
}
模块清理函数必须撤销任何由初始化函数进行的注册,并且惯例(但常常不是要求的)是按照注册时相反的顺序注销设施。
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; /* success */
fail:
my_cleanup();
return err;
}
清理函数当由非退出代码调用时不能标志为 __exit。
内核的某些别的部分会在注册完成之后马上使用任何你注册的设施,这是完全可能的,换句话说,内核将调用进你的模块,在你的初始化函数仍然在运行时,所以你的代码必须准备好被调用,一旦它完成了它的第一个注册。不要注册任何设施,直到所有的需要支持那个设施的你的内部初始化已经完成。
模块参数可以在运行 insmod 或 modprobe 命令装载模块时赋值,modprobe 可以从配置文件(/etc/modprobe.conf)中读取参数值。
在 insmod 改变模块参数之前,模块必须让参数对 insmod 命令可见。参数使用 module_param(变量名,类型,访问许可值)宏来声明,它定义在 moduleparam.h。
所有的模块参数都应该在源文件中给定一个默认值。
模块中的钩子可让我们自定义类型
使用 <linux/stat.h> 中定义的值
大多数情况下不应该让模块参数是可写的
hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static char *hello_str = "hello";
static int hello_cnt = 2;
module_param(hello_str, charp, S_IRUGO);
module_param(hello_cnt, int, S_IRUGO);
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
printk("%s, %d\n", hello_str, hello_cnt);
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
加载 hello 模块驱动,并查看打印信息
sudo insmod hello.ko
sudo dmesg
模块加载后可以在 /sys/module/模块名/parameters 目录下查看参数
cd /sys/module/hello/parameters/
ls
cat hello_cnt
cat hello_str
用户空间驱动的好处在于:
用户空间的设备驱动的方法有几个缺点,最重要的是:
insmod modprobe rmmod 用户空间工具,加载模块到运行中的内核以及去除它们。
#include <linux/init.h> module_init(init_function); module_exit(cleanup_function); 指定模块的初始化和清理函数的宏定义。
__init __initdata __exit __exitdata 函数(__init 和 __exit)和数据(__initdata 和 __exitdata)的标记,只用在模块初始化或者清理时间。
#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 类似物。