一、linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:

  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  • 块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

  每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

二、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系。

  如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operations之间的连接,通过cdev_add( )向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。

用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。

三、字符设备驱动模型

1. 驱动初始化

1.1. 分配cdev

在2.6的内核中使用cdev结构体来描述字符设备,在驱动中分配cdev,主要是分配一个cdev结构体与申请设备号,以按键驱动为例:

  1. /*……*/
  2. /* 分配cdev*/
  3. struct cdev btn_cdev;
  4. /*……*/
  5. /* 1.1 申请设备号*/
  6. if(major){
  7. //静态
  8. dev_id = MKDEV(major, );
  9. register_chrdev_region(dev_id, , "button");
  10. } else {
  11. //动态
  12. alloc_chardev_region(&dev_id, , , "button");
  13. major = MAJOR(dev_id);
  14. }
  15. /*……*/

从上面的代码可以看出,申请设备号有动静之分,其实设备号还有主次之分。

在Linux中以主设备号用来标识与设备文件相连的驱动程序。次编号被驱动程序用来辨别操作的是哪个设备。cdev 结构体的 dev_t 成员定义了设备号,为 32 位,其中高 12 位为主设备号,低20 位为次设备号。

设备号的获得与生成:

获得:主设备号:MAJOR(dev_t dev);

次设备号:MINOR(dev_t dev);

生成:MKDEV(int major,int minor);

设备号申请的动静之分:

静态:

  1. int register_chrdev_region(dev_t from, unsigned count, const char *name);
  2. /*功能:申请使用从from开始的count 个设备号(主设备号不变,次设备号增加)*/

静态申请相对较简单,但是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

动态:

  1. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
  2. /*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/

动态申请简单,易于驱动推广,但是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。

1.2. 初始化cdev

  1. void cdev_init(struct cdev *, struct file_operations *);
  2. cdev_init()函数用于初始化 cdev 的成员,并建立 cdev file_operations 之间的连接。

1.3. 注册cdev

  1. int cdev_add(struct cdev *, dev_t, unsigned);
    2      cdev_add()函数向系统添加一个 cdev,完成字符设备的注册。

1.4. 硬件初始化

硬件初始化主要是硬件资源的申请与配置,以TQ210的按键驱动为例:

  1. /* 1.4 硬件初始化*/
  2. //申请GPIO资源
  3. gpio_request(S5PV210_GPH0(), "GPH0_0");
  4. //配置输入
  5. gpio_direction_input(S5PV210_GPH0());

  2.实现设备操作

用户空间的程序以访问文件的形式访问字符设备,通常进行open、read、write、close等系统调用。而这些系统调用的最终落实则是file_operations结构体中成员函数,它们是字符设备驱动与内核的接口。以TQ210的按键驱动为例:

  1. /*设备操作集合*/
  2. static struct file_operations btn_fops = {
  3. .owner = THIS_MODULE,
  4. .open = button_open,
  5. .release = button_close,
  6. .read = button_read
  7. };

上面代码中的button_open、button_close、button_read是要在驱动中自己实现的。file_operations结构体成员函数有很多个,下面就选几个常见的来展示:

2.1. open()函数

原型:

  1. int(*open)(struct inode *, struct file*);
  2. /*打开*/

 2.2. read( )函数

原型:

  1. ssize_t(*read)(struct file *, char __user*, size_t, loff_t*);
  2. /*用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值*/

2.3. write( )函数

原型:

  1. ssize_t(*write)(struct file *, const char__user *, size_t, loff_t*);
  2. /*向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,
  3. 当用户进行write()系统调用时,将得到-EINVAL返回值*/
 

2.4. close( )函数

原型:

  1. int(*release)(struct inode *, struct file*);
  2. /*关闭*/
 

2.5. 补充说明

1. 在Linux字符设备驱动程序设计中,有3种非常重要的数据结构:struct file、struct inode、struct file_operations。

