Linux 驱动框架---cdev字符设备驱动和misc杂项设备驱动
字符设备
Linux中设备常见分类是字符设备,块设备、网络设备,其中字符设备也是Linux驱动中最常用的设备类型。因此开发Linux设备驱动肯定是要先学习一下字符设备的抽象的。在内核中使用struct cdev来描述一个字符设备如下
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
其中kobj是内核对于所有设备的共性抽象,ops是字符设备操作接口,dev为设备号共32位其中高12位为主设备号,低20位为次设备号。设备号相关的提供了如下的工具宏
//从设备号获取主设备号 MAJOR(dev_t dev); //获取次设备号 MINOR(dev_t dev); //由主、次设备号创建设备号 MKDEV(int major,int minor);
最后也是最重要的就是字符设备的操作接口,这是Linux设备可以作为文件直接使用的关键;用户进程调用的一系列系统调度最后都是调用的这个接口中的对应接口完成处理,具体定义如下
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); int (*show_fdinfo)(struct seq_file *m, struct file *f); };
其中驱动开发人员最关键最常用就是如下几个接口 :
//读取数据 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //写数据 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //多路IO 轮叙时会调用 unsigned int (*poll) (struct file *, struct poll_table_struct *); //驱动缓冲区映射到用户空间 常用于显示缓冲区 int (*mmap) (struct file *, struct vm_area_struct *); //open 系统调用 int (*open) (struct inode *, struct file *); //close 系统调用 int (*release) (struct inode *, struct file *);
字符设备驱动的使用也离不开内核提供的接口调用,这里主要记录一下常用接口函数
//初始化一个字符设备对象,并绑定文件操作接口 void cdev_init(struct cdev *, const struct file_operations *); //从内核申请一个字符对象内存块 struct cdev *cdev_alloc(void); //释放上面申请到的内存 void cdev_put(struct cdev *p); //添加字符设备到内核 int cdev_add(struct cdev *, dev_t, unsigned); //删除字符设备从内核 void cdev_del(struct cdev *);
然后是设备号它是Linux 内核给予设备的唯一ID。设备文件通过设备号结合虚拟文件系统找到具体设备进而找到文件操作接口。设备的设备号注册有两个主要接口 register_chrdev_region 和 alloc_chrdev_regin。这两个接口的都可以用来向系统申请注册设备号,但是他二者的机制是不同的。从入口参数上看register_chrdev_region 是在已经定义或已知主设备号的的前提下向系统注册设备号,而第二个接口则是未知设备号的情况下向系统申请的设备号该接口会返回申请到的设备号放在dev中。
int register_chrdev_region(dev_t from, unsigned count, const char *name) { struct char_device_struct *cd; dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); if (next > to) next = to; cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name); if (IS_ERR(cd)) goto fail; } return 0; fail: to = n; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } return PTR_ERR(cd); }
alloc_chrdev_region
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) { struct char_device_struct *cd; cd = __register_chrdev_region(0, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); *dev = MKDEV(cd->major, cd->baseminor); return 0; }
通过对比实现可以发现两个接口最终都是通过调用同样的接口__register_chrdev_region 来完成设备号的注册的,只是第一个接口传入了主设备号他就会有一个风险就是设备号冲突,第二个接口设备号是由系统分配则就没有这个问题。最后就是字符设备的使用,这里参考宋宝华的书中给的使用框架。
//file open operation function static int char_drv_open(struct inode *inode , struct file *filp) { return 0; } //file read operation function static int char_drv_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) { return 0; } //file write operation function static int char_drv_write(struct file *filp,const char __user *buf , size_t cnt , loff_t *offt) { return 0; } //file close operation function static int char_drv_release(struct inode *inode , struct file *filp) { return 0; } //file operation function struct static struct file_openration my_test_fop= { .owner = THIS_MODULE, .open = char_drv_open, .read = char_drv_read, .write = char_drv_write, .release = char_drv_release }; //module init function static int __init char_drv_test_init(void) { int ret; //apply device num ret = register_chrdev(1314,"chartest",&my_test_fop); if(ret <0) { return -1; } return 0; } //module uninstall function static void __exit char_drv_test_exit(void) { //this num can use alloc_chrdev_region to apply an device num unregister_chrdev(1314,"chartest",&my_test_fop); } //module function band module_init(char_drv_test_init); module_exit(char_drv_test_exit); //license and author MODULE_LICENSE("GPL"); MODULE_AUTHOR("Smile");
这种使用方式有个问题就是设备注册成功后不会自动在/dev目录下生成设备文件,需要在执行insmod安装模块后执行mknod手动生成设备文件。所以就有了新的使用框架,通过借助设备类就可以实现设备节点的自动创建。基本框架如下
//file open operation function static int char_drv_open(struct inode *inode , struct file *filp) { return 0; } //file read operation function static int char_drv_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) { return 0; } //file write operation function static int char_drv_write(struct file *filp,const char __user *buf , size_t cnt , loff_t *offt) { return 0; } //file close operation function static int char_drv_release(struct inode *inode , struct file *filp) { return 0; } //file operation function struct static struct file_openration my_test_fop= { .owner = THIS_MODULE, .open = char_drv_open, .read = char_drv_read, .write = char_drv_write, .release = char_drv_release }; struct cdev test_dev; struct class *class; struct devic *device; dev_t devid; /* 设备结构体 */ struct test_dev { dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ }; #define NEWCHRLED_CNT 1 /* 设备号个数 */ #define NEWCHRLED_NAME "newchrdev" /* 名字 */ struct test_dev test_char_dev; //module init function static int __init char_drv_test_init(void) { //same hardware init //apply device num alloc_chrdev_region(&test_char_dev.devid, 0, NEWCHRLED_CNT,NEWCHRLED_NAME); test_char_dev.major = MAJOR(test_char_dev.devid); /* 获取主设备号 */ test_char_dev.minor = MINOR(test_char_dev.devid); /* 获取次设备号 */ //init dev struct cdev_init(&test_char_dev.cdev,&my_test_fop); //add dev to system cdev_add(&test_char_dev.cdev ,test_char_dev.devid ,NEWCHRLED_CNT ); //build class test_char_dev.class = class_create(THIS_MODULE,"test"); if (IS_ERR(newchrled.class)) { return PTR_ERR(newchrled.class); } //build device test_char_dev.device = device_create(test_char_dev.class,NULL ,devid,NULL,"test"); if (IS_ERR(newchrled.device)) { return PTR_ERR(newchrled.device); } return 0; } //module uninstall function static void __exit char_drv_test_exit(void) { /* 注销字符设备 */ cdev_del(&newchrled.cdev);/* 删除 cdev */ unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); device_destroy(newchrled.class, newchrled.devid); class_destroy(newchrled.class); } //module function band module_init(char_drv_test_init); module_exit(char_drv_test_exit); //license and author MODULE_LICENSE("GPL"); MODULE_AUTHOR("Smile");
这样的驱动编译成模块后执行模块安装后/dev下就会自动创建设备文件使用起来更加方便。接下来再来看一个特殊的字符设备
杂项设备
杂项设备是一种特殊的类似字符设备的设备,他的特性是主设备号固定为10不需要像字符设备一样需要单独创建class后注册设备才能自动创建设备节点,杂项设备统一实现了杂项设备class注册完成后就能自动在dev目录下生成对应的设备文件,使用起来比使用字符设备更加简单。杂项设备抽象的结构体如下
struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; const char *nodename; umode_t mode; };
其中主要的几个成员是minor,name、fops,其余的是内核管理相关驱动开发基本很少直接使用,杂项设备的驱动的使用和字符设备基本类似,并且更加简单了一点,先实现file_operations的文件操作接口,然后定义一个杂项设备并将这个ops绑定到这个新定义的设备上。之后调用杂项设备注册接口进行注册。
int misc_register(struct miscdevice * misc) { dev_t dev; int err = 0; INIT_LIST_HEAD(&misc->list); mutex_lock(&misc_mtx); if (misc->minor == MISC_DYNAMIC_MINOR) { int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS); if (i >= DYNAMIC_MINORS) { err = -EBUSY; goto out; } misc->minor = DYNAMIC_MINORS - i - 1; set_bit(i, misc_minors); } else { struct miscdevice *c; list_for_each_entry(c, &misc_list, list) { if (c->minor == misc->minor) { err = -EBUSY; goto out; } } } dev = MKDEV(MISC_MAJOR, misc->minor); misc->this_device = device_create(misc_class, misc->parent, dev, misc, "%s", misc->name); if (IS_ERR(misc->this_device)) { int i = DYNAMIC_MINORS - misc->minor - 1; if (i < DYNAMIC_MINORS && i >= 0) clear_bit(i, misc_minors); err = PTR_ERR(misc->this_device); goto out; } /* * Add it to the front, so that later devices can "override" * earlier defaults */ list_add(&misc->list, &misc_list); out: mutex_unlock(&misc_mtx); return err; }
执行过程很简单判断待注册的新设备是否指定了次设备号,如果是MISC_DYNAMIC_MINOR 则说明是由杂项设备子系统分配次设备号的,次设备号的管理由杂项设备子系统用的位图实现管理通过 find_first_zero_bit 接口获取次设备号;反之如果是指定的次设备号则会判断当前次设备号是否有被使用过,如过被使用过则返回EBUS错误并执行错误处理后返回。反之以上处理过程没有问题的话就继续向下处理,到这里处理过程就像使用字符设备时相同,而所属的类就是杂项设备类即(misc_class),因为通过字符设备创建可以知道在字符设备创建的时候需要创建设备类并将新的字符设备添加到指定设备类,这样字符设备的模块安装才会在/dev目录下自动创建设备文件。回到杂项设备添加的处理最后就是将当前新设备加入到杂项设备子系统的设备list上。底层涉及Linux device相关知识点这一篇是学习这部分时记录的内容可以参考。
最后是杂项设备的注销过程
int misc_deregister(struct miscdevice *misc) { int i = DYNAMIC_MINORS - misc->minor - 1; if (WARN_ON(list_empty(&misc->list))) return -EINVAL; mutex_lock(&misc_mtx); list_del(&misc->list); device_destroy(misc_class, MKDEV(MISC_MAJOR, misc->minor)); if (i < DYNAMIC_MINORS && i >= 0) clear_bit(i, misc_minors); mutex_unlock(&misc_mtx); return 0; }
执行过程更加简单,直接获取次设备号用于后面清除 bitmap管理的次设备号位图,然后就是调用字符设备驱动中相同的设备注销接口device_destroy 将设备从内核中移除。这里没有像前面的字符设备结合设备类的情形去删除设备类原因很简单,因为杂项设备类要驻留在系统中。