Linux驱动开发 ---- 3_设备驱动基本概念
目录
- Linux驱动开发 ---- 3_设备驱动基本概念
- 📌 1. 什么是设备驱动?
- 📌 2. Linux 中的设备分类
- 📌 3. 主设备号 & 次设备号
- 📌 4. `/dev` 目录中的设备文件
- 📌 5. 如何查看设备号和驱动关系?
- 📌 6. 实践任务:设备文件和设备号探索
- 📌 7. 实践代码:模拟注册一个字符设备(不含实际操作)
- 🧪 实操步骤
- 📌 8. 总结回顾
- 🧠 今日练习题(可选)
- 🔧 什么是主设备号(**major number**)和次设备号(**minor number**)?
- 🔹 主设备号(Major Number)
- 🔸 次设备号(Minor Number)
- ✅ 如何在驱动中获取主次设备号?
- 🧪 补充知识:动态分配 vs 静态指定主设备号
- 🎯 总结
- 🔧 最常用的 `file_operations` 中的 3 个函数如下:
🎯 目标:
理解 Linux 中设备驱动的基本分类、作用与结构,掌握主设备号、次设备号、设备文件之间的联系。
📌 1. 什么是设备驱动?
设备驱动是操作系统内核中的一段代码,它充当操作系统与硬件设备之间的桥梁。用户空间无法直接访问硬件,必须通过驱动进行间接访问。
📌 2. Linux 中的设备分类
Linux 将设备分为以下几类:
类型 | 描述 | 示例 |
---|---|---|
字符设备 | 数据以字符流方式传输 | 串口、键盘、鼠标、LED |
块设备 | 数据以固定块大小读写 | 硬盘、U盘、SD 卡 |
网络设备 | 用于网络通信的数据收发 | 网卡、Wi-Fi 模块 |
📌 3. 主设备号 & 次设备号
Linux 使用 设备号(device number) 来标识设备。设备号由两个部分组成:
- 主设备号(Major Number):标识使用哪个驱动程序来操作设备。
- 次设备号(Minor Number):标识同一个驱动管理下的不同设备。
设备号用 dev_t
类型表示(32 位):
MAJOR(dev_t dev); // 获取主设备号
MINOR(dev_t dev); // 获取次设备号
MKDEV(int major, int minor); // 创建一个 dev_t 类型的设备号
📌 4. /dev
目录中的设备文件
- Linux 中所有设备都以“文件”的形式出现在
/dev
下。 - 用户通过访问这些 设备文件,内核通过背后的驱动程序来控制设备。
👀 查看设备文件:
ls -l /dev | grep tty
输出示例:
crw-rw---- 1 root dialout 4, 0 Apr 6 09:12 tty0
c
表示字符设备,b
表示块设备。4, 0
就是主设备号4
,次设备号0
。
📌 5. 如何查看设备号和驱动关系?
cat /proc/devices
输出示例:
Character devices:
1 mem
4 tty
5 /dev/tty
...
这意味着主设备号为 4 的字符设备是 tty
驱动管理的。
📌 6. 实践任务:设备文件和设备号探索
✅ 步骤一:查看已有设备号和对应驱动
cat /proc/devices
✅ 步骤二:查找一个字符设备的主/次设备号
ls -l /dev/null
结果:
crw-rw-rw- 1 root root 1, 3 /dev/null
说明 /dev/null
是字符设备,主设备号 1,次设备号 3。
📌 7. 实践代码:模拟注册一个字符设备(不含实际操作)
🧪 创建一个简单字符设备框架(demo_chrdev.c)
#include <linux/init.h> // 模块初始化/退出宏:module_init, module_exit
#include <linux/module.h> // 基本模块宏,如 MODULE_LICENSE
#include <linux/fs.h> // 分配/注册设备号,file_operations 等
#include <linux/cdev.h> // cdev 结构体及相关函数
#include <linux/device.h> // class_create, device_create 等
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/string.h> // strlen
#define DEVICE_NAME "demo_chrdev"
static int major; // 主设备号
static struct class *cls; // 设备类,用于在 /sys/class 下创建设备节点
static struct cdev demo_cdev; // 字符设备结构体
// 打开设备函数
static int demo_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "demo_chrdev: device opened\n");
return 0;
}
// 读取设备函数
static ssize_t demo_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
const char *msg = "Hello from kernel!\n";
size_t msg_len = strlen(msg);
// 如果偏移量超过数据长度,说明已经读完
if (*offset >= msg_len)
return 0;
// 若请求读取长度超过剩余长度,进行裁剪
if (len > msg_len - *offset)
len = msg_len - *offset;
// 从内核空间复制数据到用户空间
if (copy_to_user(buf, msg + *offset, len))
return -EFAULT;
*offset += len; // 更新偏移
return len; // 返回实际读取的字节数
}
// 文件操作集
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
};
// 模块初始化函数
static int __init demo_init(void) {
dev_t dev;
int ret;
// 分配一个设备号(动态分配主设备号)
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to alloc major number\n");
return ret;
}
major = MAJOR(dev); // 获取主设备号
printk(KERN_INFO "demo_chrdev: registered with major %d\n", major);
// 初始化 cdev 结构并注册到内核
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
cdev_add(&demo_cdev, dev, 1);
// 创建类和设备(这样在 /dev 目录下会自动生成对应设备节点)
cls = class_create(THIS_MODULE, DEVICE_NAME);
device_create(cls, NULL, dev, NULL, DEVICE_NAME);
return 0;
}
// 模块退出函数
static void __exit demo_exit(void) {
dev_t dev = MKDEV(major, 0);
device_destroy(cls, dev); // 删除设备节点
class_destroy(cls); // 删除类
cdev_del(&demo_cdev); // 注销 cdev
unregister_chrdev_region(dev, 1); // 释放设备号
printk(KERN_INFO "demo_chrdev: unregistered\n");
}
// 注册模块初始化和退出函数
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YourName");
MODULE_DESCRIPTION("Simple Character Device Example");
🧪 实操步骤
1). 创建驱动文件
vim demo_chrdev.c
将上面的代码粘贴进去。
2). 创建 Makefile
obj-m := demo_chrdev.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
3). 编译模块
make
成功后会生成 demo_chrdev.ko
模块文件。
4). 加载模块
sudo insmod demo_chrdev.ko
查看 dmesg
输出:
dmesg | tail
你会看到如:
demo_chrdev: registered with major 240
5). 查看并测试设备节点
ls /dev/demo_chrdev
如果存在,表示 device_create
已成功创建了字符设备节点。
读取字符设备的内容:
cat /dev/demo_chrdev
应该输出:
Hello from kernel!
6). 卸载模块
sudo rmmod demo_chrdev
再次查看 dmesg
:
dmesg | tail
会显示:
demo_chrdev: unregistered
📌 8. 总结回顾
内容项 | 说明 |
---|---|
字符/块/网络设备 | 不同类型的设备按数据交互方式分类 |
主设备号 | 标识一个驱动 |
次设备号 | 区分同类设备 |
/dev 设备文件 | 用户空间访问设备的接口 |
/proc/devices | 显示当前系统已注册的主设备号与驱动对应关系 |
file_operations | 驱动操作函数集合 |
cdev 注册字符设备 | 在内核中注册实际设备的关键步骤 |
🧠 今日练习题(可选)
在 Linux 中,每个设备文件(例如 /dev/demo_chrdev
)都通过一个“设备号”来和内核中的驱动程序建立联系。
这个设备号是一个 32 位整数,由两部分组成:
部分 | 名称 | 作用 |
---|---|---|
高 12 位 | 主设备号(major) | 用于标识驱动程序 |
低 20 位 | 次设备号(minor) | 用于标识具体的设备实例 |
例如你创建的 /dev/demo_chrdev
可能对应的是:
设备号 dev_t = 240:0 // 主设备号240,次设备号0
🔹 主设备号(Major Number)
主设备号的作用是:🔗 将设备文件和具体的驱动程序绑定起来。
当你操作设备文件 /dev/demo_chrdev
时,Linux 内核会:
- 查找该文件的 主设备号;
- 根据主设备号找到对应的
file_operations
结构(也就是你的驱动程序); - 调用
.open()
、.read()
、.write()
等函数。
举个例子:
/dev/sda
、/dev/sdb
可能都由同一个驱动程序(比如 SCSI 磁盘驱动)管理,那么它们主设备号是一样的(比如 8)。- 内核知道:主设备号 8 → scsi_disk_driver
🔸 次设备号(Minor Number)
次设备号的作用是:🔍 在同一个驱动程序中区分不同的设备实例。
一个驱动可能控制多个设备。例如:
设备文件 | 主设备号 | 次设备号 | 说明 |
---|---|---|---|
/dev/led0 | 240 | 0 | 第 0 个 LED |
/dev/led1 | 240 | 1 | 第 1 个 LED |
/dev/led2 | 240 | 2 | 第 2 个 LED |
虽然都是由主设备号 240 控制,但你在驱动里的 .open()
函数可以通过 iminor(inode)
获得当前打开的是哪个设备。
✅ 如何在驱动中获取主次设备号?
在 open 函数中可以这么写:
static int demo_open(struct inode *inode, struct file *file)
{
int major = imajor(inode);
int minor = iminor(inode);
printk(KERN_INFO "demo_chrdev: open - major=%d, minor=%d\n", major, minor);
return 0;
}
🧪 补充知识:动态分配 vs 静态指定主设备号
✅ 动态分配(推荐):
alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
由内核自动分配主设备号,避免冲突,适合模块式开发。
❌ 静态指定(可能冲突):
register_chrdev_region(MKDEV(240, 0), 1, DEVICE_NAME);
你自己指定主设备号,需确保没被系统占用,否则会失败。
🎯 总结
项目 | 主设备号(Major) | 次设备号(Minor) |
---|---|---|
作用 | 识别使用哪个驱动 | 识别使用驱动中的哪个设备 |
驱动层处理 | 用于注册 file_operations | 用于驱动内部区分设备实例 |
数量 | 一般一个驱动对应一个主号 | 一个主号下可以有多个次号 |
-
用
ls -l
命令找出/dev/zero
的主设备号与次设备号。
-
写出一个
file_operations
结构体中最常用的 3 个函数名称及作用。🔧 最常用的
file_operations
中的 3 个函数如下:
函数名 | 作用说明 |
---|---|
.open | 当用户通过 open() 打开设备文件时调用。通常用于设备初始化、分配资源等。 |
.read | 当用户通过 read() 从设备中读取数据时调用。用于从设备读取数据并传递给用户空间。 |
.write | 当用户通过 write() 向设备写入数据时调用。用于接收用户空间数据并传入内核或设备处理。 |
👇 简单示意:
static const struct file_operations my_fops = {
.owner = THIS_MODULE, // 指定模块所有者,防止模块被卸载
.open = my_open, // 设备打开
.read = my_read, // 读取数据
.write = my_write, // 写入数据
};
🔍 各函数的原型:
int open(struct inode *inode, struct file *file);
ssize_t read(struct file *file, char __user *buf, size_t count, loff_t *pos);
ssize_t write(struct file *file, const char __user *buf, size_t count, loff_t *pos);