struct file 代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建, 在文件关闭后释放。其成员loff_t f_pos 表示文件读写位置。

struct inode 用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。其成员dev_t i_rdev表示设备号。

struct file_operations 一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作, 对于不支持的操作保留为NULL。

2. 在read( )和write( )中的buff 参数是用户空间指针。因此,它不能被内核代码直接引用,因为用户空间指针在内核空间时可能根本是无效的——没有那个地址的映射。因此,内核提供了专门的函数用于访问用户空间的指针:

  1. unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
  2. unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

    3. 驱动注销

3.1. 删除cdev

在字符设备驱动模块卸载函数中通过cdev_del()函数向系统删除一个cdev,完成字符设备的注销。

  1. /*原型:*/
  2. void cdev_del(struct cdev *);
  3. /*例:*/
  4. cdev_del(&btn_cdev); 

3.2. 释放设备号

在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号。

  1. /*原型:*/
  2. void unregister_chrdev_region(dev_t from, unsigned count);
  3. /*例:*/
  4. unregister_chrdev_region(MKDEV(major, 0), 1);

四、字符设备驱动程序基础:

4.1 cdev结构体

在Linux2.6 内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

  1. struct cdev {
  2.  
  3. struct kobject kobj;
  4.  
  5. struct module *owner; /*通常为THIS_MODULE*/
  6.  
  7. struct file_operations *ops; /*在cdev_init()这个函数里面与cdev结构联系起来*/
  8.  
  9. struct list_head list;
  10.  
  11. dev_t dev; /*设备号*/
  12.  
  13. unsigned int count;
  14.  
  15. };

cdev 结构体的dev_t 成员定义了设备号,为32位,其中12位是主设备号,20位是次设备号,我们只需使用二个简单的宏就可以从dev_t 中获取主设备号和次设备号:

MAJOR(dev_t dev)

MINOR(dev_t dev)

相反地,可以通过主次设备号来生成dev_t:

MKDEV(int major,int minor)

4.2 Linux 2.6内核提供一组函数用于操作cdev 结构体:

  1. void cdev_init(struct cdev*,struct file_operations *);
  2.  
  3. struct cdev *cdev_alloc(void);
  4.  
  5. int cdev_add(struct cdev *,dev_t,unsigned);
  6.  
  7. void cdev_del(struct cdev *);

其中(1)用于初始化cdev结构体,并建立cdev与file_operations 之间的连接。(2)用于动态分配一个cdev结构,(3)向内核注册一个cdev结构,(4)向内核注销一个cdev结构

4.3  Linux 2.6内核分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,首先应向系统申请设备号,有二种方法申请设备号,一种是静态申请设备号:

5:int register_chrdev_region(dev_t from,unsigned count,const char *name)

另一种是动态申请设备号:

6:int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);

其中,静态申请是已知起始设备号的情况,如先使用cat /proc/devices 命令查得哪个设备号未事先使用(不推荐使用静态申请);动态申请是由系统自动分配,只需设置major = 0即可。

相反地,在调用cdev_del()函数从系统中注销字符设备之后,应该向系统申请释放原先申请的设备号,使用:

7:void unregister_chrdev_region(dev_t from,unsigned count);

4.4 cdev结构的file_operations结构体

