Linux设备驱动会以内核模块的形式出现,因此学会编写Linux内核模块编程是学习linux设备驱动的先决条件。
Linux内核的整体结构非常庞大,其包含的组件非常多。我们把需要的功能都编译到linux内核,以模块方式扩展内核功能。
先来看下最简单的内核模块
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_ALERT "Hello world! %s, %d\n", __FILE__, __LINE__);
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_ALERT "Hello world! %s, %d\n", __FILE__, __LINE__);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mikcy Liu");
MODULE_DESCRIPTION("A simple Module");
MODULE_ALIAS("a simple module");
头文件init.h包含了宏_init和_exit,它们允许释放内核占用的内存。
module_init()和hello_exit()是模块编程中最基本也是必须的两个函数。
module_init()是驱动程序初始化的入口点。
hello_exit是模块的退出和清理函数。此处可以做所有终止该驱动程序时相关的清理工作。
内核模块中用于输出的函数式内核空间的printk()而非用户空间的printf(),printk()的用法和printf()相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段
前者可以定义输出级别,在 <内核目录>/include/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 condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
未设定级别的,在<内核目录>/kernel/printk.c中定义
/* printk's without a loglevel use this.. */
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */
只有当printk打印信息时的loglevel小于DEFAULT_CONSOLE_LOGLEVEL的值(优先级高于console loglevel),这些信息才会被打印到console上。
模块声明与描述
在linux模块中,我们可以使用
MODULE_LICENSE(license) //定义模块的license,一般为GPL,或相关公司的license
MODULE_AUTHOR //模块的作者
MODULE_DESCRIPTION //对模块程序的描述,string
MODULE_VERSION //版本
MODULE_DEVICE_TABLE //模块程序所支持的设备,string
MODULE_ALIAS //别名
MODULE_PARM(var,type) //模块参数
==========================模块编译================================
首先看看Makefile文件:
obj-m := hello.o
KERNEL_BUILD := /lib/modules/$(shell uname -r)/build
all:
make -C $(KERNEL_BUILD) M=$(shell pwd) modules
clean:
-rm -rf *.o *.ko *.mod.c .*.cmd *.order *.symvers .tmpversions
KERNELBUILD :=/lib/modules/$(shell uname -r)/build是编译内核模块需要的Makefile的路径,Ubuntu下是/lib/modules/2.6.31-14-generic/build
如果是Arm平台的开发板,则-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层Makefile文件.
make -C $(KERNEL_BUILD) M=$(shell pwd) modules 编译内核模块。-C 将工作目录转到KERNEL_BUILD,调用该目录下的Makefile,并向这个Makefile传递参数M的值是$(shell pwd) modules。
M=选项让该makefile在构造modules目标之前返回到模块源代码目录。然后modules目标指向obj-m变量中设定的模块
执行make命令开始编译模块,生成hello.ko,执行make clean可清除编译产生的文件。
======================模块操作=========================
1、添加模块
insmod hello.ko
2、查看模块
lsmod | grep hello
lsmod命令实际上读取并分析/proc/modules文件,也可以cat /proc/modules文件
在模块所在目录下执行
modinfo hello.ko可以查看模块信息,如下所示
filename: hello.ko
alias: a simple module
description: A simple Module
author: Mikcy Liu
license: GPL
srcversion: 875C95631F4F336BBD4216C
depends:
vermagic: 3.5.0-17-generic SMP mod_unload modversions 686
3、删除模块
rmmod hello
=======================模块函数解析===========================
模块加载函数
Linux内核模块加载函数一般以__init标识声明,典型的模块加载函数的形式如下:
static int __init initialization_function(void) {
//初始化代码
}
module_init(initialization_function);
模块加载函数必须以“module_init(函数名)”的形式指定。它返回整形值,若初始化成功,应返回0。而在初始化失败时。应该返回错误编码。
在linux内核里,错误编码是一个负值,在<linux/errno.h>中定义,包含-ENODEV、-ENOMEM之类的符号值。返回相应的错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。
在linux2.6内核中,所有标识为__init的函数在连接的时候都会放在.init.text(这是module_init宏在目标代码中增加的一个特殊区段,用于说明内核初始化函数的所在位置)这个区段中,此外,所有的__init函数在区段.initcall.init中还保存着一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数,并在初始化完成后释放init区段(包括.init.text和.initcall.init等)。所以大家应注意不要在结束初始化后仍要使用的函数上使用这个标记。
模块卸载函数
Linux内核卸载模块函数一般以__exit标识声明,典型的模块卸载函数的形式如下:
static void __exit cleanup_function(void) {
//释放代码
}
module_exit(cleanup_function);
模块卸载函数在模块卸载时被调用,不返回任何值,必须以”module_exit(函数名)”的形式来指定
与__init一样__exit也可以使对应函数在运行完成后自动回收内存。
一般来说,模块卸载函数完成与模块加载函数相反的功能:
如果模块加载函数注册了 XXX模块,则模块卸载函数应注销XXX。
若模块加载函数动体申请了内存,则模块卸载函数应释放该内存。
若模块加载函数申请了硬件资源,则模块卸载函数应释放这些硬件资源。
若模块加载函数开启了硬件,则模块卸载函数应关闭硬件。