3、字符设备驱动基础:设备号、file_operations、cdev 与设备节点
字符设备驱动是 Linux 驱动开发中最基础的驱动类型,也是网络驱动中控制面(如 MDIO、GPIO、中断注册)的底层支撑。本章聚焦字符设备驱动的四个核心要素:设备号(主/次)、file_operations 结构体、cdev 的注册与注销,以及设备节点的创建(手动与自动)。掌握这些基础,后续编写网络设备控制接口时将得心应手。
学习提示:字符设备驱动是理解 VFS(虚拟文件系统)与硬件交互的最佳入口。本章所有代码示例均基于 Linux 6.x 内核,API 向后兼容。
3.1 设备号:主设备号与次设备号
Linux 内核通过设备号唯一标识一个字符设备。设备号是一个 dev_t 类型(32 位无符号整数),其中:
- 主设备号(major):高 12 位,标识设备对应的驱动程序。
- 次设备号(minor):低 20 位,由驱动内部使用,通常用于区分同类型设备的不同实例或硬件通道。
| 概念 | 位数 | 作用 | 示例 |
|---|---|---|---|
| 主设备号 | 12 bit | 内核根据主设备号找到对应的驱动 | 89(I2C 设备) |
| 次设备号 | 20 bit | 驱动内部区分不同设备或通道 | 0、1、2 |
| 设备号 | 32 bit | 内核全局唯一标识 | MKDEV(89, 0) |
内核提供以下宏操作设备号:
#include <linux/types.h>
#include <linux/kdev_t.h>
dev_t devno = MKDEV(major, minor); // 组合主次设备号
int major = MAJOR(devno); // 提取主设备号
int minor = MINOR(devno); // 提取次设备号
分配策略:
- 静态分配:驱动指定主设备号(需确保不冲突),使用
register_chrdev_region()。 - 动态分配:内核自动分配未使用的主设备号,使用
alloc_chrdev_region(),推荐用于可动态加载的驱动。
3.2 file_operations 结构体
file_operations 是字符设备驱动的核心接口,定义了用户空间通过系统调用(open、read、write、ioctl 等)操作设备时的回调函数。内核 VFS 层在每次文件操作时,都会查找该结构体中的函数指针。
| 成员 | 系统调用 | 典型实现 |
|---|---|---|
.open |
open() |
递增使用计数、初始化硬件 |
.release |
close() |
递减使用计数、释放资源 |
.read |
read() |
从硬件 FIFO 拷贝数据到用户空间 |
.write |
write() |
从用户空间拷贝数据到硬件 FIFO |
.unlocked_ioctl |
ioctl() |
控制命令(如设置波特率、复位) |
.mmap |
mmap() |
将 DMA 缓冲区映射到用户空间 |
典型定义示例:
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
// 从内核缓冲区拷贝数据到用户空间
if (copy_to_user(buf, kernel_buf, count))
return -EFAULT;
return count;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
注意事项:
.owner必须设置为THIS_MODULE,防止模块正在使用时被卸载。- 用户空间指针(
__user)必须使用copy_to_user/copy_from_user,不能直接解引用。 - 网络驱动中,
ioctl常用于配置 MAC 地址、PHY 寄存器等控制面操作。
3.3 cdev 的注册与注销
cdev 是内核中表示字符设备的核心结构体。驱动必须将 cdev 注册到内核,才能将设备号与 file_operations 关联起来。
3.3.1 注册流程
- 分配设备号:
alloc_chrdev_region()或register_chrdev_region()。 - 初始化 cdev:
cdev_init(&cdev, &fops)。 - 添加 cdev 到内核:
cdev_add(&cdev, devno, count)。
#include <linux/cdev.h>
static dev_t devno;
static struct cdev my_cdev;
static int __init my_init(void) {
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册 cdev
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(devno, 1);
return ret;
}
pr_info("My device registered, major=%d minor=%d\n",
MAJOR(devno), MINOR(devno));
return 0;
}
3.3.2 注销流程
- 删除 cdev:
cdev_del(&cdev)。 - 释放设备号:
unregister_chrdev_region(devno, count)。
static void __exit my_exit(void) {
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
pr_info("My device unregistered\n");
}
3.4 设备节点创建:手动与自动
设备节点是用户空间访问字符设备的入口,位于 /dev 目录下。创建方式分为手动和自动两种。
3.4.1 手动创建(mknod)
驱动加载后,使用 mknod 命令手动创建设备节点:
# mknod /dev/my_device c 240 0
# 参数说明:c 表示字符设备,240 为主设备号,0 为次设备号
# 查看已创建的节点
# ls -l /dev/my_device
crw-r--r-- 1 root root 240, 0 Jan 15 10:00 /dev/my_device
手动创建的缺点:
- 需要用户知道主设备号,不便于动态分配。
- 设备节点在系统重启后消失,无法持久化。
- 不适合热插拔场景(如 USB 设备)。
3.4.2 自动创建(udev / devtmpfs)
现代 Linux 系统使用 udev(用户空间设备管理器)或 devtmpfs 自动创建设备节点。驱动只需在初始化时创建 device 对象,并关联到 class,udev 会自动在 /dev 下生成节点。
自动创建步骤:
- 创建 class:
class_create(THIS_MODULE, "my_class")。 - 创建设备:
device_create(cls, NULL, devno, NULL, "my_device")。 - 注销时销毁:
device_destroy(cls, devno)+class_destroy(cls)。
#include <linux/device.h>
static struct class *my_class;
static int __init my_init(void) {
// ... 分配设备号、注册 cdev ...
// 创建 class
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
pr_err("Failed to create class\n");
// 回滚 cdev 和设备号
return PTR_ERR(my_class);
}
// 创建设备节点 /dev/my_device
device_create(my_class, NULL, devno, NULL, "my_device");
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, devno);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
}
3.5 完整驱动框架模板
将上述要素整合,得到一个可编译的字符设备驱动模板:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_device"
#define CLASS_NAME "my_class"
static dev_t devno;
static struct cdev my_cdev;
static struct class *my_class;
static char kernel_buf[1024];
static int my_open(struct inode *inode, struct file *filp) {
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
if (copy_to_user(buf, kernel_buf, count))
return -EFAULT;
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
if (copy_from_user(kernel_buf, buf, count))
return -EFAULT;
return count;
}
static const 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) {
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
if (ret < 0) return ret;
// 2. 初始化并注册 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
// 3. 自动创建设备节点
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, devno, NULL, DEVICE_NAME);
pr_info("Device %s registered (major=%d minor=%d)\n",
DEVICE_NAME, MAJOR(devno), MINOR(devno));
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, devno);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
pr_info("Device %s unregistered\n", DEVICE_NAME);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Network Driver Course");
MODULE_DESCRIPTION("Character device driver template");
编译与测试:
# Makefile 示例
obj-m += my_device.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
# 加载与测试
sudo insmod my_device.ko
echo "Hello" > /dev/my_device
cat /dev/my_device
sudo rmmod my_device
3.6 本章小结
| 知识点 | 核心函数/宏 | 关键注意点 |
|---|---|---|
| 设备号分配 | alloc_chrdev_region() |
动态分配优于静态,避免冲突 |
| file_operations | .open、.read、.write、.unlocked_ioctl |
用户指针必须用 copy_to/from_user |
| cdev 注册 | cdev_init() + cdev_add() |
先分配设备号,再添加 cdev |
| cdev 注销 | cdev_del() + unregister_chrdev_region() |
先删除 cdev,再释放设备号 |
| 自动创建节点 | class_create() + device_create() |
配合 udev 实现热插拔支持 |