2026/3/20 9:03:42
网站建设
项目流程
苏州建网站的公司哪家公司好,新手要如何让网站被收录,个人做电商怎么入门,qq官网登录入口网页版从零开始写一个字符设备驱动#xff1a;手把手带你走进内核开发大门你有没有试过在 Linux 系统中读写/dev目录下的某个设备文件#xff1f;比如用echo hello /dev/ttyS0向串口发数据#xff0c;或者通过/dev/input/event0获取键盘输入。这些看似普通的“文…从零开始写一个字符设备驱动手把手带你走进内核开发大门你有没有试过在 Linux 系统中读写/dev目录下的某个设备文件比如用echo hello /dev/ttyS0向串口发数据或者通过/dev/input/event0获取键盘输入。这些看似普通的“文件”其实背后连接的是真实的硬件——而让这一切成为可能的正是设备驱动程序。对于刚接触内核开发的新手来说第一个挑战往往不是复杂的中断或DMA而是如何让我的模块被系统识别为一个可读写的设备本文就以最基础也最重要的字符设备驱动为例带你一步步实现从“空模块”到“可在用户空间操作的设备”的全过程。我们不堆术语、不讲虚概念只聚焦一件事把注册流程讲清楚让你真正动手能跑起来。字符设备到底是什么简单说字符设备就是按字节流方式访问的设备。它不像块设备那样支持随机读写如硬盘而是像管道一样一端写入、另一端顺序读出。常见的字符设备包括- 串口UART- 键盘、鼠标- GPIO 控制接口- 自定义的传感器驱动- 调试用的虚拟设备在 Linux 中每个字符设备都有一个对应的主设备号 次设备号并且会映射到/dev下的一个节点文件比如/dev/mychardev。当用户程序调用open()、read()、write()时内核就会找到这个设备并执行你事先注册好的函数。我们的目标是写一个模块加载后自动创建/dev/mychardev并能实现基本的数据收发。核心结构体cdev到底怎么用要注册一个字符设备绕不开的核心结构就是struct cdev。它是内核用来管理字符设备的“身份证”。#include linux/cdev.h struct cdev { struct kobject kobj; struct module *owner; // 所属模块 const struct file_operations *ops; // 操作函数集合 dev_t dev; // 设备号 unsigned int count; // 设备数量 };你可以把它理解为一个“设备容器”-owner告诉内核这个设备属于哪个模块防止模块被卸载-ops是一组函数指针定义了open、read、write等行为-dev是设备号相当于设备的唯一 ID-count表示连续注册几个设备通常为1整个注册过程可以归纳为四个步骤申请设备号初始化cdev并绑定操作函数将cdev添加到内核创建/dev下的设备节点下面我们逐个拆解。第一步动态分配设备号 —— 别再硬编码了Linux 使用dev_t类型表示设备号它是一个 32 位整数高 12 位是主设备号major低 20 位是次设备号minor。主设备号标识设备类型次设备号区分同类型的多个实例。小知识主设备号 4 是 tty5 是 ttyS串口1 是内存设备/dev/mem传统做法是静态指定主设备号比如register_chrdev_region(MKDEV(200, 0), 1, mydev);但这样很容易冲突——万一别人也在用 200 号呢✅推荐做法动态分配static dev_t dev_num; alloc_chrdev_region(dev_num, 0, 1, mychardev);这行代码的意思是- 让内核自动选一个可用的主设备号- 次设备号从 0 开始- 注册 1 个设备- 名字叫mychardev出现在/proc/devices中成功后dev_num就会被填入实际分配的设备号。你可以用MAJOR(dev_num)和MINOR(dev_num)提取主次号。⚠️ 注意如果失败必须及时释放资源否则会造成设备号泄漏。第二步设置文件操作接口 —— 用户能做什么由你定所有对设备的操作最终都会落到file_operations结构体上。这是你和用户空间交互的“契约”。static const struct file_operations fops { .owner THIS_MODULE, .open my_open, .release my_release, .read my_read, .write my_write, };这几个函数的作用如下函数触发时机典型用途.openopen()系统调用初始化设备状态、计数器加一.releaseclose()系统调用清理资源、引用计数减一.readread()系统调用把数据从内核传给用户.writewrite()系统调用接收用户发来的数据⚠️ 特别注意这些函数运行在内核态但接收的缓冲区指针来自用户空间不能直接访问第三步注册cdev—— 把设备交给内核管理有了设备号和操作函数就可以组装cdev并注册了static struct cdev my_cdev; // 初始化 cdev_init(my_cdev, fops); my_cdev.owner THIS_MODULE; // 添加到内核 cdev_add(my_cdev, dev_num, 1);这里有两个关键点cdev_init()把fops绑定到cdevcdev_add()告诉内核“我现在有一个设备设备号是dev_num长度为 1即只有一个设备”。如果失败返回非0一定要调用unregister_chrdev_region()回收设备号避免下次加载失败。第四步自动生成/dev/mychardev—— 告别手动 mknod很多人卡在这一步明明注册成功了为什么/dev/mychardev没出现因为cdev_add()只完成了内核侧注册并不会自动创建设备文件。你需要借助udev sysfs机制来触发节点生成。方法是使用class_create和device_createstatic struct class *my_class; static struct device *my_device; // 创建设备类会在 /sys/class 下生成目录 my_class class_create(THIS_MODULE, myclass); // 创建设备节点触发 udev 创建 /dev/mychardev my_device device_create(my_class, NULL, dev_num, NULL, mychardev);这两步完成后-/sys/class/myclass/mychardev出现- udev 监听到事件自动创建/dev/mychardev- 权限默认为 664组为 dialout 提示如果不希望暴露太多属性也可以不用 class改用手动mknod但在生产环境中不推荐。完整代码实战一个可运行的模板下面是你可以直接编译测试的完整驱动代码#include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #include linux/device.h #include linux/slab.h #define DEVICE_NAME mychardev #define CLASS_NAME myclass static dev_t dev_num; static struct cdev my_cdev; static struct class *my_class; static struct device *my_device; static char *kernel_buffer; #define BUFFER_SIZE 1024 // open: 设备打开时调用 static int my_open(struct inode *inode, struct file *file) { printk(KERN_INFO Device opened\n); return 0; } // release: 设备关闭时调用 static int my_release(struct inode *inode, struct file *file) { printk(KERN_INFO Device closed\n); return 0; } // read: 向用户返回数据 static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { int ret; if (*offset BUFFER_SIZE) return 0; // EOF if (copy_to_user(buf, kernel_buffer *offset, len)) return -EFAULT; *offset len; return len; } // write: 接收用户数据 static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) { if (*offset len BUFFER_SIZE) return -ENOSPC; if (copy_from_user(kernel_buffer *offset, buf, len)) return -EFAULT; *offset len; kernel_buffer[*offset] \0; // 保证字符串结尾 printk(KERN_INFO Received: %s\n, kernel_buffer); return len; } // 模块初始化 static int __init char_driver_init(void) { int ret; // 1. 分配设备号 ret alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); if (ret) { printk(KERN_ERR Failed to allocate device number\n); return ret; } // 2. 初始化 cdev cdev_init(my_cdev, fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, dev_num, 1); if (ret) { printk(KERN_ERR Failed to add cdev\n); unregister_chrdev_region(dev_num, 1); return ret; } // 3. 创建设备类和设备节点 my_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(my_class)) { cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_class); } my_device device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(my_device)) { class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_device); } // 4. 分配内核缓冲区 kernel_buffer kzalloc(BUFFER_SIZE, GFP_KERNEL); if (!kernel_buffer) { device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); return -ENOMEM; } printk(KERN_INFO Character device %s registered, major%d\n, DEVICE_NAME, MAJOR(dev_num)); return 0; } // 模块退出 static void __exit char_driver_exit(void) { kfree(kernel_buffer); device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO Character device unregistered\n); } // 文件操作结构 static const struct file_operations fops { .owner THIS_MODULE, .open my_open, .release my_release, .read my_read, .write my_write, }; module_init(char_driver_init); module_exit(char_driver_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple character device driver); MODULE_VERSION(1.0);编译与测试让它跑起来新建一个Makefileobj-m mychardev.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean install: sudo insmod mychardev.ko remove: sudo rmmod mychardev然后依次执行make sudo insmod mychardev.ko dmesg | tail你应该看到类似输出[ 1234.567890] Character device mychardev registered, major240再检查ls /dev/mychardev cat /proc/devices | grep mychardev最后试试读写echo Hello Kernel /dev/mychardev cat /dev/mychardev查看内核日志确认是否收到数据dmesg | tail新手常踩的坑与避坑指南❌ 问题1卸载模块时报 “Device or resource busy”原因还有进程正在打开该设备比如 shell 脚本没退出或忘记close()。✅ 解决方案- 确保所有测试程序已结束- 在.open中增加引用计数在.release中判断是否允许卸载- 或重启后再卸载。❌ 问题2write()返回-EFAULT原因你在.write函数里直接用了*buf试图访问用户空间地址。✅ 正确做法必须使用copy_from_user()if (copy_from_user(kernel_buf, user_buf, len)) { return -EFAULT; // 复制失败可能是无效指针 }同理read必须用copy_to_user()。❌ 问题3/dev/mychardev没有自动生成原因缺少class_create或device_create。✅ 解决方案确保两步都完成且没有错误返回。提示可以用sudo mdev -s或重启 udev 强制刷新设备节点。❌ 问题4多次加载模块导致设备号冲突原因上次卸载时没有正确释放资源。✅ 防御性编程建议- 所有资源分配都要有对应的释放路径- 使用 goto 统一错误处理进阶技巧- 加载前先rmmod一次。进阶思考这个驱动还能怎么改进虽然我们现在实现了基本功能但离工业级驱动还有距离。你可以继续优化添加互斥锁防止多进程同时读写造成数据混乱c static DEFINE_MUTEX(device_mutex);支持 ioctl扩展控制命令加入等待队列实现阻塞式读写使用 miscdevice简化注册流程适合单设备场景对接设备树用于真实硬件匹配这些内容我们后续可以单独展开。写在最后你已经迈出了关键一步恭喜你当你成功用echo向自己写的驱动发送第一条消息时你就已经越过了内核开发最大的心理门槛。字符设备注册机制看似繁琐实则逻辑清晰-设备号 → 唯一身份-cdev → 内核登记簿-file_operations → 用户接口-class/device → 自动化节点管理掌握这套流程不仅是写一个 LED 驱动的基础更是深入 platform driver、I2C/SPI 子系统、中断处理等高级主题的起点。下一步不妨尝试- 改造驱动支持多个次设备号- 连接真实的 GPIO 控制 LED- 实现一个简单的环形缓冲区用于异步通信如果你在实现过程中遇到了其他挑战欢迎在评论区分享讨论。驱动开发的路上我们一起前行。