驱动程序是专用于控制和管理特定硬件设备的软件,因此也被称作设备驱动程序。从操作系统的角度来看,它可以位于内核空间(以特权模式运行),也可以位于用户空间(具有较低的权限)。
对于 Linux 驱动程序来说,其运行在内核空间,把硬件功能提供给用户程序。
本篇文章主要介绍Linux驱动程序的一些基础知识。后面的文章再逐步展开。
内核空间和用户空间
内核空间和用户空间的概念有点抽象,主要涉及内存的访问权限。内核是有特权的,而用户应用程序则是受限制的。
内核空间
内核驻留和运行的地址空间。
内核内存受访问标志保护,只能由内核访问,用户应用程不能访问。另一方面,内核可以访问整个系统内存,因为它在系统上以更高的优先级运行。在内核模式下,CPU可以访问整个内存(内核空间和用户空间)。
用户空间
用户程序运行的地址空间。
在用户模式下,CPU只能访问标有用户空间访问权限的内存。用户应用程序运行到内核空间的唯一方法是通过系统调用。
当进程执行系统调用时,软件中断被发送到内核,这将打开特权模式,以便该进程可以在内核空间中运行。系统调用返回时,内核关闭特权模式,进程再次受限。
模块
Linux内核可以在运行时扩展。当系统运行时,我们可以向内核添加、删除功能。
可以在运行时添加到内核中的代码被称为“模块”。内核模块是即插即用的,一旦插入就可以使用。
模块要运行,应该先把它加载到内核,可以用 insmod 或 modprobe 来实现,前者需要指定模块路径作为参数,这是开发期间的首选;后者更智能化,是生产系统中的首选。
insmod /test/mydrv.ko
常用的模块卸载命令是 rmmod,使用该命令时,应该把要卸载的模块名作为参数向其传递。当卸载某个模块时,不会有其他影响,则会直接卸载;若有不良影响,内核会阻止这次卸载。
rmmod mymodule
或者使用下边的指令
modeprobe -r mymodule
设备模块分类
Linux系统的模块有三种基本类型:
- 字符模块
- 块模块
- 网络模块
对应的设备设备驱动程序:
- 字符设备驱动
- 块设备驱动
- 网络设备驱动
字符设备是个能够像字节流一样被访问的设备,由字符设备驱动程序来实现。
块设备每次只能传输一个或者多个完整的块,每块包含512字节(或者2的更高次幂字节的数据)。
网络接口由内核中的网络子系统驱动,负责发送和接收数据包。网络驱动程序不需要知道各个连接的相关信息,它只负责处理数据包即可。
当然还有其他划分驱动程序模块的方法,此处不再赘述。
驱动程序框架
Linux 驱动程序是有固定框架的,我们按照既定的框架,填写内容即可。
先看一个简单的内核模块程序 helloworld.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/* 模块入口点函数 */
static int helloworld_init(void)
{
pr_info("Hello world!\n");
return 0;
}
/* 模块出口点函数 */
static void helloworld_exit(void)
{
pr_info("End of the world\n");
}
/* 指定函数用途 */
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_AUTHOR("zsky");
MODULE_LICENSE("GPL");
内核驱动程序与用户空间的程序是有很大区别的。
内核模块驱动程序有入口点和出口点,函数名字可以任意。用户程序的入口函数名称一般为 main()。
对于内核模块程序来说,需要开发人员指定入点和出点函数。在上例中,module_init()用于声明模块加载(使用 insmod 或 modprobe)时应该调用的函数为 helloworld_init,入口函数中要完成的操作是定义模块的行为。
module_exit() 用于声明模块卸载(使用 rmmod )时应该调用的函数为 helloworld_exit。
模块加载或者卸载后,init 函数或者 exit 函数立即运行一次。
在编写驱动程序的时候,需要包含很多头文件,以便获取函数、数据类型、变量的定义。有几个头文件是专门用于模块的:
#include <linux/init.h>
#include <linux/module.h>
module.h包含可装载模块需要的大量符号和函数的定义。init.h 用于指定入口函数和出口函数。
模块信息
内核模块使用其 .modinfo 部分来存储关于模块的信息,所有MODULE_*宏都用参数传递的值更新这部分的内容 。
其中一些宏是 MODULE_DESCRIPTION()、MODULE_AUTHOR() 和 MODULE_LICENSE()。
MODULE_LICENSE() 告诉内核模块采用何种许可,他对模块行为有影响,如果与指定的许可不兼容将导致内核模块被污染。
MODULE_AUTHOR() 用于声明模块的作者。
MODULE_DESCRIPTION() 简要描述模块的功能。
错误和消息打印
在模块函数处理过程中,一定要检测返回值,确保所有的请求操作已经真正成功。
当遇到错误时,必须撤销在这个错误发生之前的所有设置。通常的做法是使用goto语句。
ptr = kmalloc(sizeof (device_t));
if(!ptr)
{
ret = -ENOMEM
goto err_alloc;
}
ev = init(&ptr);
if(dev)
{
ret = -EIO
goto err_init;
}
eturn 0;
err_init:
free(ptr);
err_alloc:
return ret;
若模块装载过程中出错,要将出错之前的任何注册工作全部撤销,否则内核会处于一种不稳定的状态,因为内核中包含了一些指向并不存在的代码内部指针。
错误有时会跨越内核空间,传播到用户空间。如果返回的错误是对系统调用(open、read、ioctl、mmap)的响应,则该值将自动赋给用户空间 errno 全局变量,在该变量上调用 strerror(errno) 可以将错误转换为可读字符串。
当返回指针的函数返回错误时,通常返回的是NULL 指针。而去检查为什么会返回空指针是没有任何意义的,因为无法准确了解为什么会返回空指针。为此,内核提供了3个函数 ERR_PTR、IS_ERR 和 PTR_ERR:
void *ERR_PTR(long error);
long IS_ERR(const void *ptr);
long PTR_ERR(const void *ptr);
ERR_PTR 函数实际上把错误值作为指针返回。假若函数在内存申请失败后要执行语句 return -ENOMEM,则必须改为这样的语句:return ERR_PTR (-ENOMEM);。
IS_ERR 函数用于检查返回值是否是指针错误:if(IS_ERR(foo))。
PTR_ERR 函数返回实际错误代码:return PTR_ERR(foo);。
消息打印--printk()
不同于用户空间的 printf()函数。printk()是在内核空间使用的,其作用和在用户空间使用 printf() 一样,执行 dmesg 命令可以显示 printk() 写入的信息。
根据所打印消息的重要性不同,可以选用 include/linux/kern_levels.h 中定义的八个级别的日志消息,每个级别对应iyge字符串格式的数字,其优先级与该数字的值成反比:
#define KERN_SOH "\001" /* ASCII头开始 */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* 系统不可用*/
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动*/
#define KERN_CRIT KERN_SOH "2" /* 重要条件*/
#define KERN_ERR KERN_SOH "3" /* 错误条件*/
#define KERN_WARNING KERN_SOH "4" /* 警报条件*/
#define KERN_NOTICE KERN_SOH "5" /* 正常但重要的情况*/
#define KERN_INFO KERN_SOH "6" /* 信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试级别消息 */
举例:
printk(KERN_ERR "This is an error\n");
实际上可以使用以下宏,其名称更有意义,它们是对前面所定义内容的包装—— pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info和 pr_debug。
printk() 的实现是这样的:调用它时,内核会将消息日志级别与当前控制台的日志级别进行比较;如果前者比后者更高(值更低),则消息会立即打印到控制台。
模块参数
像用户程序一样,内核模块也可以接收命令行参数。这样能够根据给定的参数动态地改变模块的行为,开发者不必在测试/调试期间无限期地修改/编译模块。
为了对此进行设置,首先应该声明用于保存命令行参数值的变量,并在每个变量上使用 module_param() 宏:
module_param(name, type, perm);
- name:用作参数的变量的名称。
- type:参数的类型(bool、charp、byte、short、ushort、int、uint、long、ulong),其中 charp 代表字符指针。
- perm:代表/sys/module/<module>/parameters/<param>文件的权限,其中包括S_IWUSR、S_IRUSR、S_IXUSR、S_IRGRP、S_WGRP和S_IRUGO 。
当使用模块参数时,应该用 MODULE_PARM_DESC 描述每个参数。这个宏将把每个参数的描述填充到模块信息部分。
举例:
module_param(myint, int, S_IRUGO);
module_param(mystr, charp, S_IRUGO);
module_param_array(myarr, int,NULL, S_IWUSR|S_IRUSR);
MODULE_PARM_DESC(myint,"this is my int variable");
MODULE_PARM_DESC(mystr,"this is my char pointer variable");
MODULE_PARM_DESC(myarr,"this is my array of int");
要在加载该模块时提供参数,请执行以下操作:
# insmod hellomodule-params.ko
mystring="packtpub" myint=15 myArray=1,2,3
在加载模块之前,执行modinfo可以显示该模块支 持的参数说明:
modinfo ./helloworld-params.ko