我的第一个字符驱动:基于Linux2.4之前版本的古法编程

发布时间:2026/5/17 20:30:16

我的第一个字符驱动:基于Linux2.4之前版本的古法编程 目录一、先明确核心前提Linux2.4及更早的极简设备模型二、Linux2.4内核cdev的前身——char_device_struct1. 2.4内核字符设备核心数据结构关键解读2. 2.4内核唯一注册函数register_chrdev核心作用3. 2.4内核必备操作mknod手动创建设备文件mknod的本质三、2.4内核古法驱动完整运行流程四、内核演进2.6内核重构char_device_struct→cdev重点强调二者的关系1. 2.6内核核心结构体struct cdev对比2.4的char_device_struct核心变化2. 2.6内核驱动注册流程拆分式替代register_chrdev五、新手必避的3大核心误区误区1register_chrdev会自动生成/dev设备文件误区2cdev是char_device_struct的子类/兼容扩展误区3Linux早期三类设备共用一个全局链表六、总结一句话理清cdev的前世今生七、古法字符驱动示例刚接触Linux驱动开发的新手大概率会被cdev、设备号、mknod、自动创建设备节点这些概念绕晕更搞不懂现代驱动和早期古法驱动的区别。其实想要摸透Linux字符设备的底层逻辑必须回到最原始的内核架构抛开设备树、platform总线、udev这些后期封装的复杂机制回归设备管理的本质。这篇文章就从零讲起严格遵循Linux内核真实演进历史拆解早期字符设备核心架构结合源码讲清char_device_struct和cdev的关系帮新手彻底理清古法驱动的工作流程避开所有概念误区。一、先明确核心前提Linux2.4及更早的极简设备模型Linux 2.4内核及之前没有总线架构、没有设备树、没有sysfs更没有复杂的设备模型内核的设备管理逻辑极其朴素直接内核将设备分为三大类每类设备独立维护一套管理结构三套体系完全隔离、互不干扰没有统一的全局总链表字符设备char dev按字节流串行读写如串口、键盘、自定义字符驱动是驱动入门最常用的类型块设备block dev按数据块批量读写如硬盘、闪存网络设备net dev专注网络数据包收发独立于前两类设备这个时期设备号主设备号次设备号已经是核心标识主设备号对应内核中的具体驱动次设备号区分同一驱动下的不同设备是用户态和内核态通信的唯一桥梁。工程师的工作也很纯粹定义设备操作函数绑定设备号将驱动信息注册到内核对应设备的管理结构中再通过mknod手动创建用户态入口整个流程没有任何多余封装。二、Linux2.4内核cdev的前身——char_device_struct这里必须纠正一个极易混淆的点Linux 2.4及更早内核中根本没有struct cdev这个结构体我们现在熟知的cdev在2.4内核里有功能完全一致的前身只是名字不同它就是struct char_device_struct——二者是彻底的替代关系不是兼容、继承、包含关系2.6内核重构时直接废弃了旧结构重新命名为cdev旧结构随之彻底消失。1. 2.4内核字符设备核心数据结构2.4内核用全局指针数组管理字符设备数组下标就是主设备号这是字符设备的核心花名册源码定义如下// Linux 2.4.x 内核源码include/linux/fs.h // 字符设备核心管理结构 → cdev的前身2.4独有 struct char_device_struct { struct char_device_struct *next; // 链表指针处理同一主设备号多次设备号场景 unsigned int major; // 主设备号核心标识 unsigned int baseminor; // 次设备号起始值 int minorct; // 次设备号数量 const char *name; // 设备名称 struct file_operations *fops; // 驱动灵魂设备操作函数集合 }; // 字符设备全局管理数组最大支持255个主设备号0-254 #define MAX_CHRDEV 255 static struct char_device_struct *chrdevs[MAX_CHRDEV];关键解读chrdevs[]是字符设备专属数组和块设备、网络设备的管理结构完全隔离各司其职数组下标主设备号内核通过主设备号直接索引查找效率极高struct file_operations是驱动的核心存放open、read、write、close等用户态调用的底层实现函数这个结构承担了后续cdev的所有核心功能只是命名不同2.6内核重构后直接更名为cdev2. 2.4内核唯一注册函数register_chrdev早期古法驱动只需要调用这一个函数就能完成字符设备的内核注册它做了现代的3个函数的事情---设备号请求、与fops的绑定、注册链入chrdevs链表源码简化版如下// Linux 2.4.x 内核源码fs/char_dev.c int register_chrdev(unsigned int major, const char *name, struct file_operations *fops) { struct char_device_struct *cd; // 1. 分配字符设备管理结构内存 cd kmalloc(sizeof(*cd), GFP_KERNEL); if (!cd) return -ENOMEM; // 2. 填充核心信息绑定主设备号、设备名、操作函数集 cd-major major; cd-name name; cd-fops fops; cd-next NULL; // 3. 将结构挂入chrdevs数组对应主设备号下标位置 if (chrdevs[major] NULL) { chrdevs[major] cd; } else { // 同一主设备号多个设备用链表挂载扩展 cd-next chrdevs[major]; chrdevs[major] cd; } return 0; }核心作用这个函数只做内核态的注册工作将自定义的file_operations和设备号绑定存入内核字符设备数组绝对不会创建/dev目录下的设备文件这是新手最容易踩的误区。3. 2.4内核必备操作mknod手动创建设备文件内核注册完驱动只是在内核里“登记了信息”用户态进程完全无法访问必须通过mknod命令手动在根文件系统的/dev目录下创建设备文件这是用户态访问内核驱动的唯一入口。命令格式# mknod /dev/设备文件名 设备类型(c字符设备/b块设备) 主设备号 次设备号 mknod /dev/my_char_drv c 120 0mknod的本质在/dev下创建一个特殊设备文件inode不占用实际存储空间将主设备号次设备号写入该inode的属性中建立用户态和内核态的关联应用程序通过这个设备文件找到内核中对应的驱动三、2.4内核古法驱动完整运行流程把注册驱动和mknod结合就是早期字符驱动最核心的工作链路逻辑极其清晰驱动编写工程师实现file_operations中的open/read/write等函数内核注册调用register_chrdev将char_device_struct结构加入chrdevs数组用户态入口执行mknod在/dev创建设备文件绑定对应设备号应用调用用户进程执行open(/dev/my_char_drv, O_RDWR)内核解析内核读取设备文件刚刚mknod生成的文件inode中的设备号通过主设备号索引chrdevs数组找到对应的char_device_struct函数绑定获取绑定的file_operations执行对应的open函数为用户进程返回文件描述符fd后续操作用户进程通过fd调用read/write内核直接匹配执行file_operations中的对应函数一句话总结register_chrdev是在内核挂电话号码mknod是在用户态立路牌open是用户按路牌拨号内核负责精准接通。四、内核演进2.6内核重构char_device_struct→cdevLinux2.6内核对字符设备模型进行了彻底重构核心目的是支持动态设备号、多设备扩展、统一设备模型适配更复杂的硬件场景这次重构直接废弃了struct char_device_struct推出了全新的struct cdev结构体。重点强调二者的关系不是兼容、不是继承、不是子类包含而是彻底的替代2.4的char_device_struct被直接删除2.6用cdev完全取代它的角色核心功能不变只是结构优化、命名简化同时管理方式从数组变为链表。1. 2.6内核核心结构体struct cdev// Linux 2.6.x 内核源码include/linux/cdev.h // 2.6内核全新推出取代char_device_struct struct cdev { struct kobject kobj; // 融入内核设备模型支持后期自动化机制 struct module *owner; // 指向驱动模块填THIS_MODULE const struct file_operations *ops; // 核心不变设备操作函数集 struct list_head list; // 链表节点全局cdev链表管理 dev_t dev; // 设备号主次合并封装 unsigned int count; // 次设备号数量 };对比2.4的char_device_struct核心变化命名简化冗长的char_device_struct直接更名为cdev更简洁管理方式升级从全局数组chrdevs改为全局链表管理支持更多设备号、动态扩展融入设备模型新增kobject成员为后续class_create/device_create自动创建设备节点打下基础设备号封装用dev_t类型统一封装主、次设备号扩展性更强旧结构彻底消失2.6内核源码中不再有char_device_struct完全被cdev取代2. 2.6内核驱动注册流程拆分式替代register_chrdev2.6内核不再推荐单一的register_chrdev拆分为多个函数灵活性大幅提升// 1. 申请设备号静态指定/动态分配均可 dev_t devno; alloc_chrdev_region(devno, 0, 1, my_cdev_drv); // 2. 分配并初始化cdev结构绑定file_operations struct cdev *cdev cdev_alloc(); cdev_init(cdev, fops); cdev-owner THIS_MODULE; // 3. 将cdev加入内核全局cdev链表完成注册 cdev_add(cdev, devno, 1);虽然注册流程变了但底层逻辑和2.4内核完全一致依旧是绑定设备号和操作函数只是管理结构从char_device_struct换成了cdev。五、新手必避的3大核心误区误区1register_chrdev会自动生成/dev设备文件正解无论2.4还是2.6内核register_chrdev只负责内核态注册绝不会创建用户态设备文件。2.4必须手动mknod2.6后期是udev/mdev工具自动执行mknod并非驱动本身创建。误区2cdev是char_device_struct的子类/兼容扩展正解二者是替代关系2.6内核直接废弃了char_device_struct重新设计并命名为cdev旧结构彻底从源码中消失没有继承、包含、兼容关系。误区3Linux早期三类设备共用一个全局链表正解2.4及更早内核中字符、块、网络设备三套管理体系完全独立字符用chrdevs数组、块用blkdevs数组、网络用独立链表没有统一的全局总链表。六、总结一句话理清cdev的前世今生Linux 2.4及更早字符设备核心管理结构是struct char_device_struct通过chrdevs数组管理必须手动mknod创建设备文件是古法驱动的核心Linux 2.6及以后内核重构字符设备模型彻底删除char_device_struct推出struct cdev取而代之管理方式升级为链表融入设备模型支持自动化设备节点创建本质不变无论是char_device_struct还是cdev核心作用都是绑定设备号和file_operations作为内核管理字符设备的载体用户态始终通过设备号设备文件访问内核驱动。对于新手来说先吃透2.4内核的古法驱动逻辑看懂char_device_struct到cdev的演进本质再去学习现代驱动的复杂机制就会发现所有后期封装都是为了简化手动操作底层逻辑一脉相承再也不会被繁杂的概念绕晕。后续我会基于2.4内核写一个极简的可运行古法字符驱动手把手实现注册、mknod、应用层调用让新手彻底落地这套核心逻辑。七、古法字符驱动示例字符驱动代码#includelinux/init.h #includelinux/module.h #includelinux/kernel.h #include linux/fs.h //我的第一个字符驱动---Linux内核2.4版本及之前的驱动开发方法 static ssize_t my_chardev_read(struct file *, char __user *, size_t, loff_t *) { printk(read驱动函数调用成功\n); return 0; } static ssize_t my_chardev_write(struct file *, const char __user *, size_t, loff_t *) { printk(write驱动函数调用成功\n); return 0; } static int my_chardev_open(struct inode *, struct file *) { printk(open驱动函数调用成功\n); return 0; } static int my_chardev_release(struct inode *, struct file *) { printk(release驱动函数调用成功\n); return 0; } // //文件操作集合 struct file_operations my_char_fops { .owner THIS_MODULE, .open my_chardev_open, .release my_chardev_release, .read my_chardev_read, .write my_chardev_write }; //模块加载函数 static int __init my_chardev_init(void) { printk(my_chardev模块加载中\n); register_chrdev(200,my_chardev,my_char_fops); return 0; } //模块卸载函数 static void __exit my_chardev_exit(void) { printk(my_chardev模块卸载中\n); unregister_chrdev(200,my_chardev); } module_init(my_chardev_init); module_exit(my_chardev_exit); MODULE_LICENSE(GPL);用户态代码#include stdio.h #include fcntl.h #include unistd.h #include string.h int main(void) { int fd; char buf[100] {0}; const char *test_str Hello, my_chardev!; // 1. 打开设备节点 fd open(/dev/my_chardev, O_RDWR); if (fd 0) { perror(open failed); return -1; } printf(open /dev/my_chardev success\n); // 2. 写数据触发驱动 write ssize_t w_ret write(fd, test_str, strlen(test_str)); if (w_ret 0) { perror(write failed); close(fd); return -1; } printf(write %zd bytes\n, w_ret); // 3. 读数据触发驱动 read ssize_t r_ret read(fd, buf, sizeof(buf)-1); if (r_ret 0) { perror(read failed); close(fd); return -1; } printf(read %zd bytes: %s\n, r_ret, buf); // 4. 关闭设备触发驱动 release close(fd); printf(close /dev/my_chardev success\n); return 0; }Makefile# 内核源码路径 KERNELDIR : /home/hmy/linux-mini/linux-6.8 # 当前目录 CURRENT_PATH : $(shell pwd) # 驱动模块文件名 obj-m : main.o # 交叉编译器 CROSS_COMPILE : arm-linux-gnueabihf- CC : $(CROSS_COMPILE)gcc # 默认编译驱动 用户测试程序 all: module test # 编译内核驱动模块 module: make -C $(KERNELDIR) M$(CURRENT_PATH) modules ARCHarm CROSS_COMPILE$(CROSS_COMPILE) # 编译用户态测试程序【静态编译解决 not found 问题】 test: $(CC) test.c -o test -static # 清理 clean: make -C $(KERNELDIR) M$(CURRENT_PATH) clean ARCHarm rm -f test

相关新闻