字符设备流式按字节访问无固定块大小随机访问不要求块设备按固定大小块扇区512B访问必须经过内核块缓存支持随机读写。项目字符设备Char Device块设备Block Device访问单位字节流一个一个字节块扇区一般 512 字节访问方式顺序流式读写也可以 lseek 随机读必须支持随机访问内核会预读、缓存内核缓存无强制缓存read/write 直接到驱动强制块缓存page cache读写先操作缓存延迟刷盘设备节点/dev/xxx类型标记c/dev/xxx类型标记b典型硬件LED、按键、串口、ADC、触摸屏、传感器SD 卡、eMMC、硬盘、Flash驱动框架cdev、file_operationsblock_device、gendisk、request 队列open 行为直接调用驱动.open内核会先加载块驱动、处理分区表字符设备核心 4 大要素dev_t dev设备号主 次内核用来定位驱动。主设备号驱动编号次设备号同一驱动下不同设备实例MKDEV(major,minor)合成MAJOR(dev)/MINOR(dev)拆分register_chrdev_region()静态注册指定设备号alloc_chrdev_region()动态分配struct cdev字符设备内核对象代表一个字符设备实例cdev_init()绑定file_operationscdev_add()把 cdev 和设备号注册进内核cdev_del()卸载struct file_operations fops函数指针表应用层系统调用的映射.open.read.write.release.unlocked_ioctl应用open()→内核调用.openclass devicesysfs让 mdev 自动生成/dev/xxx节点可选没有就手动mknodclass_create创建设备类/sys/class/xxxdevice_create在 sysfs 创建设备发送 ueventmdev 自动 mknod手动mknod命令mknod /dev/dev_name c 255 0 命令 节点名字 字符设备 主设备号 次设备号tags 索引文件ctagstags 是源码符号索引文件由ctags生成Vim/Neovim 用来快速跳转到函数、结构体、宏、变量的定义阅读 Linux 内核、STM32、i.MX6ULL 这类大工程源码必备成 tags 文件进入源码根目录执行# -R 递归扫描所有子目录生成tags文件 ctags -R *Vim 核心跳转命令快捷键 / 命令功能Ctrl ]光标放在符号上跳转到定义最常用Ctrl t回退返回上一处跳转位置tag 栈:tag func_name直接搜索符号 func_name跳到定义:ts func_name/:tselect func_name列出该符号所有定义位置同名函数 / 结构体多选:tn多个匹配时下一个 tag:tp多个匹配时上一个 tag:tags查看跳转栈历史shell 直接打开并定位符号vim -t main字符设备驱动程序完整开发步骤标准 2.6 内核 cdev 框架动态申请设备号alloc_chrdev_region定义file_operations实现 open/read/write/release定义struct cdevcdev_init(cdev, fops)绑定操作集cdev_add(cdev, devno, count)把 cdev 注册进内核使用class_createdevice_create自动生成/dev/xxx节点udev/mdev旧方法手动mknod /dev/xxx c 主号 次号module_init 调用初始化函数module_exit 做反向清理对应代码开发模板//字符设备驱动模板 dev_t dev; // 定义设备号变量32位高12位主设备号低20位次设备号 // 文件操作函数集用户态open/read/write/release系统调用对应的驱动回调 struct file_operations fops { .open demo_open, .read demo_read, .write demo_write, .release demo_release, }; struct cdev cdev; //字符设备对象内核用来管理字符设备 //模块加载入口函数insmod时自动执行 demo_init() { MKDEV(); //拼接主、次设备号生成dev_t类型设备号 cdev_init(); //初始化cdev绑定file_operations操作集 cdev_add(); //向内核注册cdev添加字符设备内核可识别 register_chrdev_region(); //静态申请设备号分配主、次设备号 /*动态申请设备号*/ /* alloc_chrdev_region(dev, 0, 1, DEV_NAME); dev存放申请好的设备号 第二参数为申请的次设备号 第三参数为连续申请设备商量 */ class_create(); //创建设备类在/sys/class下生成目录 device_create(); //创建设备udev/mdev自动在/dev生成设备节点 } //模块卸载出口函数rmmod时自动执行 demo_exit() { device_destroy(); //销毁/dev下的设备节点 class_destroy(); //删除设备类 unregister_chrdev_region(dev, 1); //释放之前申请的设备号资源 cdev_del(cdev); //从内核删除cdev字符设备 } module_init(demo_init); //指定模块入口函数 module_exit(demo_exit); //指定模块出口函数实验测试添加一个字符设备名为demo2#include linux/init.h #include linux/printk.h #include linux/fs.h #include linux/export.h #include linux/types.h #include linux/cdev.h #include linux/kdev_t.h #include linux/device.h /*主设备号*/ #define MAJOR_DEMO 255 /*次设备号*/ #define MINOR_DEMO 0 /*设备名称*/ #define DEV_NAME demo2 static int open(struct inode * node,struct file *file) { printk(demo init \n); return 0; } static ssize_t read(struct file *file,char __user *buf,size_t size,loff_t *offset) { printk(demo read \n); return 0; } static ssize_t write(struct file *file,const char __user *buf,size_t size,loff_t *offset) { printk(demo write ****\n); return 0; } static int close(struct inode *node,struct file *file) { printk(demo close ...\n); return 0; } /*设备号类型本质是unsigned int 的32位变量存放主设备号和次设备号*/ dev_t dev; /*字符设备信息的结构体*/ struct cdev cdev; /*打包驱动内的操作函数给内核调用*/ static struct file_operations fops { .owner THIS_MODULE, .open open, .read read, .write write, .release close }; /*设备类型指针用于向内核链表添加设备*/ struct class * cls; static int __init demo_init(void) { /*结构体指针代表了/dev下面的设备文件*/ struct device *ret_dev; int ret 0; /*拼接生成设备号*/ dev MKDEV(MAJOR_DEMO,MINOR_DEMO); // (MAJOR_DEMO 20) | MINOR_DEMO /*初始化字符设备信息结构体绑定fops这个驱动函数指针的结构体*/ cdev_init(cdev,fops); /*将字符设备向内核注册设备号为dev注册设备数量为1*/ ret cdev_add(cdev,dev,1); if(ret 0) goto err_cdev_add; /*注册静态设备号向内核申请注册这一组的主次设备号*/ ret register_chrdev_region(dev,1,DEV_NAME); if(ret 0) goto err_register; /*创建设备类在/sys/class/DEV_NAME生成目录*/ cls class_create(fops.owner,DEV_NAME); /*创建设备节点自动在根目录下dev生成设备节点*/ ret_dev device_create(cls,NULL,dev,NULL,DEV_NAME); if(IS_ERR(ret_dev)) goto err_device_create; printk(demo_init ******************############\n); return 0; err_device_create: device_destroy(cls,dev); err_register: unregister_chrdev_region(dev,1); err_cdev_add: cdev_del(cdev); printk(demo_init failed**%d***************############\n,ret); return ret; } static void __exit demo_exit(void) { device_destroy(cls,dev); class_destroy(cls); unregister_chrdev_region(dev,1); cdev_del(cdev); printk(demo_exit ################**************\n); } module_init(demo_init); module_exit(demo_exit);misc 杂项设备miscdevicemisc 是字符设备的简化封装本质依然是字符设备驱动内核统一固定主设备号10不需要手动register_chrdev_region、cdev_init、cdev_add代码比原生字符设备简短很多LED、按键、简单外设驱动很常用。核心特点主设备号固定 10只需要自己指定次设备号封装了cdev、设备号管理底层还是字符设备帮你省去一大堆注册代码自动支持 udev/mdevdevice_create这一步也被封装进misc_register头文件#include linux/miscdevice.h核心结构体struct miscdevicestruct miscdevice { int minor; //次设备号MISC_DYNAMIC_MINOR 代表让内核自动分配次号 const char *name; //设备名字/dev/xxx 就是这个名字 const struct file_operations *fops; //文件操作集和字符设备完全一样 struct list_head list; struct device *parent; struct device *this_device; //...其他成员 };核心 APImisc_register(misc_dev)注册杂项设备模块加载时调用misc_deregister(misc_dev)卸载杂项设备rmmod 调用不用手动MKDEV、register_chrdev_region、cdev_init、cdev_add、class_create、device_create。全部被 misc 封装。misc 设备 VS 原生字符设备项目原生字符设备 cdevmisc 杂项设备主设备号自定义静态 / 动态分配固定主设备号 10cdev手动 cdev_init、cdev_add内核内部自动封装不用写设备号注册register_chrdev_region / alloc_chrdev_region不需要内核统一管理自动创建设备节点class_create device_createmisc_register 内部自动完成适用场景多个同类设备需要自定义主设备号简单单设备LED、按键、简单传感器misc类杂项设备驱动程序模板//杂项(混杂)设备驱动 主设备号固定为10本质是简化封装后的字符设备驱动 struct file_operations fops { .open demo_open, .read demo_read, .write demo_write, .release demo_release }; //杂项设备结构体一次性存放次设备号、设备名、操作函数集 struct miscdevice misc { .minor MISC_DYNAMIC_MINOR, //次设备号MISC_DYNAMIC_MINOR 表示内核动态分配次设备号 .name misc_demo, //设备名称加载后自动生成 /dev/misc_demo .fops fops //绑定文件操作函数集 }; //模块入口函数insmod加载模块执行 demo_init() { misc_register(misc); //注册杂项设备内部自动完成cdev初始化、 //设备号管理、class、device创建设备节点 } //模块出口函数rmmod卸载模块执行 demo_exit() { misc_deregister(misc); //注销杂项设备自动释放所有内核资源 } module_init(demo_init); //指定模块入口 module_exit(demo_exit); //指定模块出口实验测试添加一个demo2的杂项设备#include linux/init.h // 模块加载/卸载宏__init、__exit、module_init、module_exit #include linux/printk.h // 内核打印函数 printk #include linux/fs.h // 文件系统相关结构体inode、file、file_operations #include linux/export.h // 符号导出相关本例未使用 #include linux/types.h // 内核基础数据类型定义 #include linux/miscdevice.h // 杂项设备(miscdevice)驱动头文件 #define DEV_NAME demo2 // 杂项设备名称会在/dev/下生成 /dev/demo2 /** * brief 设备打开回调函数用户空间open(/dev/demo2)时触发 * param node 索引节点描述设备文件本身信息 * param file 文件结构体代表本次打开的文件句柄 * return 成功返回0 */ static int open(struct inode * node,struct file *file) { printk(demo init \n); return 0; } /** * brief 设备读回调用户空间调用read读取/dev/demo2触发 * param file 文件句柄 * param buf 用户态缓冲区指针__user标记这是用户空间地址不能直接内核解引用 * param size 用户想要读取的字节数 * param offset 文件读写偏移位置 * return 读到的字节数这里直接返回0 */ static ssize_t read(struct file *file,char __user *buf,size_t size,loff_t *offset) { printk(demo read \n); return 0; } /** * brief 设备写回调用户空间write写入/dev/demo2触发 * param buf 用户态写入缓冲区 * param size 用户写入的数据长度 * param offset 文件偏移 * return 写入字节数本例返回0 */ static ssize_t write(struct file *file,const char __user *buf,size_t size,loff_t *offset) { printk(demo write ****\n); return 0; } /** * brief 设备释放回调用户close(fd)时执行对应file_operations.release * param node 索引节点 * param file 文件句柄 * return 成功返回0 */ static int close(struct inode *node,struct file *file) { printk(demo close ...\n); return 0; } // 文件操作函数集把上面的回调函数注册到内核 static struct file_operations fops { .owner THIS_MODULE, // 标记属于当前模块防止模块在使用中被卸载 .open open, // 绑定open回调 .read read, // 绑定read回调 .write write, // 绑定write回调 .release close // 绑定close释放回调 }; // 杂项设备结构体 static struct miscdevice misc { .minor MISC_DYNAMIC_MINOR, // 动态申请次设备号由内核分配不用手动指定 .name DEV_NAME, // 设备名/dev/DEMO_NAME .fops fops // 绑定文件操作方法 }; /** * brief 模块入口函数insmod/rmmod加载模块时执行 * return 成功返回0失败返回负的错误码 */ static int __init demo_init(void) { // 注册杂项设备到内核 int ret misc_register(misc); if(ret 0) goto err_register; // 注册失败跳转到错误处理 printk(demo_init ********************\n); return ret; err_register: // 注册失败反注册这里其实misc_register失败不需要deregister只是示例写法 misc_deregister(misc); printk(demo_init failed *************\n); return ret; } /** * brief 模块出口函数rmmod卸载模块时执行 */ static void __exit demo_exit(void) { misc_deregister(misc); // 注销杂项设备从内核移除 printk(demo_exit ################**************\n); } module_init(demo_init); // 指定模块入口函数 module_exit(demo_exit); // 指定模块出口函数