第三章:字符设备驱动

来源:互联网 发布:推荐几个淘宝粘土店铺 编辑:程序博客网 时间:2024/05/17 23:24
编写驱动程序的第一步就是:定义驱动程序为用户程序提供的功能(机制)

1、主设备号和次设备号
      对字符设备的访问是通过文件系统内的设备名称进行的。那些名称被称为特殊文件、设备文件、或者简单称之为文件系统树的节点,他们通常位于/dev目录下。
    一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
    在内核中,dev_t类型用来保存设备号——包括主设备号和次设备号。
    dev_t是一个32位的数,前12位用来表示主设备号,后20位用来表示次设备号。如果要获得dev_t的主设备号和次设备号,应该使用下面的宏:
  1. #define MINORBITS 20
  2. #define MINORMASK ((1U << MINORBITS) - 1)
  3. #define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS)
  4. #define MINOR(dev) ((unsigned int)((dev) & MINORMASK)
MAJOR宏获得主设备号,MINOR宏获得次设备号。
    相反,如果需要将主设备号和次设备号转换成dev_t类型则使用下面的宏:
  1. #define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))
2、分配和释放设备编号
        在建立一个字符设备之前,我们的驱动程序首先要获得一个或多个设备号。完成该工作的函数是:
  1. #include <linux/fs.h>
  2. int register_chrdev_region(dev_t first, unsigned int count, char *name)
其中,first是要分配的设备编号范围的起始值。first的次设备号经常被置为0。
count是所请求的连续设备编号的个数。如果count非常大,则所请求的范围可能与下一个主设备号重叠,但是只要我们所请求的编号范围时可用的,那就没有问题。
name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
    register_chrdev_region的返回值在成功的时候返回0。在错误的情况下,返回一个负的错误码,并且不能使用所请求的编号区域。

 动态分配所需的主设备号:
  1. int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor是要使用的被请求的第一个次设备号,它通常是0。count和name跟上面的是一样的意思。

释放设备号
  1. void unregister_chrdev_region(dev_t first, unsigned int count)
first表示要释放的设备号,count表示从first开始要释放的设备号个数。
通常在模块的清除函数中调用unregister_chrdev_region函数。

分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是在编译时指定主设备号的余地。
  1. if (scull_major)
  2. {
  3. dev = MKDEV(scull_major, scull_minor);
  4. result = register_chrdev_region(dev, scull_nr_devs, "scull");
  5. }
  6. else
  7. {
  8. result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
  9. scull_major = MAJOR(dev);
  10. }
  11. if (result < 0)
  12. {
  13. printk(KERN_WARNING "scull: cannot get major %d\n", scull_major);
  14. return result;
  15. }
3、重要的数据结构
        大部分的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file、inode

文件操作
        file_opreations结构就是用来将驱动程序操作连接到我们保留的设备编号上的。
  1. struct file_operations {
  2. struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES
  3. loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置
  4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据
  5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
  6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作
  7. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作
  8. int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL
  9. unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
  10. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令
  11. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl
  12. long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
  13. int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
  14. int (*open) (struct inode *, struct file *); //打开
  15. int (*flush) (struct file *, fl_owner_t id);
  16. int (*release) (struct inode *, struct file *); //关闭
  17. int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据
  18. int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据
  19. int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化
  20. int (*lock) (struct file *, int, struct file_lock *);
  21. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  22. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  23. int (*check_flags)(int);
  24. int (*flock) (struct file *, int, struct file_lock *);
  25. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
  26. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
  27. int (*setlease)(struct file *, long, struct file_lock **);
  28. };
Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
内核驱动模块不必须实现上面所有函数.
  1. struct file_operations scull_fops = {
  2. .owner = THIS_MODULE,
  3. .llseek = scull_llseek,
  4. .read = scull_read,
  5. .write = scull_write,
  6. .ioctl = scull_ioctl,
  7. .open = scull_open,
  8. .release = scull_release,
  9. };

file结构
    struct  file文件结构代表一个打开的文件描述符,它不是专门给驱动程序使用的,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,知道最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
    内核中,指向struct  file结构的指针通常被称为file或者filp。
