文章目录
什么是Linux内核模块、驱动程序和ioctl?
Linux内核模块(Linux Kernel Module)和硬件驱动程序(Hardware Driver)
Linux使用可加载的内核模块(loadable kernel module, LKM)来在运行时动态的添加(或删除)Linux内核的代码。
它有几点好处:
体积:模块化的系统可以减少Linux内核的体积,需要用到的模块用时再加载。
可扩展+安全:内核模块是运行在内核态的,它与用户态应用分离,增强了安全性,又保证了内核的可扩展性。
一个比较典型的Linux可加载内核模块就是Linux中设备的驱动程序。
驱动程序如下图所示,它向下直接与硬件沟通,向上为系统调用提供具体实现。
经过驱动程序的这一层封装,让用户态的程序无需关心硬件运行细节,而直接使用其提供的API,降低了(用户态)程序开发者的开发难度。
ioctl
刚才提到,用户态程序是无法直接调用内核态API的,那么应用程序开发者如何与硬件沟通?
Linux就提供了一系列的系统调用,用来让开发者在用户态与系统沟通,进而与硬件沟通,其中就包括了我们常用的open、close、read、write和ioctl。
用户态的ioctl原型为
int ioctl(int fd, int cmd, ...)
而陷入内核之后,ioctl的函数原型为
int (*ioctl)(struct inode *node, struct file *filp, unsigned int cmd, unsigned long arg)
他们之间的关系如下图所示,fd被转换为两个结构体,用来标识操作的设备文件,cmd被原封不动的传入了驱动中。
写一个简单的内核模块
内核模块HelloWorld
我们结合代码来看一下一个最简单的内核模块的例子
#include <linux/init.h> // 包含了__init和__exit函数的宏定义
#include <linux/module.h> // 可加载内核模块的核心的头文件
#include <linux/kernel.h> // 包含了kernel中的类型、宏和函数等
MODULE_LICENSE("GPL"); //此模块使用的licence,如果不用GPL的话编译时会出warning
MODULE_AUTHOR("StayrealS"); ///模块作者,加载后使用modeinfo可以在系统中看到
MODULE_DESCRIPTION("A simple Linux driver."); //模块信息,加载后使用modeinfo可以在系统中看到
MODULE_VERSION("0.1"); ///模块版本,加载后使用modeinfo可以在系统中看到
/** @brief Module初始化函数,作为module的入点,使用insmod加载时会调用本函数。
*/
static int __init helloHW_init(void){
printk(KERN_INFO "HW: HelloWorld from the BBB LKM!\n");
return 0;
}
/** @brief 与init函数相似,在rmmod卸载本模块的时候会调用本函数进行清理等工作。
*/
static void __exit helloHW_exit(void){
printk(KERN_INFO "HW: Goodbye from the HW LKM!\n");
}
/** @brief 一个模块必需使用module_init() 和module_exit() 宏来
* 定义module的入点和出点
*/
module_init(helloHW_init);
module_exit(helloHW_exit);
内核模块Makefile
另外,还需要Makefile
obj-m+=hello.o
all:
make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules # 使用M=本文件夹的方式编译本文件夹下的.o为内核模块.ko
clean:
make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
其中$shell uname -r
用来打印本机linux 内核的版本信息,用于找寻linux内核关于编译module用到的工具。接下来,只需要使用make
就可以将hello world编译成为后缀名为.ko
的可加载module。
接下来,使用
insmod hello.ko # 加载模块
lsmod # 查看已加载的模块
rmmod hello.ko # 卸载模块
来加载或卸载编写好的模块。
刚才在编写模块的时候打印是使用了printk,它是printf的内核态版本。
printk的输出不会打印在linux终端上,但是会打印到内核缓冲区中,可以使用
dmesg # 查看内核缓冲区
dmesg -c # 清空内核缓冲区
编写一个提供ioctl接口的设备驱动程序(内核模块)
如第一部分中所说,设备驱动程序是内核模块的一个应用案例。本节我们将编写一个提供ioctl接口的设备驱动程序。
Linux的设备
设备的分类
Linux中I/O设备分为两类:字符设备和块设备。两种设备本身没有严格限制,但是,基于不同的功能进行了分类。
- 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,键盘、串口、调制解调器都是典型的字符设备。
- 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘、软盘、CD-ROM驱动器和闪存都是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
这两种设备都可以通过访问文件系统中的设备文件(在/dev
下)来访问。例如,接下来我们要编写的一个虚拟字符设备,就被挂载在/dev/test
处。
设备的主编号(major number)与副编号(minor number)
设备的主编号是为了让操作系统区分不同