3.1 字符设备驱动基础框架
字符设备是 Linux 驱动中最基础的设备类型,以字节流方式读写数据。本章从零搭建一个完整的字符设备驱动骨架,涵盖模块生命周期、设备号管理、文件操作接口注册等核心环节。
学习目标:掌握模块加载/卸载宏的使用、设备号动态分配与注销、file_operations 结构体填充、以及简单的 open/release 回调实现。
3.1.1 模块加载与卸载
Linux 驱动以内核模块形式存在,通过 module_init() 和 module_exit() 宏指定入口和出口函数。模块加载时完成资源申请,卸载时释放资源。
#include <linux/module.h>
#include <linux/init.h>
static int __init mychar_init(void)
{
printk(KERN_INFO "mychar: module loaded\n");
return 0;
}
static void __exit mychar_exit(void)
{
printk(KERN_INFO "mychar: module unloaded\n");
}
module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Simple char device driver example");
关键点:
__init 和 __exit 标记用于优化内存布局,加载完成后 init 函数占用的内存可被回收。
3.1.2 设备号注册与注销
每个字符设备在系统中由主设备号和次设备号唯一标识。主设备号对应驱动,次设备号对应具体设备实例。
| 概念 | 说明 | 范围 |
|---|---|---|
| 主设备号 | 标识设备驱动程序 | 0~4095(12位) |
| 次设备号 | 标识具体设备实例 | 0~1048575(20位) |
| 静态注册 | 手动指定主设备号 | 容易冲突,不推荐 |
| 动态分配 | 内核自动分配空闲号 | 推荐方式 |
#include <linux/fs.h>
#include <linux/cdev.h>
#define DEVICE_NAME "mychar"
static dev_t dev_num; // 设备号
static struct cdev my_cdev; // 字符设备对象
static int __init mychar_init(void)
{
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated dev: %d:%d\n",
MAJOR(dev_num), MINOR(dev_num));
// 2. 初始化 cdev 并关联 file_operations
cdev_init(&my_cdev, &mychar_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加字符设备到系统
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
return 0;
}
static void __exit mychar_exit(void)
{
cdev_del(&my_cdev); // 删除字符设备
unregister_chrdev_region(dev_num, 1); // 注销设备号
printk(KERN_INFO "mychar: device unregistered\n");
}
注意事项:设备号是系统全局资源,卸载模块时必须注销,否则下次加载时可能分配不同的设备号,导致用户空间程序无法找到设备。
3.1.3 file_operations 结构体
file_operations 是字符设备驱动的核心接口表,定义了用户空间通过系统调用操作设备时的内核回调函数。
static int mychar_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "mychar: device opened\n");
return 0;
}
static int mychar_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "mychar: device closed\n");
return 0;
}
// 定义 file_operations 结构体
static struct file_operations mychar_fops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
// .read = mychar_read, // 后续章节实现
// .write = mychar_write, // 后续章节实现
// .unlocked_ioctl = mychar_ioctl, // 后续章节实现
};
3.1.4 open 与 release 实现
open 在用户空间调用 open() 系统调用时触发,release 在最后一个文件描述符关闭时触发。这两个回调是驱动与用户空间交互的入口和出口。
static int mychar_open(struct inode *inode, struct file *filp)
{
// 获取次设备号
unsigned int minor = iminor(inode);
// 权限检查示例
if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
printk(KERN_INFO "mychar: opened for write only (minor=%u)\n", minor);
}
// 保存私有数据(后续章节使用)
filp->private_data = NULL;
printk(KERN_INFO "mychar: open minor=%u, pid=%d\n",
minor, current->pid);
return 0;
}
static int mychar_release(struct inode *inode, struct file *filp)
{
unsigned int minor = iminor(inode);
// 清理私有数据
if (filp->private_data) {
kfree(filp->private_data);
filp->private_data = NULL;
}
printk(KERN_INFO "mychar: release minor=%u, pid=%d\n",
minor, current->pid);
return 0;
}
最佳实践:在
open 中分配资源,在 release 中释放资源,确保资源生命周期与文件描述符绑定,避免泄漏。
3.1.5 完整驱动框架
将上述组件整合为完整的字符设备驱动模板:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mychar"
#define MAX_DEVICES 4
static dev_t dev_base;
static struct cdev my_cdevs[MAX_DEVICES];
static int mychar_open(struct inode *inode, struct file *filp)
{
unsigned int minor = iminor(inode);
if (minor >= MAX_DEVICES)
return -ENODEV;
filp->private_data = (void *)(unsigned long)minor;
printk(KERN_INFO "mychar: open minor=%u\n", minor);
return 0;
}
static int mychar_release(struct inode *inode, struct file *filp)
{
unsigned int minor = (unsigned long)filp->private_data;
printk(KERN_INFO "mychar: release minor=%u\n", minor);
return 0;
}
static struct file_operations mychar_fops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
};
static int __init mychar_init(void)
{
int ret, i;
ret = alloc_chrdev_region(&dev_base, 0, MAX_DEVICES, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device numbers\n");
return ret;
}
for (i = 0; i < MAX_DEVICES; i++) {
cdev_init(&my_cdevs[i], &mychar_fops);
my_cdevs[i].owner = THIS_MODULE;
ret = cdev_add(&my_cdevs[i], dev_base + i, 1);
if (ret) {
pr_err("Failed to add cdev for minor %d\n", i);
goto err_cleanup;
}
}
pr_info("mychar: loaded with major=%d, devices=%d\n",
MAJOR(dev_base), MAX_DEVICES);
return 0;
err_cleanup:
while (--i >= 0)
cdev_del(&my_cdevs[i]);
unregister_chrdev_region(dev_base, MAX_DEVICES);
return ret;
}
static void __exit mychar_exit(void)
{
int i;
for (i = 0; i < MAX_DEVICES; i++)
cdev_del(&my_cdevs[i]);
unregister_chrdev_region(dev_base, MAX_DEVICES);
pr_info("mychar: unloaded\n");
}
module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Complete char device driver skeleton");
3.1.6 编译与测试
使用 Makefile 编译驱动模块,并通过用户空间程序验证基本功能:
# Makefile
obj-m += mychar.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
# 编译与测试命令
$ make
$ sudo insmod mychar.ko
$ dmesg | tail -5
[12345.678901] mychar: loaded with major=240, devices=4
# 创建设备节点
$ sudo mknod /dev/mychar0 c 240 0
$ sudo mknod /dev/mychar1 c 240 1
# 测试 open/release
$ cat /dev/mychar0
$ dmesg | tail -2
[12346.123456] mychar: open minor=0
[12346.123789] mychar: release minor=0
# 卸载模块
$ sudo rmmod mychar
$ dmesg | tail -1
[12347.987654] mychar: unloaded
验证要点:
- 使用
lsmod | grep mychar确认模块已加载 - 使用
cat /proc/devices | grep mychar查看设备号 - 使用
dmesg观察驱动打印的日志信息 - 确保卸载后
/proc/devices中不再出现该设备
3.1.7 常见问题与排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| insmod 返回错误码 -1 | 设备号分配失败或 cdev_add 失败 | 检查 dmesg 错误信息,确认主设备号未冲突 |
| open 返回 -ENODEV | 设备节点主/次设备号不匹配 | 使用 ls -l /dev/mychar* 核对设备号 |
| rmmod 提示模块正忙 | 有进程持有设备文件描述符 | 使用 lsof /dev/mychar* 查找占用进程 |
| dmesg 无输出 | 日志级别过滤或 printk 未生效 | 检查 /proc/sys/kernel/printk 级别设置 |
调试技巧:在驱动开发初期,大量使用
printk 输出关键路径信息,配合 dmesg -w 实时查看日志,是最高效的调试手段。