注意:这个file结构跟用户空间的FILE没有任何关系。FILE是在C库中定义的,不会出现在内核中;而struct file是一个内核结构,不会出现在用户程序中。
  1. struct file {
  2. /*
  3. * fu_list becomes invalid after file_free is called and queued via
  4. * fu_rcuhead for RCU freeing
  5. */
  6. union {
  7. struct list_head fu_list;
  8. struct rcu_head fu_rcuhead;
  9. } f_u;
  10. struct path f_path;
  11. #define f_dentry f_path.dentry //该成员是对应的 目录结构 。
  12. #define f_vfsmnt f_path.mnt
  13. const struct file_operations *f_op; //该操作 是定义文件关联的操作的。内核在执行open时对这个指针赋值。
  14. atomic_long_t f_count;//文件的引用计数(有多少进程打开该文件)
  15. unsigned int f_flags; //该成员是文件标志。
  16. mode_t f_mode;//读写模式:open的mod_t mode参数
  17. loff_t f_pos;//该文件在当前进程中的文件偏移量
  18. struct fown_struct f_owner;//该结构的作用是通过信号进行I/O时间通知的数据
  19. unsigned int f_uid, f_gid;//文件所有者id,所有者组id
  20. struct file_ra_state f_ra;
  21. u64 f_version;
  22. #ifdef CONFIG_SECURITY
  23. void *f_security;
  24. #endif
  25. /* needed for tty driver, and maybe others */
  26. void *private_data;//该成员是系统调用时保存状态信息非常有用的资源。
  27. #ifdef CONFIG_EPOLL
  28. /* Used by fs/eventpoll.c to link all the hooks to this file */
  29. struct list_head f_ep_links;
  30. spinlock_t f_ep_lock;
  31. #endif /* #ifdef CONFIG_EPOLL */
  32. struct address_space *f_mapping;
  33. #ifdef CONFIG_DEBUG_WRITECOUNT
  34. unsigned long f_mnt_write_state;
  35. #endif
  36. };
inode结构
  1. struct inode {
  2. struct hlist_node i_hash; //哈希表
  3. struct list_head i_list; //索引节点链表
  4. struct list_head i_sb_list;//目录项链表
  5. struct list_head i_dentry;
  6. unsigned long i_ino; //节点号
  7. atomic_t i_count; //引用记数
  8. unsigned int i_nlink; //硬链接数
  9. uid_t i_uid; /*使用者id */
  10. gid_t i_gid; /*使用者id组*/
  11. dev_t i_rdev; //该成员表示设备文件的inode结构,它包含了真正的设备编号。
  12. u64 i_version;
  13. loff_t i_size; /*inode多代表的文件的大小*/
  14. #ifdef __NEED_I_SIZE_ORDERED
  15. seqcount_t i_size_seqcount;
  16. #endif
  17. struct timespec i_atime; /*inode最后一次存取的时间*/
  18. struct timespec i_mtime; /*inode最后一次修改的时间*/
  19. struct timespec i_ctime; /*inode的创建时间*/
  20. unsigned int i_blkbits; /*inode在做I/O时的区块大小*/
  21. blkcnt_t i_blocks; /*inode所石油的block块数*/
  22. unsigned short i_bytes;
  23. umode_t i_mode; /*inode的权限*/
  24. spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
  25. struct mutex i_mutex;
  26. struct rw_semaphore i_alloc_sem;
  27. const struct inode_operations *i_op;
  28. const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
  29. struct super_block *i_sb;
  30. struct file_lock *i_flock;
  31. struct address_space *i_mapping;
  32. struct address_space i_data;
  33. #ifdef CONFIG_QUOTA
  34. struct dquot *i_dquot[MAXQUOTAS];
  35. #endif
  36. struct list_head i_devices; /*若是字符设备,为其对应的cdev结构体指针*/
  37. union {
  38. struct pipe_inode_info *i_pipe;
  39. struct block_device *i_bdev; /*若是块设备,为其对应的cdev结构体指针*/
  40. struct cdev *i_cdev; //该成员表示字符设备的内核的 内部结构。当inode指向一个字符设备文件时,该成员包含了指向struct cdev结构的指针,其中cdev结构是字符设备结构体。
  41. };
  42. int i_cindex;
  43. __u32 i_generation;
  44. #ifdef CONFIG_DNOTIFY
  45. unsigned long i_dnotify_mask; /* Directory notify events */
  46. struct dnotify_struct *i_dnotify; /* for directory notifications */
  47. #endif
  48. #ifdef CONFIG_INOTIFY
  49. struct list_head inotify_watches; /* watches on this inode */
  50. struct mutex inotify_mutex; /* protects the watches list */
  51. #endif
  52. unsigned long i_state;
  53. unsigned long dirtied_when; /* jiffies of first dirtying */
  54. unsigned int i_flags;
  55. atomic_t i_writecount;
  56. #ifdef CONFIG_SECURITY
  57. void *i_security;
  58. #endif
  59. void *i_private; /* fs or device private pointer */
  60. };
内核中用inode结构表示具体的文件,也就是对应与硬盘上面具体的文件。提供了设备文件的信息。inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
对于单个的文件,可能会有多个表示打开的文件描述符file结构,但他们都指向同一个inode结构。
 
  1. unsigned int iminor(struct inode *inode); //用来从inode结构中获取字符设备的主设备号
  2. unsigned int imajor(struct inode *inode); //用来从inode结构中获取字符设备的次设备号

4、字符设备的注册
内核的内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个上述结构。这个结构及其一些辅助函数定义在了<linux/cdev.h>
内核汇总每个字符设备都对应一个cdev结构的变量,下面就是cdev结构的定义:
  1. struct cdev{
  2. struct kobject kobj; //每个cdev都是一个kobject
  3. struct module *owner; //指向实现驱动的模块
  4. const struct file_operations *ops //操作这个字符设备文件的方法
  5. struct list_head list; //与cdev对应的字符设备文件的inode->i_devices的链表头
  6. dev_t dev; //起始设备编号
  7. unsigned int count; //设备范围号大小
  8. };