这个结构体是字符设备当中最重要的结构体之一,file_operations 结构体中的成员函数指针是字符设备驱动程序设计的主体内容,这些函数实际上在应用程序进行Linux 的 open()、read()、write()、close()、seek()、ioctl()等系统调用时最终被调用。

  1. struct file_operations {
  2.  
  3. /*拥有该结构的模块计数,一般为THIS_MODULE*/
  4. struct module *owner;
  5.  
  6. /*用于修改文件当前的读写位置*/
  7. loff_t (*llseek) (struct file *, loff_t, int);
  8.  
  9. /*从设备中同步读取数据*/
  10. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  11.  
  12. /*向设备中写数据*/
  13. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  14.  
  15. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  16. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  17. int (*readdir) (struct file *, void *, filldir_t);
  18.  
  19. /*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
  20. unsigned int (*poll) (struct file *, struct poll_table_struct *);
  21.  
  22. /*执行设备的I/O命令*/
  23. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
  24.  
  25. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  26. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  27.  
  28. /*用于请求将设备内存映射到进程地址空间*/
  29. int (*mmap) (struct file *, struct vm_area_struct *);
  30.  
  31. /*打开设备文件*/
  32. int (*open) (struct inode *, struct file *);
  33. int (*flush) (struct file *, fl_owner_t id);
  34.  
  35. /*关闭设备文件*/
  36. int (*release) (struct inode *, struct file *);
  37.  
  38. int (*fsync) (struct file *, struct dentry *, int datasync);
  39. int (*aio_fsync) (struct kiocb *, int datasync);
  40. int (*fasync) (int, struct file *, int);
  41. int (*lock) (struct file *, int, struct file_lock *);
  42. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  43. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  44. int (*check_flags)(int);
  45. int (*flock) (struct file *, int, struct file_lock *);
  46. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
  47. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
  48. int (*setlease)(struct file *, long, struct file_lock **);
  49. };

4.5 file结构

file  结构代表一个打开的文件,它的特点是一个文件可以对应多个file结构。它由内核再open时创建,并传递给在该文件上操作的所有函数,直到最后close函数,在文件的所有实例都被关闭之后,内核才释放这个数据结构。

在内核源代码中,指向 struct file 的指针通常比称为filp,file结构有以下几个重要的成员:

  1. struct file{
  2.  
  3. mode_t fmode; /*文件模式,如FMODE_READ,FMODE_WRITE*/
  4.  
  5. ......
  6.  
  7. loff_t f_pos; /*loff_t 是一个64位的数,需要时,须强制转换为32位*/
  8.  
  9. unsigned int f_flags; /*文件标志,如:O_NONBLOCK*/
  10.  
  11. struct file_operations *f_op;
  12.  
  13. void *private_data; /*非常重要,用于存放转换后的设备描述结构指针*/
  14.  
  15. .......
  16.  
  17. };

4.6 inode 结构

内核用inode 结构在内部表示文件,它是实实在在的表示物理硬件上的某一个文件,且一个文件仅有一个inode与之对应,同样它有二个比较重要的成员:

  1. struct inode{
  2.  
  3. dev_t i_rdev; /*设备编号*/
  4.  
  5. struct cdev *i_cdev; /*cdev 是表示字符设备的内核的内部结构*/
  6.  
  7. };
  8.  
  9. 可以从inode中获取主次设备号,使用下面二个宏:
  10.  
  11. /*驱动工程师一般不关心这二个宏*/
  12.  
  13. unsigned int imajor(struct inode *inode);
  14.  
  15. unsigned int iminor(struct inode *inode);

4.7字符设备驱动模块加载与卸载函数

在字符设备驱动模块加载函数中应该实现设备号的申请和cdev 结构的注册,而在卸载函数中应该实现设备号的释放与cdev结构的注销。

我们一般习惯将cdev内嵌到另外一个设备相关的结构体里面,该设备包含所涉及的cdev、私有数据及信号量等等信息。常见的设备结构体、模块加载函数、模块卸载函数形式如下:

  1. /*设备结构体*/
  2.  
  3. struct xxx_dev{
  4.  
  5. struct cdev cdev;
  6.  
  7. char *data;
  8.  
  9. struct semaphore sem;
  10.  
  11. ......
  12.  
  13. };
  14.  
  15. /*模块加载函数*/
  16.  
  17. static int __init xxx_init(void)
  18.  
  19. {
  20.  
  21. .......
  22.  
  23. 初始化cdev结构;
  24.  
  25. 申请设备号;
  26.  
  27. 注册设备号;
  28.  
  29. 申请分配设备结构体的内存; /*非必须*/
  30.  
  31. }
  32.  
  33. /*模块卸载函数*/
  34.  
  35. static void __exit xxx_exit(void)
  36.  
  37. {
  38.  
  39. .......
  40.  
  41. 释放原先申请的设备号;
  42.  
  43. 释放原先申请的内存;
  44.  
  45. 注销cdev设备;
  46.  
  47. }

