Chinaunix首页 | 论坛 | 博客
  • 博客访问: 805307
  • 博文数量: 281
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2770
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-02 19:45
个人简介

邮箱:zhuimengcanyang@163.com 痴爱嵌入式技术的蜗牛

文章分类
文章存档

2020年(1)

2018年(1)

2017年(56)

2016年(72)

2015年(151)

分类: LINUX

2017-03-01 15:48:22

2.3 Linux 设备和驱动
    2.3.1 驱动在 Linux 中的地位
    驱动是 Linux 系统中设备和用户之间的桥梁, Linux 系统中,访问设备必须通过设备驱
    动进行操作,用户程序是不能直接操作设备的。 Linux 系统中硬件、驱动和用户程序的关系
    如图 2.2 所示。
    
        图2.2 Linux 中设备、驱动和应用程序关系图
    
    驱动程序运行与内核空间,用户程序只能通过内核提供的系统调用,由经 VFS 以及驱
    动程序才能访问和操作硬件,硬件设备传递的数据也必须经过驱动、 VFS 和系统调用才能
    被用户程序接收。所以说,设备驱动是应用程序访问系统设备以及进行数据传递的桥梁和通
    道。
    
    2.3.2 驱动的基本要素
        Linux 设备驱动是具有入口和出口的一组方法的集合,各方法之间相互独立。 驱动内部
        逻辑结构如图 2.3 所示。
                        图2.3  Linux 驱动程序逻辑结构


        Linux 设备在内核中是用设备号进行区分的,而决定这些设备号的正是设备驱动程序。另外,在用户空间如何管理
        这些设备,这也是与驱动程序息息相关的。一个完整的设备驱动必须具备以下基本要素:
        
        1) 驱动的入口和出口。
            驱动入口和出口部分的代码,并不与应用程序直接交互,仅仅只与内核模块管理子系统有交互。
            在加载内核的时候执行入口代码,卸载的时候执行出口代码。这部分代码与内核版本关系较大,
            严重依赖于驱动子系统的架构和实现。
        
        2) 操作设备的各种方法。
            驱动程序实现了各种用于系统服务的各种方法,但是这些方法并不能主动执行,发挥相应的功能,
            只能被动的等待应用程序的系统调用,只有经过相应的系统调用,各方法才能发挥相应的功能,如应用程序执行
            read()系统调用,内核才能执行驱动 xxx_read()方法的代码。这部分代码主要与硬件和所需要实现的操作相关。
            
        3) 提供设备管理方法支持。
            包括设备号的分配和设备的注册等。这部分代码与内核版本以及最终所采用的设备管理方法
            相关系,如采用 udev,则驱动必须提供相应的支持代码。
                                
    2.3.3 驱动和应用程序的差别
        驱动程序与普通应用程序有很大不同,主要表现在以下 3 个方面:
        (1)在程序组成和逻辑方面,普通应用程序一般都是由始至终完成某个任务,而驱动程
        序内部各方法之间相互独立,没有逻辑联系。
        
        (2)在系统资源访问方面, 内核模块运行在内核态,可以操作系统的任何资源,包括硬
        件,但是应用程序却不能直接访问系统硬件,只有借助驱动程序才能访问硬件。        
        
        (3)在出错危害性方面,应用程序出错或者崩溃一般不会引起内核崩溃,可以通过杀死
        程序进程终止,但是内核模块出错,有可能导致内核崩溃,一旦内核崩溃,只能复位系统。
                
    2.3.4 驱动的入口和出口
        驱动的入口与模块的初始化类似,基本功能是向系统注册驱动本身,同时还需完成驱动
        所需资源的申请如设备号的获取、中断的申请以及设备的注册等工作,在一些驱动中还需要
        进行相关的硬件初始化。
        
        驱动的出口则与驱动的入口相反,从系统中注销驱动本身,同时需按照与入口相反的顺
        序对所占用的资源进行释放。
        
        驱动的入口和出口代码,与 Linux 内核版本关系很大,更确切的说是与内核驱动管理子
        系统关系很大。由于 Linux 内核驱动管理系统的不断升级发展,驱动管理机制发生了变化,
        某些数据结构发生了变化,提供的接口函数也有不少变化,这些都直接影响到驱动的注册和
        注销。
        
    2.3.5 支持 udev 设备管理方法
        Linux 2.6 引入了动态设备管理, 用 udev 作为设备管理器, 相比之前的静态设备管理,在使用上更加方便灵活。
        udev 根据 sysfs 系统提供的设备信息实现对/dev 目录下设备节点的动态管理,包括设备节点的创建、删除等。
        后来出现了两个变种: mdev 和 eudev。mdev 是 BusyBox 自带的动态设备管理器,是 udev
        的简化版; eudev 则是 Gentoo 开发的 udev 分支。 无论是 udev,还是后来的 mdev 和 eudev,
        它们都是用户空间的设备管理器。只要系统采用动态设备管理,无论采用哪个管理器,对驱
        动编写的要求都是相同的。通常都以 udev 来指代动态设备管理器。
        
        1. udev 和驱动

            手动创建设备节点:
 
            --------------------          
            如果设备驱动不支持自动创建设备节点,则必须由驱动使用者来完成。驱动使用者必须

            根据驱动规定的主次设备号和设备名称来手动创建设备节点。例如一个设备驱动中设定设备名为
            led,主设备号 231,次设备号 0,则创建设备节点的命令为:
                # mknod /dev/led c 231 0
            已经分配的设备号会出现在/proc/devices 文件中。无论驱动采用静态设备节点还是动态
            设备节点,插入驱动后,都可根据/proc/devices 文件中的信息来创建设备节点。
            
            自动创建设备节点,借助sysfs文件系统:
            ------------------------------------

            在 2.6 内核,引入了新的设备管理机制 sysfs。 sysfs 是 2.6 内核引入的用于管理设备的一
            种虚拟文件系统,挂载在/sys 目录下。 sysfs 将实际连接到系统上的设备和总线组织成一个文
            件分级结构,每个设备在 sysfs 目录中都有唯一对应的目录,可被用户访问,用户空间程序
            可以利用这些信息实现与内核的交互。
            
            若要编写一个能用 udev 管理的设备驱动,需要在驱动代码中调用 class_create()为设备
            创建一个 class 类,再调用 device_create()为每个设备创建对应的设备。
            class_create()函数用于在 sysfs 的 class 目录下创建一个类,函数原型如程序清单 2.12 所示。
                程序清单 2.12 class_create 函数定义
                    #define class_create(owner, name)                 \
                    (                                                 \
                        static struct lock_class_key __key;             \
                        __class_create(owner, name, &__key);         \
                    )
                    extern struct class * __must_check __class_create ( struct module *owner,
                                                                        const char *name,
                                                                        struct lock_class_key *key);
                                                                
            __must_check 宏表示调用者必须检查函数的返回值, 否则会产生警告。
            与 class_create()对应的销毁函数是 class_destroy(),用于销毁在 class 目录下创建的类,
            函数原型如下:
                void class_destroy(struct class *cls);
            device_create()用于在 sysfs 系统中创建设备节点相关的文件 dev 等文件, 函数原型如程序清单 2.13 所示。
            
                程序清单 2.13 device_create 函数定义
                    extern struct device *device_create (struct class *cls, struct device *parent,
                                                            dev_t devt, void *drvdata,
                                                            const char *fmt, ...);
            函数详细说明请看第 2.10.1 小节的描述。与 device_create()对应的销毁函数是
            device_destroy(),用于销毁在 sysfs 中创建的设备节点相关文件,函数原型如下:
                void device_destroy(struct class *cls, dev_t devt);        
                
            下面这个示例将在/sys/class/目录下创建 char_cdev_class 目录, 并在 sysfs 中创建
            char_cdev%d 文件:
            class_create(THIS_MODULE, "char_cdev_class");
            device_create(char_cdev_class, NULL, devno, NULL, "char_cdev" "%d", MINOR(devno));
        
        2. 支持 udev 的驱动范例
            这一节将用前面提到的知识,编写一个能自动创建设备节点的驱动程序。这个程序依然
            只有入口和出口,但是与前一个程序相比,有几点不同:
            ( 1) 直接使用 cdev 来操作;
            ( 2) 支持自动创建设备节点;
            ( 3) 支持传入参数。

