)
目录一、字符设备驱动程序框架二、基本原理1. 设备号的申请与归还设备号的概念设备号的申请2. cdev 结构体的分配与初始化file_operations接口实现cdev内存分配方式cdev 初始化与注册注销设备号三、使用设备文件设备文件的生成方式四、总结一、字符设备驱动程序框架字符设备驱动是一种Linux驱动用于支持以字符为单位进行I/O操作的设备如串口、终端等。字符设备驱动的设计原理主要包括定义一个结构体该结构体内部定义了一些设备的打开、关闭、读、写、控制函数.在结构体外分别实现这些函数,并向内核中注册或删除驱动模块。字符设备驱动框架主要涉及Linux软件系统的层次关系包括应用程序与底层驱动程序之间的交互过程。我们从下面的思维导图来解读内核源码。梳理一下字符设备驱动的流程:1.加载驱动程序2.为设备申请设备号(分为主设备号和次设备号)3.分配cdev空间4.初始化cdev5.设备添加到系统中6.使用file_operations进行相应操作7.卸载驱动程序二、基本原理1. 设备号的申请与归还设备号的概念主设备号major标识设备驱动程序同一驱动的所有设备共享同一主设备号。次设备号minor由驱动内部区分不同设备或实例。dev_t 类型通常是一个 32 位数其中高 12 位为主设备号低 20 位为次设备号。设备号的申请静态申请设备号使用register_chrdev_region()为设备手动申请设备号其原型如下int register_chrdev_region(dev_t from, unsigned count, const char *name);参数说明:from起始设备号dev_t类型包含主次设备号信息。count要申请的连续设备号个数。name设备名称可在/proc/devices中查看。若设备号已被占用则返回错误。动态申请设备号使用alloc_chrdev_region()让内核自动分配未使用的设备号int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);示例代码dev_t devno; //设备号 if (alloc_chrdev_region(devno, 0, 1, my_char_device) 0) { printk(Failed to allocate device number\n); return -1; } int major MAJOR(devno); // 提取主设备号 int minor MINOR(devno); // 提取次设备号分配后记得在卸载时使用unregister_chrdev_region(dev_t from, unsigned count);另一种设备号申请方式内核还提供了register_chrdev()内联函数static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }2. cdev 结构体的分配与初始化字符设备在内核中由struct cdev结构体表示其主要作用是与file_operations绑定使内核能够调用相应的设备操作函数。file_operations接口实现file_operations 结构体定义了字符设备支持的各项操作接口内核在用户进程调用设备文件的 open/read/write 等系统调用时会依据这些接口转发到相应的驱动函数。常用接口包括open设备打开操作release设备关闭操作read从设备中读取数据write向设备中写入数据ioctl / unlocked_ioctl设备控制命令可选llseek设备读写位置移动static int my_open(struct inode *inode, struct file *filp) { printk(KERN_INFO Device opened\n); return 0; } static ssize_t my_read(struct file *filp, char __user *buf, size_t size, loff_t *off) { // 从设备缓冲区中读取数据 //buf用户空间的缓冲区指针应用程序调用 read 时传入的目标地址。 //内核中存储设备数据的缓冲区地址加上文件偏移量 *f_pos 表示从设备数据的某个位置开始读取。 //要拷贝的字节数即用户请求读取的数据量。 if (copy_to_user(buf, device_buffer *f_pos, size)) { return -EFAULT; } *f_pos size; return 0; } static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) { if (copy_from_user(device_buffer *f_pos, buf, count)) { return -EFAULT; } printk(KERN_INFO Received data: %s\n, device_buffer); return 0; } static int my_release(struct inode *inode, struct file *filp) { printk(KERN_INFO Device closed\n); return 0; } static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { // 实现设备控制操作 return 0; } //file_operations结构体 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, // .llseek my_llseek, // 根据需要添加 };cdev内存分配方式静态分配static struct cdev my_cdev;动态分配struct cdev* my_cdev cdev_alloc(); //为空则分配失败则打印错误 if (!my_cdev) { printk(KERN_ERR cdev_alloc failed\n); return -ENOMEM; //-ENOMEM 返回后一次出现的异常代码 }对比分析方式优点缺点静态分配设备号固定便于管理需手动避免冲突灵活性差动态分配自动分配避免冲突每次加载主设备号可能变化cdev 初始化与注册初始化 cdev 时需要调用cdev_init()将 file_operations 绑定进去cdev_init(my_cdev, fops); // 绑定操作集注销设备号在驱动模块卸载的时候需要注销对应的设备号static void __exit my_char_exit(void) { cdev_del(my_cdev); //注销cdev unregister_chrdev_region(devno, 1); //注销设备号 printk(KERN_INFO my_char_device unregistered\n); }三、使用设备文件当编译完驱动代码后将其写入内核中之后会在/dev/路径下生成一个设备文件设备文件的生成方式方式操作驱动代码是否需要干预手动创建mknod /dev/my_device c 250 0需指定主次设备号不需要自动创建推荐驱动调用device_create()通过udev规则自动生成设备文件需要自动创建方式:static int __init my_init(void) { int ret; /* 动态分配设备号 */ ret alloc_chrdev_region(devno, 0, 1, my_device); if (ret 0) { printk(KERN_ERR alloc_chrdev_region failed\n); return ret; } /* 初始化 cdev 并注册 */ cdev_init(my_cdev, my_fops); ret cdev_add(my_cdev, devno, 1); if (ret 0) { unregister_chrdev_region(devno, 1); printk(KERN_ERR cdev_add failed\n); return ret; } /* 创建设备类 */ my_class class_create(my_device_class); if (IS_ERR(my_class)) { cdev_del(my_cdev); unregister_chrdev_region(devno, 1); printk(KERN_ERR class_create failed\n); return PTR_ERR(my_class); } /* 创建设备这会在 sysfs 中创建相应的项 并触发 udev 自动在 /dev 下创建设备文件 /dev/my_device */ my_device device_create(my_class, NULL, devno, NULL, my_device); if (IS_ERR(my_device)) { class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(devno, 1); printk(KERN_ERR device_create failed\n); return PTR_ERR(my_device); } printk(KERN_INFO my_device driver loaded, major %d\n, MAJOR(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); printk(KERN_INFO my_device driver unloaded\n); }通过class_create和device_create()去创建对应的设备文件但是要在驱动卸载时也要记得卸载对应的设备文件。PS:最新内核(6.12.xx)class_create()只需要填写类名即可其头文件包含在linxu/drivce/下交互代码示例:// test_user.c #include fcntl.h #include unistd.h #include stdio.h int main() { int fd open(/dev/my_device, O_RDWR); // 触发驱动中的 my_open() if (fd 0) { perror(Open failed); return -1; } char buf[100]; read(fd, buf, sizeof(buf)); // 触发驱动中的 my_read() printf(Read: %s\n, buf); write(fd, Hello Driver, 12); // 触发驱动中的 my_write() close(fd); // 触发驱动中的 my_release() return 0; }四、总结通过上述步骤的系统化整理字符设备驱动开发流程可归纳为分配设备号 → 绑定操作集 → 注册设备 → 实现功能 → 释放资源关键点优先使用动态分配设备号避免冲突。严格逆序释放资源防止内核状态不一致。用户空间与内核空间数据交互必须通过copy_to_user/copy_from_user。下一步可结合具体硬件如GPIO、I2C设备实现真实数据交互并学习并发控制如自旋锁、信号量。所有代码#includelinux/module.h #includelinux/init.h #includelinux/fs.h #includelinux/cdev.h #includelinux/uaccess.h #includelinux/errno.h #includelinux/reboot.h #includelinux/device.h #define DEMOMEM_SIZE 0X1000 #define MEN_CLEAR 0X1 #define NEWDEMOCHRC_NUM 1 #define DEMO_NAME my_demo_device #define DEMO_CLASS_NAME my_demo_class //字符设备结构体 struct newchrdemo_dev { dev_t dev_id; //设备ID 为32位数据 中高12位为主设备号低20位为次设备号 struct cdev* cdev; //设备核心驱动结构体 struct device* demo_device; //分配的设备文件对象 int major; //主设备号 int minor; //次设备号 unsigned char men[DEMOMEM_SIZE]; //内核数据存储空间 }; static struct class* demo_class; struct newchrdemo_dev* newchrdemo; static int demo_open(struct inode* inode, struct file* filp) { filp-private_data newchrdemo; return 0; } static ssize_t demo_read( struct file* filp, char __user* buf, size_t size, loff_t* offt) { //获取文件地址偏移量 unsigned long p *offt;、 //读取字符数量 unsigned int count size; //当偏移量大于内存时说明已经读完 if (p DEMOMEM_SIZE) return 0; //当读取字符大于可读内存空间时防止越界 if (count DEMOMEM_SIZE - p) count DEMOMEM_SIZE - p; //从内核拷贝数据到用户空间 if (copy_to_user(buf, newchrdemo-men *offt, count)) { return -EFAULT; } printk(KERN_INFO read from kern data %s, buf); *offt count; return count; } static ssize_t demo_wirte( struct file* filp, const char __user* buf, size_t count, loff_t* offt) { unsigned long p *offt; unsigned int size count; if (p DEMOMEM_SIZE) return -ENOSPC; if (count DEMOMEM_SIZE - p) count DEMOMEM_SIZE - p; if (copy_from_user(newchrdemo-men *offt, buf, size)) return -EFAULT; printk(KERN_INFO user input data %s, newchrdemo-men); *offt size; return size; } static int demo_release(struct inode* inode, struct file* filp) { printk(KERN_INFO Device buf closed\n); return 0; } static long demo_ioctl(struct file* filp, unsigned int cmd, unsigned long arg) { return 0; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_wirte }; static int __init demo_char_init(void) { int ret 0; //分配对象内存 newchrdemo kmalloc(sizeof(*newchrdemo), GFP_KERNEL); ret alloc_chrdev_region( newchrdemo-dev_id, 0, NEWDEMOCHRC_NUM, DEMO_NAME); if (ret 0) { printk(KERN_ERR alloc_chrdev_region failed\n); return ret; } //获取主次设备号 newchrdemo-major MAJOR(newchrdemo-dev_id); newchrdemo-minor MINOR(newchrdemo-dev_id); //分配设备结构体内存 newchrdemo-cdev cdev_alloc(); if (!newchrdemo-cdev) { printk(KERN_ERR cdev_alloc failed\n); return -ENOMEM; } //结构体初始化(绑定file_opeations) cdev_init(newchrdemo-cdev, demo_fops); //绑定设备号 ret cdev_add(newchrdemo-cdev, newchrdemo-dev_id, NEWDEMOCHRC_NUM); if (ret 0) { unregister_chrdev_region(newchrdemo-dev_id, NEWDEMOCHRC_NUM); printk(KERN_ERR cdev_add failed\n); return ret; } //创建一个设备类型,会在/sys/class/下生成对应的一个目录(单纯用来分别设备类型) demo_class class_create(DEMO_CLASS_NAME); if (IS_ERR(demo_class)) { printk(KERN_ERR Failed to create class\n); unregister_chrdev_region(newchrdemo-dev_id, NEWDEMOCHRC_NUM); return PTR_ERR(demo_class); } //在/dev下创建设备文件 newchrdemo-demo_device device_create(demo_class, NULL, newchrdemo-dev_id, NULL, DEMO_NAME); return 0; } static void __exit demo_char_exit(void) { //注销设备文件 device_destroy(demo_class, newchrdemo-dev_id); //注销设备类 class_destroy(demo_class); //释放设备号 unregister_chrdev_region(newchrdemo-dev_id, NEWDEMOCHRC_NUM); //内核节点删除该设备结构 cdev_del(newchrdemo-cdev); printk(KERN_INFO Demo drive is exit!!!\n); } module_init(demo_char_init); module_exit(demo_char_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Vermouth);