4.8字符设备驱动的 file_operations 结构体重成员函数

  1. /*读设备*/
  2.  
  3. ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
  4.  
  5. {
  6.  
  7. ......
  8.  
  9. 使用filp->private_data获取设备结构体指针;
  10.  
  11. 分析和获取有效的长度;
  12.  
  13. /*内核空间到用户空间的数据传递*/
  14.  
  15. copy_to_user(void __user *to, const void *from, unsigned long count);
  16.  
  17. ......
  18.  
  19. }
  20.  
  21. /*写设备*/
  22.  
  23. ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
  24.  
  25. {
  26.  
  27. ......
  28.  
  29. 使用filp->private_data获取设备结构体指针;
  30.  
  31. 分析和获取有效的长度;
  32.  
  33. /*用户空间到内核空间的数据传递*/
  34.  
  35. copy_from_user(void *to, const void __user *from, unsigned long count);
  36.  
  37. ......
  38.  
  39. }
  40.  
  41. /*ioctl函数*/
  42.  
  43. static int xxx_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
  44.  
  45. {
  46.  
  47. ......
  48.  
  49. switch(cmd){
  50.  
  51. case xxx_CMD1:
  52.  
  53. ......
  54.  
  55. break;
  56.  
  57. case xxx_CMD2:
  58.  
  59. .......
  60.  
  61. break;
  62.  
  63. default:
  64.  
  65. return -ENOTTY; /*不能支持的命令*/
  66.  
  67. }
  68.  
  69. return ;
  70.  
  71. }

4.9、字符设备驱动文件操作结构体模板

  1. struct file_operations xxx_fops = {
  2.  
  3. .owner = THIS_MODULE,
  4.  
  5. .open = xxx_open,
  6.  
  7. .read = xxx_read,
  8.  
  9. .write = xxx_write,
  10.  
  11. .close = xxx_release,
  12.  
  13. .ioctl = xxx_ioctl,
  14.  
  15. .lseek = xxx_llseek,
  16.  
  17. };
  18.  
  19. 上面的写法需要注意二点,一:结构体成员之间是以逗号分开的而不是分号,结构体字段结束时最后应加上分号。

五、字符设备驱动小结:

  字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。

  版权所有,转载请注明转载地址:http://www.cnblogs.com/lihuidashen/p/4426129.html

