Linux内核驱动模块
Linux 设备驱动会以内核模块的形式出现,因此,学会编写 Linux 内核模块编程是学习 Linux 设备驱动的先决条件。
1、Linux 内核模块简介
Linux 内核的整体结构已经非常庞大,而其包含的组件也非常多。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。Linux 提供了这样的一种机制,这种机制被称为模块(Module)。使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中。
2、初识Linux内核模块
先来看一个最简单的内核模块“Hello World”,代码如下:
#include <linux/init.h> #include <linux/module.h> static int hello_init(void) /*初始化函数*/ { printk(KERN_INFO " Hello World enter\n"); return 0; } static void hello_exit(void) /*卸载函数*/ { printk(KERN_INFO " Hello World exit\n "); } module_init(hello_init); /*模块初始化*/ module_exit(hello_exit); /*卸载模块*/ MODULE_LICENSE("Dual BSD/GPL"); /*许可声明*/ MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>"); MODULE_DESCRIPTION("A simple Hello World Module"); MODULE_ALIAS("a simplest module");
这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证.
注:内核模块中用于输出的函数是内核空间的 printk()而非用户空间的 printf(),具体用法参考附件 printk函数介绍。
3、几个常用命令
3.1 加载模块
通过“insmod ./hello.ko”命令可以加载,加载时输出“Hello World enter”。
3.2 卸载模块
通过“rmmod hello”命令可以卸载,卸载时输出“Hello World exit”。
3.3 查看系统中已经加载的模块列表
在Linux中,使用lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系,例如:
root@imx6:~$ lsmod Module Size Used by hello 1568 0 ohci1394 32716 0 ide_scsi 16708 0 ide_cd 39392 0 cdrom 36960 1 ide_cd
3.4 查看某个具体模块的详细信息
使用modinfo <模块名>命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持 的参数以及 vermagic:
root@imx6:~$ modinfo hello.ko filename: hello.ko license: Dual BSD/GPL author: Song Baohua description: A simple Hello World Module alias: a simplest module vermagic: 2.6.15.5 686 gcc-3.2 depends:
4、Linux 内核模块程序的结构
一个Linux内核模块主要由如下几个部分组成:
1、模块加载函数(一般需要) 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。 2、模块卸载函数(一般需要) 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。 3、模块许可证声明(必须) 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染 (kernel tainted)的警告。在Linux 2.6内核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权。Linux 2.6内核模块最常见的是以MODULE_LICENSE( “Dual BSD/GPL” )语句声明模块采BSD/GPL双LICENSE。 4、模块参数(可选) 模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。 5、模块导出符号(可选) 内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数。 6、模块作者等信息声明(可选) 用于申明模块作者的相关信息,一般用于备注作者姓名、邮箱等。
4.1 模块加载函数
Linux 内核模块加载函数一般以_ _init 标识声明,典型的模块加载函数如下:
static int _ _init initialization_function(void) { /* 初始化代码 */ } module_init(initialization_function);
模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回 0。而在初始化失败时,应该返回错误编码。在 Linux 内核里,错误编码是一个负值。
在 Linux 2.6 内核中,可以使用 request_module(const char *fmt, ...)函数加载内核模块,驱动开发人员可以通过调用。
request_module(module_name); /**** 或者 ****/ request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));
注意:在 Linux 中,所有标识为_ init 的函数在连接的时候都放在.init.text 这个区段内,此外,所有的 init 函数在区段.initcall.init 中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 _init 函数,并在初始化完成后,释放 init 区段(包括.init.text、.initcall.init 等)。
4.2 模块卸载函数
static void _ _exit cleanup_function(void) { /* 释放代码 */ } module_exit(cleanup_function);
模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完成与模块加载函数相反的功能,如下所示。
-
若模块加载函数注册了 XXX,则模块卸载函数应该注销 XXX。
-
若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。
-
若模块加载函数申请了硬件资源(中断、DMA 通道、I/O 端口和 I/O 内存等)的占用,则模块卸载函数应释放这些硬件资源。
-
若模块加载函数开启了硬件,则卸载函数中一般要关闭之。
4.3 模块参数
用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数,例如下列代码定义了 1 个整型参数和 1 个字符指针参数:
static char *book_name = " dissecting Linux Device Driver "; static int num = 4 000; module_param(num, int, S_IRUGO); module_param(book_name, charp, S_IRUGO);
参数类型可以是 byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool 或 invbool(布尔的反),在模块被编译时会将 module_param 中声明的类型与变量定义的类型进行比较,判断是否一致。
在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或 modprobe)模块名 参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。
4.4 内核模块的符号导出
模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名); EXPORT_SYMBOL_GPL(符号名);
导出的符号将可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含 GPL 许可权的模块。
4.5 模块声明与描述
在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名,例如:
MODULE_AUTHOR(author); MODULE_DESCRIPTION(description); MODULE_VERSION(version_string); MODULE_DEVICE_TABLE(table_info); MODULE_ALIAS(alternate_name);
对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE。
5、Linux 内核模块的编译
5.1 makefile
Linux 内核模块的编译需要编写makefile文件,具体的makefile文件编写介绍详见附件。
5.2 内核模块的makefile文件
编写一个简单的 Makefile:
KVERS = $(shell uname -r) #显示内核版本号 # Kernel modules obj-m += hello.o # Specify flags for the module compilation. #EXTRA_CFLAGS=-g -O0 build: kernel_modules kernel_modules: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules #modules表示编译成模块的意思 #CURDIR是make的内嵌变量,自动设置为当前目录 clean: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
该 Makefile 文件应该与源代码 hello.c 位于同一目录,开启其中的 EXTRA_CFLAGS=-g -O0可以得到包含调试信息的 hello.ko 模块。运行 make 命令得到的模块可直接在 PC 上运行。
注:uname 的更多用法详见附件
如果一个模块包括多个.c 文件(如 file1.c、file2.c),则应该以如下方式编写 Makefile:
obj-m := modulename.o modulename-objs := file1.o file2.o
obj-m是个makefile变量,它的值可以是一串.o文件的表列。
附件
1、printk函数介绍
printk的用法
内核通过 printk() 输出的信息具有日志级别,日志级别是通过在 printk() 输出的字符串前加一个带尖括号的整数来控制的,如 printk("<6>Hello, world!\n");。内核中共提供了八种不同的日志级别,在 linux/kernel.h 中有相应的宏对应。
#define KERN_EMERG "<0>" /* system is unusable */ #define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */ #define KERN_ERR "<3>" /* error conditions */ #define KERN_WARNING "<4>" /* warning conditions */ #define KERN_NOTICE "<5>" /* normal but significant */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
所以 printk() 可以这样用:printk(KERN_INFO "Hello, world!\n");。未指定日志级别的 printk() 采用的默认级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在 kernel/printk.c 中被定义为整数 4,即对应KERN_WARNING。
在头文件中共定义了八个可用的记录级;我们下面按其严重性倒序列出: KERN_EMERG Used for emergency messages, usually those that precede a crash. 用于突发性事件的消息,通常在系统崩溃之前报告此类消息。 KERN_ALERT A situation requiring immediate action. 在需要立即操作的情况下使用此消息。 KERN_CRIT Critical conditions, often related to serious hardware or software failures. 用于临界条件下,通常遇到严重的硬软件错误时使用此消息。 KERN_ERR Used to report error conditions; device drivers often use KERN_ERR to report hardware difficulties. 用于报告错误条件;设备驱动经常使用KERN_ERR报告硬件难题。 KERN_WARNING Warnings about problematic situations that do not, in themselves, create serious problems with the system. 是关于问题状况的警告,一般这些状况不会引起系统的严重问题。 KERN_NOTICE Situations that are normal, but still worthy of note. A number of security-related conditions are reported at this level. 该级别较为普通,但仍然值得注意。许多与安全性相关的情况会在这个级别被报告。 KERN_INFO Informational messages. Many drivers print information about the hardware they find at startup time at this level. 信息消息。许多驱动程序在启动时刻用它来输出获得的硬件信息。
KERN_DEBUG
Used for debugging messages.
用于输出调试信息
每一个字符串(由宏扩展而成)表示了尖括号内的一个整数。数值范围从0到7,数值越小,优先级越高。
2、makefile介绍
2.1 简介
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,也可以执行操作系统的命令。
编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。
2.2 具体操作
1.makefile的命名(两种)
-
makefile
-
Makefile
注:在同一个工程文件夹下建立名为makefile的txt文件。
2. makefile的规则
规则的三个要素:目标、依赖、命令
格式: 目标:依赖 tab 命令
3. 多文件的makefile的编写
makefile可以有多个规则,当第一个规则的的命令在执行的时候发现没有相应的依赖,就在下面的规则中找。最上面的规则的目标是终极目标一定写在最上面,也就是最后要生成的文件。
4.makefile中的变量
-
自定义变量 obj=main.o add.o sub.o 引用的时候直接使用 $(obj)
-
自动变量(只能在规则的命令中使用(第二行)) $< : 规则中的第一个依赖 $@:规则中的目标 $^: 规则中所有的依赖
-
模式自动匹配 子规则中: 目标:依赖 %.o:%.c 自动匹配终极目标的依赖 : main.o:main.c add.o:add.c sub.o:sub.c
-
makefile维护的变量(通常大写,自己可以修改) CC:cc(即gcc) APPFLAGS:预处理使用的选项 CFLAGS:编译的时候使用的选项 LDFLAGS:链接库使用的选项
5.makefile中的函数(都是有返回值)
-
wildcard 查找当前目录下所有.c文件,返回值给src src=$(wildcard ./*.c)
-
patsubst 替换所有.c文件为.o文件 obj=$(patsubst ./%.c, ./%.o, $(src))
6.make clean
在makefile最后加入clean的目标,为了重新编译所有文件得删除原来生成的文件
7.最终的简单的makefile
objs = hello1.o hello2.o #src=$(wildcard ./*.c) #查找该目录下所有的.c文件 #objs=$(patsubst ./%.c, ./%.o, $(src)) #将.o改为.c target = hello #CC=gcc $(target) : $(objs) #链接,将hello1.o hello2.o 链接为hello可执行程序 gcc $(objs) -o $(target) %.o : %.c #编译,将.c文件编译为.o文件 gcc -c $< -o $@ .PHONY : clean clean : rm $(objs) $(target) -f
3、uname
常用命令uname -v # uname -i #uname -a
dream361@master:~$ uname -n #主机名称 master dream361@master:~$ uname -v #操作系统版本 #-Ubuntu SMP Fri Mar :: UTC dream361@master:~$ uname -r #内核版本号 --generic dream361@master:~$ uname -s #内核名称 Linux dream361@master:~$ uname -p #CPU类型 x86_64 dream361@master:~$ uname -i #硬件平台 x86_64 dream361@master:~$ uname -o #操作系统名 GNU/Linux dream361@master:~$ uname -m #主机的硬件名 x86_64 dream361@master:~$ uname -a #显示所有信息 Linux master --generic #-Ubuntu SMP Fri Mar :: UTC x86_64 x86_64 x86_64 GNU/Linux