引言
linux设备驱动的作用是隐藏具体的硬件细节,将所有的设备都抽象成一个个文件供应用程序调用,也就是说,在应用程序员的眼中,所有的硬件都是文件,U盘是文件,磁盘是文件,LED灯是文件,引脚是文件,蜂鸣器是文件,万物皆文件。
Linux内核非常庞大,组件也非常之多,我们有两种方法来将这些组件包含到内核中
- 直接编译到Linux内核中,这种方法会导致内核很大,而且每次需要添加或者删除组件的时候都得重新编译内核,而编译内核是一件非常耗时的事情
- 以模块的形式动态加载,在这种方法中,模块本身不会被编译进内核镜像,从而不会使内核过大,但模块一旦被加载,它就和内核中的其它模块一样。
linux设备驱动一般是以模块的形式加载卸载的。linux内核本身就是结合了宏内核与微内核的优点,既有宏内核的高效率,又有微内核的模块化。
文章主要分成如下几个部分
- 内核功能划分
- helloworld驱动模块
- 模块编译makefile解析
内核功能划分
可将内核功能分成如下几个部分
进程管理
进程管理主要负责创建和销毁进程,并处理它们和外部世界之间的连接。
内存管理
内存是计算机的主要资源之一,用来管理内存的策略是决定系统性能的一个关键因素。内核在有限的可用资源之上为每个进程创建一个虚拟地址空间。
文件系统
linux在很大程度上依赖于文件系统的概念,linux中所有对象都可以当作文件来看待。内核在没有结构的硬件上构造结构化的文件系统。,而文件抽象在整个系统中广泛使用。linux支持多种文件系统。
设备控制
几乎每一个系统操作最终都会映射到物理设备上。所有设备控制操作都由与被控制设备相关的代码来完成,这段代码就是驱动程序,这也是我接下来要学习的重点。
网络功能
网络功能也必须由操作系统来管理,因为大部分网络操作和具体进程无关。我们的重点放在设备控制上。
helloworld模块
许多编程书籍都会以一个helloworld实例来说明最简单的程序,虽然我们要学习的是设备驱动编程模型,而不是具体的程序,但提前看一下基于helloworld的小模块没准会有好处。如下所示:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Shijia Yin");
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);
上面是一个完整的内核模块,在它被编译好之后就可以被insmod加载了,具体如何编译,如何加载,如何卸载,后面再说,下面做几点说明
- hello_init:模块加载时会调用这个函数
- hello_exit:模块卸载时会调用这个函数
- module_init:指明模块加载时应该调用哪个函数,这里是hello_init
- module_exit:指明模块卸载时应该调用哪个函数,这里是hello_exit
- printk:内核打印函数,由于内核无法使用C库,所以需要自己的打印函数
- KERN_ALERT:指明要打印信息的优先级,如果信息优先级不够高,可能不会被打印出来
模块编译makefile解析
用来编译该模块的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
clean:
@rm -rf *.o *.mod.c *.ko *.symvers *.order *.makers
endif
该makefile在构造过程中会被读取两次;执行步骤如下:
- 当makefile从命令行调用时,它注意倒KERNELRELEASE尚未被设置,此时执行else的KERNELDIR和PWD设置;
- 然后执行default目标,make首先切换到-C指定的目录KERNELDIR,执行那个目录下的Makefile,KERNELRELEASE定义在KERNELDIR中的makefile中,执行完之后KERNELRELEASE已被设置;
- 通过M=$(PWD)第二次读取该makefile,它设置了obj-m,而内核的makefile负责真正的构造模块;
Tips
:有个疑问,为什么第二次读取makefile不会继续往下执行,如果继续往下执行不就构成了一个无限循环了吗,也许这是语法的规定,反正设置完obj-m之后,本次makefile就算结束了,下面就是内核makefile的事情了。
模块的装载与卸载
模块的装载使用insmod和modprobe,卸载使用rmmod。
insmod
模块是一个.ko文件,insmod可以接受一些配置参数用来配置模块
modprobe
和insmod类似,modprobe也是用来将模块装载到内核的,与insmod的区别在于,它会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有这类引用,modprobe会在当前模块路径中查找定义了这些符号的其它模块。如果modprobe找到了这些模块,它会同时装载这些模块到内核。如果在这种情况下使用insmod,则该命令会失败。modprobe是处理层叠模块的一个实用工具。
rmmod
模块卸载函数
快速参考
下面是本章用到的内核函数,变量,宏及/proc文件的一个快速参考
insmod
modprobe
rmmod
用来装载模块到正在运行的内核和移除模块
__init
__initdata
__exit
__exitdata
仅用于模块初始化或清除阶段的函数和数据标记。标记为初始化的项目会在初始化结束后丢弃;而标记为清除的项目在内核未被配置为可卸载模块的情况下被丢弃。
#include <linux/sched.h>
最重要的头文件之一。该文件包含了驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明
struct task_current *current;
当前进程
current->pid
current->comm
当前进程的PID和命令名
obj-m
由内核构造系统使用的makefile符号,用来确定在当前目录中应构造哪些模块
#include <linux/module.h>
必需的头文件,它必须包含在模块源代码中
#include <linux/version.h>
包含所构造内核版本信息的头文件
MODULE_AUTHER(auther);
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);
用来创建模块参数的宏,用户可在装载这些模块时调整这些参数的值
#include <linux/kernel.h>
int printk(const char * fmt, ...);
内核打印函数
引用
[1] 讯为驱动开发资料
[2] 《Linux设备驱动程序开发详解》宋宝华 第一版