蜕变成蝶~Linux设备驱动之字符设备驱动的更多相关文章

  1. 蜕变成蝶~Linux设备驱动之中断与定时器

    “我叮咛你的 你说 不会遗忘 你告诉我的 我也全部珍藏 对于我们来说 记忆是飘不落的日子 永远不会发黄 相聚的时候 总是很短 期待的时候 总是很长 岁月的溪水边 捡拾起多少闪亮的诗行 如果你要想念我  ...

  2. 蜕变成蝶~Linux设备驱动之异步通知和异步I/O

    在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问.因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代.异步通知类 ...

  3. 蜕变成蝶~Linux设备驱动之按键设备驱动

    在上述的驱动系列博客中,我们已经了解了关于阻塞和非阻塞.异步通知.轮询.内存和I/O口访问.并发控制等知识,按键设备驱动相对来说是比较简单的,本章内容可以加深我们对字符设备驱动架构.阻塞与非阻塞.中断 ...

  4. 蜕变成蝶~Linux设备驱动之watchdog设备驱动

    看门狗(watchdog )分硬件看门狗和软件看门狗.硬件看门狗是利用一个定时器 电路,其定时输出连接到电路的复位端,程序在一定时间范围内对定时器清零 (俗称 “喂狗”),如果程序出现故障,不在定时周 ...

  5. 蜕变成蝶~Linux设备驱动中的阻塞和非阻塞I/O

    今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合 ...

  6. 蜕变成蝶~Linux设备驱动之DMA

    如果不曾相逢 也许 心绪永远不会沉重 如果真的失之交臂 恐怕一生也不得轻松 一个眼神 便足以让心海 掠过飓风 在贫瘠的土地上 更深地懂得风景 一次远行 便足以憔悴了一颗 羸弱的心 每望一眼秋水微澜 便 ...

  7. 蜕变成蝶~Linux设备驱动之CPU与内存和I/O

    那是世上最远的距离 思念让我无法去呼吸 你的一动和一举 占据我心里 陪我每个孤独无尽的夜里 用我心中盛放的画笔 描绘你微笑时的绚丽 爱让人痛彻心底 我却不怀疑 你的存在是我生命的奇迹 感受你的每一次的 ...

  8. 蜕变成蝶~Linux设备驱动中的并发控制

    并发和竞争发生在两类体系中: 对称多处理器(SMP)的多个CPU 内核可抢占的单CPU系统 访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护.在 ...

  9. Linux驱动设计——字符设备驱动(一)

    Linux字符设别驱动结构 cdev结构体 struct cdev { struct kobject kobj; struct module *owner; const struct file_ope ...

随机推荐

  1. [P1020]导弹拦截 (贪心/DP/二分/单调队列)

    一道很经典的题 这道题就是要求一个最长单调不升子序列和一个最长单调上升子序列. 先打了一个n2复杂度的 用DP #include<bits/stdc++.h> using namespac ...

  2. 和textrank4ZH代码一模一样的算法详细解读

    前不久做了有关自动文摘的学习,采用方法是TextRank算法,整理和大家分享. 一. 关于自动文摘 利用计算机将大量的文本进行处理,产生简洁.精炼内容的过程就是文本摘要,人们可通过阅读摘要来把握文本主 ...

  3. svn更换ip地址,重新地位

    问题描述 在解决问题之前,先描述一下问题发生的场景. 小组合作开发,SVN服务器采用的是VisualSVN Server,客户端是TortoiseSVN,在VS上安装了VisualSVN插件.开发大约 ...

  4. python之面向对象的程序设计

    一.什么是面向对象的程序设计 面向过程的程序设计的核心是过程,过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理什么东西. 优点是:极大的降低了程序的复杂度 缺点是: ...

  5. Asp.Net HttpPostedFile和base64两种上传图片(文件)方式

    之前上传图片基本都是用的HttpPostedFile方式获取图片,这次因为需求关系,要对准备上传的图片进行删除,最后提交的时候才去保存图片到服务器, 找了下资料,html5有个新的东西出来,js 里面 ...

  6. Docker 常用命令(一)

    9. docker 删除镜像:  docker rmi    imageID    删除容器:    docker rm containName 8. docker repo 上传: 我们看到这里有个 ...

  7. GMA Round 1 简单的线性规划

    传送门 简单的线性规划 已知D(x,y)满足$\left\{\begin{matrix}x>-3\\ y>1\\ x+y<12\end{matrix}\right.$ 求$\frac ...

  8. selenium之 文件上传所有方法整理总结【转】

    本文转自:https://blog.csdn.net/huilan_same/article/details/52439546 文件上传是所有UI自动化测试都要面对的一个头疼问题,今天博主在这里给大家 ...

  9. angular 2 - 004 routing 路由

    https://angular.io/tutorial/toh-pt5 定义一个模块用来定义路由 src/app/app-routing.module.ts import { NgModule } f ...

  10. 中期linux课程考试题

    [口头表达] 1)请描述你了解的磁盘分区的相关知识2)什么是rsync,你有什么生产环境的应用?3)在生产环境中,公司的IDC机房即将超过254台机器,请问你有什么解决方案来规划扩展IDC机房的内网网 ...