点击(此处)折叠或打开

  1. #include <linux/init.h>
  2. #include <linux/module.h>
  3. #include <linux/fs.h>
  4. #include <linux/cdev.h>
  5. #include <linux/device.h>

  6. static int major = 232; /* 静态设备号方式的默认值 */
  7. static int minor = 0; /* 静态设备号方式的默认值 */
  8. module_param(major, int, S_IRUGO);
  9. module_param(minor, int, S_IRUGO);

  10. struct cdev *char_null_udev; /* cdev 数据结构 */
  11. static dev_t devno; /* 设备编号 */
  12. static struct class *char_null_udev_class;

  13. #define DEVICE_NAME "char_null_udev"

  14. static int __init char_null_udev_init(void)
  15. {
  16.     int ret;

  17.     if (major > 0) { /* 静态设备号 */
  18.         devno = MKDEV(major, minor);
  19.         ret = register_chrdev_region(devno, 1, "char_null_udev");
  20.     } else { /* 动态设备号 */
  21.         ret = alloc_chrdev_region(&devno, minor, 1, "char_null_udev"); /* 从系统获取主设备号 */
  22.         major = MAJOR(devno);
  23.     }
  24.     if (ret < 0) {
  25.         printk(KERN_ERR "cannot get major %d \n", major);
  26.         return -1;
  27.     }

  28.     char_null_udev = cdev_alloc(); /* 分配 char_null_udev 结构 */
  29.     if (char_null_udev != NULL) {
  30.         cdev_init(char_null_udev, &major); /* 初始化 char_null_udev 结构 */
  31.         char_null_udev->owner = THIS_MODULE;
  32.         if (cdev_add(char_null_udev, devno, 1) != 0) { /* 增加 char_null_udev 到系统中 */
  33.         printk(KERN_ERR "add cdev error!\n");
  34.         goto error;
  35.         }
  36.     } else {
  37.         printk(KERN_ERR "cdev_alloc error!\n");
  38.         return -1;
  39.     }

  40.     ///sys/class/下创建 char_null_udev_class 目录
  41.     char_null_udev_class = class_create(THIS_MODULE, "char_null_udev_class");
  42.     if (IS_ERR(char_null_udev_class)) {
  43.         printk(KERN_INFO "create class error\n");
  44.         return -1;
  45.     }
  46.     /* 将创建/dev/char_null_udev0 文件 */
  47.     //device_create(char_null_udev_class, NULL, devno, NULL, "char_null_udev" "%d", MINOR(devno));
  48.     
  49.     /* 将创建/dev/char_null_udev 文件 */
  50.     device_create(char_null_udev_class, NULL, devno, NULL, "char_null_udev");

  51.     return 0;

  52. error:
  53.     unregister_chrdev_region(devno, 1); /* 释放已经获得的设备号 */
  54.     return ret;
  55. }

  56. static void __exit char_null_udev_exit(void)
  57. {
  58.     device_destroy(char_null_udev_class, devno);
  59.     class_destroy(char_null_udev_class);
  60.     cdev_del(char_null_udev); /* 移除字符设备 */
  61.     unregister_chrdev_region(devno, 1); /* 释放设备号 */
  62. }
  63. module_init(char_null_udev_init);
  64. module_exit(char_null_udev_exit);

  65. MODULE_LICENSE("GPL");
  66. MODULE_AUTHOR("......");

            编译代码后得到 char_null_udev.ko 模块,插入系统,将可以在/sys/class 目录下看到
            char_null _udev 目录以及其它信息,先看 dev 文件:
            # insmod char_null_udev.ko
            #cat /sys/class/char_null_udev_class/char_null_udev/dev
                232:0
            udev 将根据 dev 文件来创建设备节点。
            可以看到在 sysfs 中创建的设备节点的主次设备号分别是 232 和 0。再看 uevent 文件:
            #cat /sys/class/char_null_udev_class/char_null_udev/uevent
                MAJOR=232
                MINOR=0
                DEVNAME=char_null_udev
            除了看到主次设备号之外,还可以得知设备名称。
            udev 根据 sysfs 目录中的内容,在/dev 目录下创建相应的设备节点,查看:
            #ls -l /dev/char_null_udev
            crw------- 1 root root 232, 0 2011-01-19 11:21 /dev/char_null_udev
            
    2.3.6 设备驱动的操作方法
        驱动是具有入口和出口的一组方法的集合,这一组方法才是驱动的核心内容。这是一组
        什么样的方法?如何将这一组方法与注册的字符设备相关联起来,或者说系统如何知道用哪
        一组方法操作哪个设备?解开这些谜团,得从定义在文件中的字符驱动的核心数
        据结构 file_operations 开始。
        
        1. fops 核心数据结构
            file_operations 结构是一系列指针的集合, 用来存储对设备提供各种操作的函数的指针,
            这些操作函数通常被称之为方法。 file_operations 数据的定义如程序清单 2.15 所示。
            
            程序清单 2.15 file_operations 数据结构定义
            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);
                ....
            };
            
            file_oprations 结构中定义了非常多的成员, 但是对于大多数字符驱动而言, 只需要实现
            其中很少的几个方法即可。下面将忽略不常用的成员,对常用成员进行介绍:
            (1)owner 属主
                struct module *owner
                    owner 是一个指向拥有这个结构的模块的指针, 这个成员可阻止该模块还在被使用时被
                    卸载,通常初始化为 THIS_MODULE
                    
            (2)read 方法
                ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
                    read 方法用来从设备中获取数据。非负返回值表示成功读取的字节数(返回值是一个
                    “ signed size”类型,常常是目标平台本地的整数类型)。
            
            (3)write 方法
                ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
                    发送数据给设备。非负返回值表示成功写入的字节数。
            
            (4)open 方法
                int (*open) (struct inode *, struct file *);
                    对应于设备的打开操作。如果驱动不实现打开操作,即将 open 设置为 NULL,则设备
                    打开一直成功。
                    
            (5)release 方法
                int (*release) (struct inode *, struct file *);
                    对应于设备的关闭操作。
            (6)ioctl
                int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
                    用 read 和 write 方法不方便实现,或者实现起来很复杂的操作,一般都可以放在 ioctl
                    中来进行,主要是做一些非标准操作,增加系统调用的硬件操作能力,如格式化软盘的一个
                    磁道,这不是读也不是写操作,则可用 ioctl 方法实现。
                    
                    注意, 2.6.36 版本及之后的内核,去掉了 ioctl 方法,取而代之的是 unlocked_ioctl 和
                    campat_ioctl
                    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
                    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
                    两个函数不仅函数名称发生了变化,函数参数也有不同,与 ioctl 相比,没有了 inode 参
                    数。但是对用户空间应用程序而言,使用上是没有区别的。通常使用 unlocked_ioctl 来实现
                    驱动的 ioctl 操作。

                    为了保证驱动代码在不同内核版本的兼容性,可在驱动代码中进行版本兼容处理,用
                    LINUX_VERSION_CODE 来进行版本判断和处理。例如:
                        #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,36)
                        .unlocked_ioctl = char_cdev_ioctl
                        #else
                        .ioctl = char_cdev_ioctl
                        #endif
            下文继续用 ioctl 来描述,在实际应用中根据情况处理。
            
        2. 为驱动定义 fops
            前面编写的两个驱动实例都仅仅实现了驱动的入口和出口,设备的注册注销,并没有为
            驱动编写任何实际的操作方法。这样的驱动加载到内核中是不能为内核做任何事情的。
            若要编写一个具有实际操作方法的驱动,首先得为驱动定义一个 file_operations 结构,
            通常称为 fops,在其中定义将要实现的各种方法。假如要在 char_cdev 的驱动中实现 open、
            release、 read、 write 和 ioctl 方法,可以将 char_cdev 的 fops 定义为程序清单 2.16 所示的代
            码。
                    程序清单 2.16 字符设备的 fops
                        struct file_operations char_cdev_fops = {
                            .owner = THIS_MODULE,
                            .read = char_cdev_read,
                            .write = char_cdev_write,
                            .open = char_cdev_open,
                            .release = char_cdev_release,
                            .ioctl = char_cdev_ioctl
                        };
            各成员的赋值顺序没有特定要求,不必实现的成员可设置为 NULL 或者不写。定义
            char_cdev_fops 后,还需要分别实现 char_cdev_xxx 各方法的实际代码,并将 fops 与 char_cdev
            关联起来。
        
        3. 关联设备和 fops
            即使已经为驱动定义了 fops,但是在与设备关联起来之前,内核是无法为设备找到对应
            的操作方法的。所以必须通过某种途径将 fops 与设备关联起来。
            再回头来看 cdev_init()函数原型:
                void cdev_init(struct cdev *cdev, const struct file_operations *fops)
            第 2 个参数*fops,要求传入一个 file_operations 的结构指针。在前面介绍的范例中,
            都忽略了这个参数,仅仅传递了一个合法地址而已,但不是 file_operations 结构指针,所以
            前面两个设备驱动无法与任何操作方法关联。
            
            现在定义好了驱动的 fops 后, cdev_init()的正确用法应该是:
                cdev_init(char_cdev, &char_cdev_fops); //初始化 char_cdev 结构
            
            cdev_init()将定义好的 char_cdev_fops 的地址赋给 char_cdev 的 fops 指针,将定义的驱动
            操作方法与某个主设备号关联起来。当 open 系统调用打开某个设备,能够得知设备的主设
            备号,也就知道该用哪一组方法来操作这个设备了。 fops 在设备驱动和系统调用之间的关系
            如图 2.4 所示。
            
            图 2.4 fops 在设备驱动和系统调用之间的关系

          4. 驱动的方法和系统调用
            为驱动实现了 fops 方法,这个驱动就不再是空壳,通过驱动可以操作具体设备了。
            备注册后,该设备的主设备号与 fops 之间对应关系就一直存在于内核中,直到驱动生命周
            期结束(被卸载),应用程序发起系统调用,内核根据这个对应关系寻找正确的驱动程序来
            执行相关操作,如图 2.5 所示。
                图 2.5 系统调用和驱动方法
                
            操作流程:
            ( 1) 用户程序系统调用打开/dev/char 设备文件,获得主次设备号;
            ( 2) 根据主设备号,寻找对应的 fops;
            ( 3) 找到对应的 fops,执行驱动的 xxx_open 方法的代码。
            
