设备驱动
- 设备驱动程序(Device Driver)是操作系统中的一种软件组件,负责管理和控制计算机硬件设备的工作。驱动程序通过提供操作系统和硬件设备之间的接口,使得操作系统和应用程序能够与硬件设备进行交互,而无需了解硬件的具体细节。
- 主要功能
- 硬件抽象:设备驱动程序屏蔽了硬件的复杂性,提供了统一的接口。
- 设备控制:设备驱动程序负责管理硬件设备的初始化、配置、运行和关闭。
- 中断处理:很多硬件设备通过中断与CPU通信。设备驱动程序要能够处理这些中断信号,并在需要的时候通知操作系统进行相应的处理。
- 数据传输:设备驱动程序通常负责在设备和主存储器之间传输数据。
- 设备管理:设备驱动程序还负责管理设备资源的分配和访问,确保多个进程或线程能够安全地访问同一个硬件设备。
- 分类
- 字符设备驱动:处理按字符流进行输入输出的设备,例如串口、键盘等。这类设备允许按字符读取或写入数据。
- 块设备驱动:处理按数据块读写的设备,例如硬盘、固态硬盘等。这类设备允许随机访问数据。
- 网络设备驱动:处理网络接口设备的驱动程序,负责在操作系统与网络硬件之间传输数据。
- Linux中的设备驱动
- 在Linux系统中,设备驱动程序一般以内核模块(Kernel Module)的形式存在,可以动态地加载和卸载。
- 设备驱动开发者需要了解Linux内核的编程接口(API)以及设备驱动的体系结构。
- 驱动开发流程步骤
- 注册设备:通过适当的API将设备驱动程序注册到内核中,以便内核能够识别并管理设备。
- 实现文件操作接口:Linux中的设备通常以文件的形式呈现,设备驱动需要实现诸如open()、read()、write()等文件操作接口。
- 处理中断:如果设备支持中断,驱动程序需要注册中断处理程序,处理硬件中断请求。
- 内存映射:某些设备可能需要将硬件寄存器或内存区域映射到用户空间,以提高数据传输效率。
一、内核模块
- 可以动态加载和卸载的代码段,用来扩展或修改Linux内核的功能,而无需重启系统或重新编译内核。
- 内核模块可以在系统运行时通过insmod命令加载到内核中,或者通过rmmod命令卸载,而无需重启系统。
- 通过内核模块机制,内核的功能可以按需扩展。
- 内核模块直接运行在内核空间,与内核共享相同的地址空间,因此模块能够直接访问内核的功能和数据结构。
- 一个简单的内核模块例子
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL"); // 指定GPL许可证
MODULE_AUTHOR("Author Name"); // 作者信息
MODULE_DESCRIPTION("A Simple Kernel Module"); // 模块描述
// 初始化函数
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, Kernel! My module has been loaded.\n");
return 0; // 返回0表示加载成功
}
// 退出函数
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel! My module has been unloaded.\n");
}
// 指定初始化和退出函数
module_init(my_module_init);
module_exit(my_module_exit);
二、内核模块必须包含的部分
-
模块初始化函数(module_init):这是内核加载模块时调用的函数,用来初始化模块,注册设备驱动或其他功能。通常通过module_init()宏指定初始化函数。
-
模块退出函数(module_exit):这是在卸载模块时调用的函数,用于清理资源、注销设备驱动等。通过module_exit()宏指定退出函数。
-
模块许可证声明(MODULE_LICENSE):指定模块的许可证类型,通常为"GPL"(GNU General Public License),表明模块可以与内核兼容,否则内核可能拒绝加载非GPL的模块。
三、常用的模块指令
- lsmod :列出已加载的模块
lsmod 用于显示当前内核中加载的模块列表。它读取 /proc/modules 文件,并以更友好的格式显示已加载的模块。 - modinfo - 显示模块的详细信息
modinfo 用于显示已加载模块或模块文件的详细信息,例如作者、许可证、依赖项等。你可以使用它来查看内核模块的元数据。
m o d i n f o 模块名 modinfo 模块名 modinfo模块名 - insmod - 手动加载模块
insmod 是一个用于手动加载模块到内核的命令。它需要指定模块的完整路径(通常是.ko文件)。不指定的话,默认为当前目录 - rmmod - 卸载模块
rmmod 用于卸载已经加载的模块。它会移除指定的模块,并解除与之相关的依赖关系。如果模块正在被其他模块使用或正在使用的资源没有被释放,它会阻止卸载。不用加ko - dmesg - 查看内核日志
dmesg 用于查看内核日志输出。加载或卸载内核模块时,内核日志会记录相关信息,比如模块加载成功与否、是否存在错误等。
- 如果模块加载时有任何 printk() 输出,它应该出现在 dmesg 中。你可以使用 grep 来过滤特定的输出,
dmesg | grep "Device registered"
找到所有包含 Device registered 的日志条目,这些通常是由 printk 函数输出的设备注册信息(假设驱动程序中有 printk("Device registered...") 这样的语句)。
dmesg | grep my_module//查询特定模块的日志信息
一、字符设备驱动
- 注册字符设备
- 在字符设备驱动中,首先要将设备注册到系统中。Linux内核通过register_chrdev()或**alloc_chrdev_region()**函数分配设备号,并注册字符设备。
- register_chrdev():用于直接注册字符设备。
- alloc_chrdev_region():用于动态分配主设备号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
参数:
dev_t *dev:指向保存设备号的变量的指针。该函数成功执行后,会通过这个指针将分配到的主设备号和次设备号返回给调用者。包含了主设备号和次设备号。在设备注册时,系统通过 dev_t 识别不同的设备。
unsigned int firstminor:表示要分配的第一个次设备号(minor number)。通常设置为 0,表示从第一个次设备号开始。
unsigned int count:表示需要分配的连续次设备号的数量。对于一个简单的字符设备,通常设置为 1。
如果你希望一个驱动程序支持多个次设备,可以设置 count 为更大的值。
const char *name:设备的名称,它通常是一个用于标识设备的字符串。例如,你可以将它设置为你的驱动程序名称或设备名称。
返回值:
成功时返回 0,表示设备号分配成功。
失败时返回负数的错误代码(如 -ENOMEM,表示内存不足)。
- 使用范例
static int major;
static int minor;
dev_t dev;
if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
major = MAJOR(dev);//获取主设备号
minor = MINOR(dev);//获取从设备号
- 定义file_operation结构
- file_operations结构体是字符设备驱动的核心,定义了设备的各种操作接口。需要实现其中几个函数来支持常见的文件操作。
- 常见的几个
static struct file_operations fops = {
.open = device_open, // 打开设备
.release = device_release, // 关闭设备
.read = device_read, // 读取设备数据
.write = device_write, // 写入设备数据
};
- 实现设备操作函数
static int device_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device opened.\n");
return 0;
}
static int device_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device closed.\n");
return 0;
等等
- 注册和注销字符设备
- 注册字符设备并将设备号绑定到file_operations结构体。通常在模块加载时完成这一过程
- 声明字符设备结构体,用于表示并管理驱动程序中的一个字符设备。之后可以通过 cdev_init() 初始化它,通过 cdev_add() 将它注册到内核,使其作为字符设备提供给用户使用。
- class_create
- 用于在 Linux 内核中创建一个设备类的函数。
- 设备类是用于分组和管理设备的抽象概念,它使得多个相似类型的设备可以被组织和识别,比如分为字符设备、块设备等。
- 通常,在字符设备驱动程序开发中,创建设备类是用于创建 /sys/class 下的设备节点目录,以便用户空间可以通过 /dev 目录访问设备文件。
struct class *class_create(struct module *owner, const char *name);
参数:
owner:指向拥有该类的模块。通常设置为 THIS_MODULE
name:类的名称,用于识别和创建类目录。它会出现在 /sys/class/ 目录下。
返回值:
class_create 成功时会返回一个指向 struct class 的指针,表示创建的类。如果失败,返回 ERR_PTR(-ENOMEM) 或者其他负值错误指针。
- device_create 是用于在 Linux 内核中创建设备节点的函数,常与 class_create 搭配使用。它会在 /sys/class/ 下创建设备相关的目录,并且通常会在 /dev 下创建设备文件,以供用户空间使用。用户可以通过该设备文件与内核中的字符设备进行交互,比如使用 open、read、write 系统调用。
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
参数:
cls: 指向通过 class_create 创建的设备类(struct class)。它指定了该设备属于哪个设备类,即设备会被归属到 /sys/class/cls_name/ 目录下。
parent: 设备的父设备。如果当前设备没有父设备(通常在多数情况下是 NULL),传 NULL 即可。
devt: 设备号,类型为 dev_t。这是通过 MKDEV(major, minor) 创建的一个 32 位的设备号,包含主设备号和次设备号,标识设备在系统中的唯一性。
drvdata: 驱动私有数据的指针。这个指针通常会指向设备的私有数据,设备驱动可以通过该指针存储和管理与设备相关的数据。
fmt: 设备名称格式字符串。设备文件的名称可以使用格式化字符串,类似于 printf。例如,可以为设备命名为 "my_device%d",其中 %d 表示动态分配的设备号或某些其他标识符。
static struct cdev my_cdev;
- 驱动初始化函数
- 主要任务
- 分配和注册设备号 register_chrdev_region()
- 初始化 cdev 结构体 cdev_init(&my_cdev, &fops);
- 将 cdev 添加到系统中 cdev_add(&my_cdev, dev, 1);
- 创建设备类,并在设备类下创建设备文件节点
- 主要任务
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h> // 设备类和设备节点所需的头文件
static int major; // 存储主设备号
static dev_t dev; // 存储设备号(包含主设备号和次设备号)
static struct cdev my_cdev; // 定义字符设备结构体
static struct class *my_class = NULL; // 设备类,用于设备节点的创建
static struct device *my_device = NULL; // 设备节点
extern struct file_operations fops; // 假设文件操作函数集合在其他地方定义
// 模块初始化函数
static int __init my_driver_init(void) {
int ret;
// 动态分配设备号
// dev:指向设备号变量的指针,用于存储分配到的设备号
// 0:从次设备号0开始分配
// 1:分配一个次设备号
// "my_device":设备名称,用于区分设备
ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
if (ret < 0