3. 字符设备驱动基础(一):主设备号与次设备号、设备文件、file_operations 结构体、简单的字符设备驱动实现
字符设备是嵌入式 Linux 驱动开发中最基础、最常用的设备类型。本章将深入剖析字符设备驱动的核心概念:主设备号与次设备号、设备文件以及file_operations 结构体,并通过一个完整的示例,带你实现第一个简单的字符设备驱动。
学习目标:理解设备号的分配与管理机制;掌握设备文件与内核驱动的关联方式;熟练填充 file_operations 结构体;能够独立编写并测试一个基础的字符设备驱动。
3.1 主设备号与次设备号
在 Linux 内核中,每个设备都由一个 设备号 唯一标识。设备号是一个 dev_t 类型的 32 位无符号整数,其中高 12 位表示主设备号(major number),低 20 位表示次设备号(minor number)。
- 主设备号:用于标识设备对应的驱动程序。不同的设备驱动拥有不同的主设备号。内核通过主设备号找到正确的驱动。
- 次设备号:由驱动程序内部使用,用于区分同一种驱动控制下的不同物理设备或逻辑单元。例如,一个串口驱动可以管理多个串口,每个串口使用相同的驱动(主设备号相同),但次设备号不同。
| 组成部分 | 位数 | 范围 | 说明 |
|---|---|---|---|
| 主设备号 | 12 位 | 0 ~ 4095 | 标识驱动程序 |
| 次设备号 | 20 位 | 0 ~ 1048575 | 标识具体设备实例 |
内核宏操作:使用
MAJOR(dev_t dev) 从设备号中提取主设备号,使用 MINOR(dev_t dev) 提取次设备号,使用 MKDEV(int major, int minor) 将主次设备号组合成 dev_t 类型。
3.2 设备文件
在 Linux 用户空间,设备通过 设备文件(又称设备节点)来访问。设备文件位于 /dev 目录下,是用户程序与内核驱动交互的桥梁。用户程序通过标准的文件操作(如 open、read、write)对设备文件进行操作,内核将这些操作转发给对应的驱动程序。
设备文件有两种类型:
- 字符设备文件:以
c开头,对应字符设备驱动,数据以字节流形式访问。 - 块设备文件:以
b开头,对应块设备驱动,数据以块为单位访问。
创建设备文件可以使用 mknod 命令:
mknod /dev/mydevice c 240 0
其中 c 表示字符设备,240 是主设备号,0 是次设备号。
注意事项:手动使用
mknod 创建设备文件需要 root 权限。在实际产品中,通常使用 udev 或 devtmpfs 自动管理设备节点,驱动加载时自动在 /dev 下生成对应的设备文件。
3.3 file_operations 结构体
file_operations 结构体是字符设备驱动的核心,它定义了驱动能够响应的所有文件操作函数指针。当用户程序对设备文件执行系统调用时,内核通过该结构体找到对应的驱动函数并执行。
常用的成员函数包括:
| 成员函数 | 系统调用 | 功能描述 |
|---|---|---|
open |
open() |
打开设备,进行初始化操作 |
release |
close() |
关闭设备,释放资源 |
read |
read() |
从设备读取数据 |
write |
write() |
向设备写入数据 |
unlocked_ioctl |
ioctl() |
设备控制命令(无大内核锁) |
llseek |
lseek() |
修改文件读写位置 |
关键点:驱动开发者不需要实现所有函数,未实现的函数指针应设置为
NULL,内核会使用默认行为。例如,llseek 为 NULL 时,默认不允许定位。
3.4 简单的字符设备驱动实现
下面我们通过一个完整的示例,实现一个简单的字符设备驱动。该驱动在内存中维护一个缓冲区,支持 open、read、write 和 release 操作。
3.4.1 驱动代码框架
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "my_char_dev"
#define BUFFER_SIZE 1024
static int major;
static char *kernel_buffer;
/* 打开设备 */
static int my_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device opened\n");
return 0;
}
/* 释放设备 */
static int my_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Device closed\n");
return 0;
}
/* 从设备读取数据 */
static ssize_t my_read(struct file *file, char __user *user_buf,
size_t count, loff_t *pos) {
size_t available = BUFFER_SIZE - *pos;
size_t to_read = count < available ? count : available;
if (to_read == 0)
return 0;
if (copy_to_user(user_buf, kernel_buffer + *pos, to_read)) {
return -EFAULT;
}
*pos += to_read;
return to_read;
}
/* 向设备写入数据 */
static ssize_t my_write(struct file *file, const char __user *user_buf,
size_t count, loff_t *pos) {
size_t available = BUFFER_SIZE - *pos;
size_t to_write = count < available ? count : available;
if (to_write == 0)
return -ENOSPC;
if (copy_from_user(kernel_buffer + *pos, user_buf, to_write)) {
return -EFAULT;
}
*pos += to_write;
return to_write;
}
/* file_operations 结构体定义 */
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
/* 模块初始化 */
static int __init my_init(void) {
major = register_chrdev(0, DEVICE_NAME, &my_fops);
if (major < 0) {
printk(KERN_ALERT "Failed to register device\n");
return major;
}
kernel_buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!kernel_buffer) {
unregister_chrdev(major, DEVICE_NAME);
return -ENOMEM;
}
memset(kernel_buffer, 0, BUFFER_SIZE);
printk(KERN_INFO "Module loaded, major number: %d\n", major);
return 0;
}
/* 模块退出 */
static void __exit my_exit(void) {
kfree(kernel_buffer);
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
3.4.2 驱动加载与测试步骤
- 编译驱动:使用内核源码树中的 Makefile 编译生成
.ko文件。 - 加载模块:
sudo insmod my_char_dev.ko,查看内核日志确认主设备号。 - 创建设备文件:
sudo mknod /dev/my_char_dev c <major> 0。 - 测试读写:
- 写入:
echo "Hello, Driver!" > /dev/my_char_dev - 读取:
cat /dev/my_char_dev
- 写入:
- 卸载模块:
sudo rmmod my_char_dev,并删除设备文件。
调试技巧:使用
dmesg 查看内核打印信息,确认驱动是否正常加载以及 open/read/write 操作是否被调用。
3.5 本章小结
- 主设备号标识驱动,次设备号标识设备实例。
- 设备文件是用户空间访问内核驱动的入口,位于
/dev目录。 - file_operations 结构体定义了驱动支持的所有文件操作,是驱动与 VFS 的接口。
- 通过
register_chrdev注册驱动,通过mknod创建设备文件,即可完成一个基础字符设备驱动的加载与测试。