2.4 字符驱动框架
    2.4.1 字符驱动框架
        接下来将前面所讲述的编写驱动的知识融合起来,给出一个完整的字符驱动程序的框架,
        一个典型的字符驱动框架如下代码所示。

点击(此处)折叠或打开

  1. #include <linux/init.h>
  2. #include <linux/module.h>
  3. ...
  4. static int char_cdev_open(struct inode *inode, struct file *file )
  5. { }

  6. static int char_cdev_release(struct inode *inode, struct file *file )
  7. { }

  8. static ssize_t char_cdev_read(struct file *file, char *buf,size_t count, loff_t *f_pos)
  9. { }

  10. static ssize_t char_cdev_write(struct file *file, const char *buf, size_t count, loff_t *f_pos)
  11. { }

  12. static int char_cdev_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
  13. { }

  14. struct file_operations char_cdev_fops = {
  15.     .owner = THIS_MODULE,
  16.     .read = char_cdev_read,
  17.     .write = char_cdev_write,
  18.     .open = char_cdev_open,
  19.     .release = char_cdev_release,
  20.     .ioctl = char_cdev_ioctl
  21. };

  22. static int __init char_cdev_init(void)
  23. { }

  24. static void __exit char_cdev_exit(void)
  25. { }

  26. module_init(char_cdev_init);
  27. module_exit(char_cdev_exit);

  28. MODULE_LICENSE("GPL");