一个cdev一般有两种初始化方式:静态的和动态的
静态的定义初始化:
  1. struct cdev my_cdev;
  2. cdev_init(&my_cdev, &fops);
  3. my_cdev.owner = THIS_MODULE;
动态的定义初始化:
  1. struct cdev *my_cdev = cdev_alloc();
  2. my_cdev->ops = &fops;
  3. my_cdev->owner = THIS_MODULE;
cdev_init和cdev_alloc的区别
  1. /* cdev_alloc 函数 动态定义初始化cdev */
  2. struct cdev *cdev_alloc(void)
  3. {
  4. struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
  5. if (p) {
  6. INIT_LIST_HEAD(&p->list);
  7. kobject_init(&p->kobj, &ktype_cdev_dynamic);
  8. }
  9. return p;
  10. }
  11. /* cdev_init 函数 静态定义初始化cdev */
  12. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  13. {
  14. memset(cdev, 0, sizeof *cdev);
  15. INIT_LIST_HEAD(&cdev->list);
  16. kobject_init(&cdev->kobj, &ktype_cdev_default);
  17. cdev->ops = fops;
  18. }
初始化cdev后,需要将cdev结构添加到系统中去,这是就使用的是cdev_add
  1. int dev_add(struct cdev *p, dev_t dev, unsigned count)    //dev是该设备对应的第一个设备编号
  2. {
  3. p->dev = dev;
  4. p->count = count;
  5. return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
  6. }
  7. /* 关于 kobj_map() 函数就不展开了,我只是大致讲一下它的原理。内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。*/
当要从一个系统中移除一个字符设备是,使用如下调用
  1. void cdev_del(struct cdev *p)
  2. {
  3. cdev_unmap(p->dev, p->count); /* cdev_unmap()会调用kobj_unmap来释放cdev_map散列表中的对象 */
  4. kobject_put(&p->kobj); /* kobject_put()函数释放cdev占用的内存 */
  5. }
早期的方法
    注册一个字符设备驱动的经典方式是:
  1. int register_chrdev(unsigned int major, const char *name, struct file_operations *fops)
  2. //major 是设备的主设备号,name是驱动程序的名字,fops是默认的file_operation结构。
  3. 相应的移除设备的函数是:
  4. int unregister_chrdev(unsigned int major, const char *name);
5、open和release

open
open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。
open函数一般要完成一下的功能:
  • 检查设备特定的错误(诸如设备未就绪或类似的硬件问题)
  • 如果设备是首次打开,就对其进行初始化
  • 如有必要,更新f_op指针
  • 分配并填写filp->private_data里的数据结构。
open方法的原型如下:
  1. int (*open)(struct inode *inode, struct file *filp);
其中inode字段中的i_cdev字段包含了我们所需要的信息,即我们在前面设置的cdev结构。但是问题是,我们通常不需要cdev结构本身,而是希望得到包含有cdev结构的scull_dev。
这就通过一下的函数就可以实现这个效果:
  1. #include <linux/kernel.h>
  2. container_of(pointer, container_typer, container_field);
  3. /*返回pointer所在的container_typer的地址 */
在找到scull_dev结构后,scull将指针保存到了file结构的private_data字段中,这样方便今后对这指针的访问。一般这个指针就是scull_dev,即相应的设备。
经过简化后的open函数:
  1. int scull_open(struct inode *inode, struct file *filp)
  2. {
  3. struct scull_dev *dev;
  4. dev = container_of(inode->i_cdev, struct scull_dev, cdev);
  5. filp->private = dev;
  6. if ((filp->f_flags & O_ACCMODE) == O_WRONLY){
  7. scull_trim(dev);
  8. }
  9. return 0;
  10. }

release
release方法的作用正好与open相反。release方法都用该完成下面的任务:
  • 释放由open分配的、保存在filp->private_data中的所有内容。
  • 在最后一次关闭操作时关闭设备。
  1. int kgpu_release(struct inode *inode, struct file *filp)
  2. {
  3. return 0;
  4. }
6、read和write
read和write方法完成的功能是相似的,拷贝数据到应用程序空间,或反过来从应用程序空间拷贝数据
  1. ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
  2. ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
  3. /* 参数filp是文件指针,参数count是请求传输的数据长度。
  4. 参数buff是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区。
  5. 参数offp是一个指向long offset type 对象的指针,这个对象指明用户在文件中进行存取的位置
  6. */
需要指出的是,read和write方法中的buff参数是用户空间的指针。因此内核空间无法直接引用其中的内容。
下面的函数可以使内核和用户空间之间的数据交换变得很安全、正确:
  1. unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
  2. unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
这两个函数不限于在内核和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。如果无效就不进行拷贝,如果在拷贝过程中遇到无效地址,则只会复制部分数据。
至于实际的设备方法,read方法的任务是从设备拷贝数据到用户空间(copy_to_user),而write方法则是从用户空间拷贝数据到设备上(copy_from_user)。
在出错时,read和write方法都返回一个负值,大于等于0的返回值告诉调用程序成功传输了多少字节。

7、总结

原创粉丝点击