[快速上手Linux设备驱动]之我看字符设备驱动这段时间算是把linux下的字符设备给基本吃透了,这边walfred会根据自己的观点,分解拆卸linux下字符设备并将其整理出来。 预备知识 这边提到的linux字符设备驱动是基于linux动态加载模块的思想,所以请务必知道linux模块的应用,可参考内核模块编程入门程序及标准Makefile文件。 1Linux字符设备驱动描述图 说明: 1.1这里我向大多数介绍linux字符设备驱动的书籍一样,将字符设备、linux字符设备驱动、用户程序分别独立开来。 1.2通过上图可以简单的看出linux字符设备驱动这一块主要是有cdev这个玩意主导了一切呀,所以下文会讲到cdev以及cdev关联的file_operations结构体。 2拆分字符设备驱动 2.1两个结构体cdev和file_operations 2.1.1关于cdev结构体 在字符设备驱动中,cdev结构体顾名思义(char device)可以知道其就是来描述一个字符设备的,看下cdev结构体的定义: linux-2.6.32/include/linux/cdev.h struct cdev { struct kobject kobj;// kobj是一个嵌入在该结构中的内核对象。 struct module *owner;// owner指向提供驱动程序的模块 const struct file_operations *ops;// ops是一组文件操作,实现了与硬件通信的操作。 struct list_head list;// list用来实现一个链表,其中包含所有表示该设备的设备特殊文件的inode. dev_t dev;// 指定了设备号 unsigned int count;// count表示与该设备关联的从设备的数目 }; 看到了这个cdev结构体还真不简单,里面包含这么一些东西,关于ops在cdev结构体定义中已经有解释,所以下面将是重点描述下file_operations结构体。 2.1.2关于file_operations结构体 file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。 在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动程序提供的。通常这组设备驱动程序接口是由结构file_operations结构体向系统说明的,它定义在include/linux/fs.h中。 linux-2.6.32/include/linux/fs.h 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); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 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 *, struct dentry *, 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 **); }; 传统上, 一个 file_operation 结构或者其一个指针称为 fops( 或者它的一些变体).。结构中的每个成员必须指向驱动中的函数, 这些函数实现一个特别的操作, 或者对于不支持的操作留置为 NULL. 当指定为 NULL 指针时内核的确切的行为是每个函数不同的。 在你通读 file_operations 方法的列表时, 你会注意到不少参数包含字串 __user. 这种注解是一种文档形式, 注意, 一个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译, __user 没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用。 struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。 在常见的字符设备驱动程序中,我们一般这样做, 它的 file_operations 结构是如下初始化的: struct file_operations scull_fops = { .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl,, .open = scull_open, .release = scull_release, }; 这种做法可以将我们自己定义的接口函数和file_operations结构体中定义的函数指针关联起来,说白了我们就是这样将file_operations结构体初始化的。 2.2 dev_t dev指定设备号 在cdev结构体中,我们对const struct file_operations *ops和dev_t dev关爱有加,其中我们在上面已经详细的说了file_operations,现在就重点说下dev_t dev,因为这玩意一旦指定分配了,在用户空间创建的字符设备必须也要使用相同的设备,然后进行对应起来,用户程序才会打开字符设备。 2.2.1dev_t类型 在内核中,dev_t类型(定义在<linux/types.h>中)用来保存设备编号——包括主设备号和次设备号。在内核2.6.0之后,dev_t是一个32位的数,其中12位表示主设备号,20位表示次设备号。 获得dev_t的主、次设备号: MAJOR(dev_t dev); MINOR(dev_t dev); 生成(转换成)dev_t类型: MKDEV(int major, int minor); 分配和释放设备号 2.2.2建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明: //指定设备编号 int register_chrdev_region(dev_t first, unsigned int count,char*name); //动态生成设备编号 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); //释放设备编号 void unregister_chrdev_region(dev_t first, unsigned int count); 3 Linux字符设备模板 3.1 驱动加载和卸载函数 加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中实现设备号的释放和cdev的注销。 工程师习惯定义一个与设备相关的结构体,通常会涉及私有数据、cdev、信号量等信息(这个比较重要可以将与一个设备相关信息通过”面向对象”的方式包装起来)。 设备结构体 Struct xxx_dev_t { Struct cdev cdev; ….. 私有数据; } 加载函数 static int __init xxx_init(void) { 1. 申请设备号 2.cdev_init(&xxx_dev.dev,&xxx_fops); 3.cdev_add; } 卸载函数 static int __exit xxx_exit(void) { 1. 释放设备号 2. 注销设备; } 3.2 定义字符设备file_operations结构体成员函数 read(); write(); ioctl(); 3.3 file_operations结构体关联 Struct file_operations xxx_ops={ .owner=THIS_MODULE, .read=xxx_read, .write=xxx_write, ……. }; 结语:关于字符设备驱动,walfred就暂时分析到这边了,总体来说字符设备还是蛮简单的,其实只要掌握我画的那图,就基本ok了。可能我在上面分析时系统对cdev的操作没有详细的讲到,但这也无妨,希望读者通过通过我的分析能够理解Linux字符设备驱动程序的基本框架。 |
|