2.4.2 测试程序
        驱动编写后,都需要进行测试才能知道驱动是否能工作,工作是否正常。驱动程序实现
        了哪些方法,测试程序就需要编写程序,进行相关的系统调用,对各种方法进行测试。如果
        测试不完善,带来的问题是很难估计的。    
        
2.5 第一个完整意义上的驱动
    在这一节将以 LED 驱动为例,讲述一个完整的有实际操作意义的驱动的实现过程。在
    编写一个驱动之前,必须根据硬件电路的特性为硬件设计合理的驱动方法。就 LED 指示灯
    而言,通常都是通过点亮或者熄灭指示灯,以指示不同的运行状态信息,显然,用 read 和
    write 这样的标准系统操作是不方便操作的。对于 LED 这样的硬件 I/O 操作,在驱动首选 ioctl
    方法来实现相应的功能。
    
    ioctl 系统调用主要用于增加系统调用的硬件控制能力,它可以构建自己的命令,也能接
    受参数。通过 ioctl 控制硬件 I/O,必须在驱动中为 ioctl()系统调用设计一些控制命令,通过
    不同的命令实现不同的硬件控制。    
    
    2.5.1 ioctl 命令
        先看一个用户程序通过 ioctl 系统调用控制 LED 的例子:
        ioctl(fd, SET_LED_ON, 2);
        其中的 SET_LED_ON 是命令, 2 是与命令相关的参数,至于参数具体表达什么含义,
        完全由驱动编写者来定义。
        
        1.ioctl 命令构成
            ioctl 操作与硬件平台相关,使用 ioctl 的驱动需要包含文件。然而实际上,
            这个文件却只是包含了一个与硬件平台相关的文件,对于 ARM 处理器,使用
            通用的 ioctl,最终使用
            
            每个 ioctl 命令实际上都是一个 32 位整型数,各字段和含义如表 2.1 所示。
            表 2.1 ioctl 命令各字段含义说明
                31~30
                    00 没有参数: _IO
                    01 写: _IOW
                    10 读: _IOR
                    11 读写: _IOWR    
                29~16
                    参数长度
                15~8
                    驱动的幻数,或者说特征码,常用 ASCII 字符表示,用于标识驱动                
                7~0
                    命令功能
            
            例如, 0x82187201 是带长度为 0x218 的参数读命令,功能号为 1,幻数用 ASCII 表示
            是“ r”,实际上这个命令是中的 VFAT_IOCTL_READDIR_BOTH 命令:
            #define     VFAT_IOCTL_READDIR_BOTH     _IOR('r', 1, struct __fat_dirent[2])
                        
        2. 构造 ioctl 命令
            为驱动构造 ioctl 命令,首先要为驱动选择一个可用的幻数作为驱动的特征码,以区分
            不同驱动的命令。内核已经使用了很多幻数,为了防止冲突,最好不要再使用这些系统已经
            占用的幻数来作为驱动的特征码。已经被使用的幻数列表详见文件。
            在不同平台上,幻数所使用情况都不同,为防止冲突,可以选择其它平台使用的幻数来用。
            
            选定幻数后,可以这样来进行定义:
            #define LED_IOC_MAGIC     'Z'
            
            ioctl 命令字段的 bit[31:30]表示命令的方向,分别表示使用_IO、 _IOW、 _IOR 和_IOWR
            这几个宏定义,分别用于构造不同的命令:
                _IO(type, nr)             构造无参数的命令编号
                _IOW(type, nr, size)     构造往驱动写入数据的命令编号
                _IOR(type, nr, size)     构造从驱动中读取数据的命令编号
                _IOWR(type, nr, size)     构造双向传输的命令编号
                
            这些宏定义中, type 是幻数, nr 是功能号, size 是数据大小。
            
            例如,为 LED 驱动构造 ioctl 命令,由于控制 LED 无需数据传输,可以这样定义:
                #define SET_LED_ON         _IO(LED_IOC_MAGIC, 0)
                #define SET_LED_OFF        _IO(LED_IOC_MAGIC, 1)
                
            如果想在 ioctl 中往驱动写入一个 int 型的数据,可以这样定义:
                #define CHAR_WRITE_DATA     _IOW(CHAR_IOC_MAGIC, 2, int)
            
            类似的,要从驱动中读取 int 型的数据,则定义为:
                #define CHAR_READ_DATA         _IOR(CHAR_IOC_MAGIC, 3, int)
            注意:同一份驱动的 ioctl 命令定义,无论有无数据传输以及数据传输方向是否相同,各命令的序号都
            不能相同。
            
        3. 解析 ioctl 命令
            驱动程序必须对传入的命令进行解析,包括传输方向、命令类型、命令编号以及参数大小,分别可以通过下面的宏定义完成:
                _IOC_DIR(nr)      解析命令的传输方向  (传输方向)
                _IOC_TYPE(nr)     解析命令类型  (也就是  命令幻数)
                _IOC_NR(nr)       解析命令序号 (也就是   命令功能)
                _IOC_SIZE(nr)     解析参数大小 (也就对应 参数长度)
                
            如果解析发现命令出错,可以返回-ENOTTY,如:
                if (_IOC_TYPE(cmd) != LED_IOC_MAGIC) {
                    return -ENOTTY;
                }
                
                if (_IOC_NR(cmd) >= LED_IOC_MAXNR) {
                    return -ENOTTY;
                }
                
    2.5.2 内核空间的 ioctl
        内核空间 iotcl 函数原型,即驱动的 ioctl 方法定义如下:
            int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
        定义的 ioctl 命令通过 cmd 传递,数据通过 arg 传递。驱动得到 cmd 命令和 arg 参数后,
        须首先用解析 ioctl 命令的宏定义对命令和参数进行解析判断,没有问题再进行后续处理。
        
    2.5.3 用户空间的 ioctl
        前面已经见过 ioctl 系统调用方法了,对比内核空间的 ioctl 函数原型,会发现两者是不
        同的,用户空间的 ioctl 系统调用原型如下:
            int ioctl (int fd, unsigned long cmd, ...)
        fd 是被打开的设备文件, cmd 是操作设备的命令,“ ...”代表可变数目的参数表,通常
        用 char *argp 来定义,如果 cmd 命令不需要参数,则传入 NULL 即可。
                
2.6 内核/用户空间的数据交换
    驱动与用户空间进行数据传递,可以通过 read 和 write 方法实现,也可以在 ioctl中完成,
    具体采用什么方式完成数据传递,取决于驱动和系统的实际情况。无论在什么方法中完成数
    据传递,都必须有数据传递的通道和工具,内核提供了几种与用户空间交换数据的方法,如
    put_user/get_user、 copy_to_user/copy_from_user 等。
    
    2.6.1 检查地址的合法性
        与用户空间交换数据有几组函数,无论用什么函数,都必须隐式或者显式的检查空间的
        合法性,通过 access_ok()完成。有些函数已经在内部完成了空间验证,带“ __”的函数则要
        求程序编写者自行进行验证。比如函数:__put_user/__get_user、 __copy_to_user/__copy_from_user。

        access_ok()函数仅仅用于验证某段空间能否被读写,而不进行数据传输:
            access_ok(type, addr, size);
        读写操作取决于 type 类型, 可选 VERIFY_READ 或者 VERIFY_WRITE, addr 是用户空间
        的地址, size 是字节数, 返回 1 表示成功, 0 表示失败。
        
        注意:
            如果对 ioctl 命令构造还有印象的话,一定要将 ioctl 命令的方向与 access_ok 的 READ
            和 WRITE 区分开来。 ioctl 对象是驱动, access_ok 对象是用户空间,两者的方向反的:
            (1)如果构造了_IOR 命令,从驱动中读取数据,则是往用户空间写入数据,须设置 type为 VERIFY_WRITE;
            (2)如果构造了_IOW 命令,往驱动写入数据,则需从用户空间读取数据,须设置 type为 VERIFY_READ;
            (3)如果要对用户空间进行读写操作,则需设置 type 为 VERIFY_WRITE。
            感觉用起来有点绕,不过实际上,内核提供的用户数据传输的接口函数都已经完成了验
            证工作,无需用户单独验证,但是内核也提供了没有验证操作的接口函数,如果驱动用了这
            些接口函数,则必须自行验证。

    2.6.2 往用户空间传递数据
        1.传递单个数据
            put_user()可以向用户空间传递单个数据。单个数据并不是指一个字节数据,对 ARM 而
            言, put_user 一次性可传递一个 char、 short 或者 int 型的数据,即 1、2 或者 4 字节。用 put_user
            比用 copy_to_user 要快:
                int put_user(x,p)
            x 为内核空间的数据, p 为用户空间的指针。 传递成功,返回 0,否则返回-EFAULT。
            
            put_user 一般在 ioctl 方法中使用,假如要往用户空间传递一个 32 位的数据,可以这样实现:
            static int char_cdev_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
            {
                int ret;
                u32 dat,
                switch(cmd)
                {
                    case CHAR_CDEV_READ:
                    ...其它操作
                    dat = 数据;
                    if (put_user(dat, (u32 *)arg) ) {
                        printk("put_user err\n");
                        return -EFAULT;
                    }
                }
                ...其它操作
                return ret;
            }
            __put_user 是没有进行地址验证的版本。
            
        2. 传递多个数据
            copy_to_user()可以一次性向用户空间传递一个数据块,函数原型如下:
            static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n);
            参数 to 是内核空间缓冲区地址, from 是用户空间地址, n 是数据字节数,返回值是不能被复制的字节数,
            返回 0 表示全部复制成功。
            
            copy_to_user()一般在 read 方法中使用。假如驱动要将从设备读到的 count 个数据送往用户空间,可以这样实现:
            static ssize_t char_cdev_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
            {
                unsigned char data[256] = {0};
                ....从设备获取数据
                if (copy_to_user((void *)buf, data, count)) {
                    printk("copy_to_user err\n");
                    return -EFAULT;
                }
                return count;
            }
            __copy_to_user 是没有进行地址验证的版本。
            
    2.6.3 从用户空间获取数据
        1. 获取单个数据
            调用 get_user()可以从用户空间获取单个数据,单个数据并不是指一个字节数据,对
            ARM 而言, get_user 一次性可获取一个 char、 short 或者 int 型的数据,即 1、 2 或者 4 字节。
            用 get_user 比用 get_from_user 要快:
                int get_user(x, p)
            x 为内核空间的数据, p 为用户空间的指针。 获取成功,返回 0,否则返回-EFAULT。
            get_user()一般也用在 ioctl 方法中。假如驱动需要从用户空间获取一个 32 位数,然后写
            到某个寄存器中,可以这样实现:
            static int char_cdev_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
            {
                int ret;
                u32 dat,
                switch(cmd)
                {
                    case CHAR_CDEV_WRITE:
                    if (get_user(dat, (u32 *)arg) ) {
                        printk("get_user err\n");
                        return -EFAULT;
                }
                    CHAR_CDEV_REG = dat;
                    ...其它操作
                }
                ...其它操作
                return ret;
            }
            __get_user 是没有进行地址验证的版本。
            
        2. 获取多个数据
            copy_from_user()可以一次性从用户空间获取一个数据块,函数原型如下:
            static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n);
            参数 to 是内核空间缓冲区地址, from 是用户空间地址, n 是数据字节数,返回值是不能被
            复制的字节数,返回 0 表示全部复制成功。

            copy_from_user()常用在 write 方法中。如果驱动需要从用户空间获取 count 字节数据,
            用于操作设备,可以这样实现:
            static ssize_t char_cdev_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
            {
                unsigned char data[256];
                if (copy_from_user(&data, buf, 256) ) {
                    printk("copy_from_user err\n");
                    return -EFAULT;
                }
                ...
            }
            __copy_from_user 是没有进行地址验证的版本。
           
阅读(908) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~