现在的位置: 首页 > 综合 > 正文

深入linux设备驱动程序内核机制(第二章) 读书笔记

2013年02月17日 ⁄ 综合 ⁄ 共 8357字 ⁄ 字号 评论关闭

第二章    字符设备驱动程序

本文 欢迎转载,

原文地址: blog.csdn.net/dyron

    内核为了简化设备驱动程序员的工作, 从各异的设备中提取出了共性的特征, 将其化分为三大类: 字符设备

    ,块设备,网络设备。内核针对每一类设备都提供了驱动模型框架。

2.1 应用程序与设备驱动程序互动实例
    书上实现了一个调用设备驱动程序的实例, 包括驱动程序和应用程序。

2.2 struct file_operations
    这个结构中实现的几乎全是函数指针, 除了owner外, 它表示当前struct file_operations对象所属的内核
    模块, 几乎所有的设备驱动程序都会用THIS_MODULE宏给owner赋值。

    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 *, 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 **);
    };

    #define THIS_MODULE(&__this_module)

    __this_module是内核模块的编译工具链为当前模块产生的struct module类型对象, 所有THIS_MODULE实际上
    是当前内核模块对象的指针, file_operations中的owner成员可以避免file_operations中的模块正在被调用
    时, 其所属的模块被从系统中卸载掉。 如果是静态编译进内核的, THIS_MODULEU将被赋值为空指针。
???????????owner是如何被用来避免模块被卸载的
2.3 字符设备的内核抽象
    字符设备抽象出一个具体的数据结构struct cdev;

    struct cdev {
    struct kobject okbj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
    }

    struct kobject kobj: 内嵌的内核对象, 其用途将在设备模型一章中讨论
    struct module *owner: 字符设备驱动程序所在的内核模块对象指针.
    const struct file_operations *ops: 在应用程序通过文件调入到内核态时, ops指针起桥梁作用.
    struct list_head list: 用来将系统中的字符设备形成链表.
    dev_t dev: 字符设备的设备号, 由主次设备号构成.
    unsigned int count:同一主设备号的次设备号的个数, 表示当前驱动控制的实际同类设备的数量.

    产生struct cdev的方式:
    静态: struct cdev  动态: static struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL);

    Linux源码中还提供一个接口: cdev_alloc,专门用于动态分配, 而且还会进行必要的初始化。

    struct cdev *cdev_alloc(void)
    {
            struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
        if (p) {
             INIT_LIST_HEAD(&p->list);
             kobject_init(&p->kobj, &ktype_cdev_dynamic);
        }   
        return p;
    }

    系统抽象出cdev仅对字符驱动框架结构设计的需要, 现实中往往比这复杂的多, cdev结构常常做过一种内嵌
    的数据结构出现在实际设备的数据结构中。

    初始化cdev的方式:

    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    {
        memset(cdev, 0, sizeof *cdev);
        INIT_LIST_HEAD(&cdev->list);
        kobject_init(&cdev->kobj, &ktype_cdev_default);
        cdev->ops = fops;
    }

2.4 设备号的构成与分配
    设备号由主设备号与次设备号组成, 主设备号用来定位对应的设备驱动, 次设备号用来标识同一驱动所管理
    的若干类同类设备。 dev_t类型变量标识一个设备号, 这是个32位的unsigned int;

    dev_t的代20位用来表示次设备号, 高12位用来表示主设备号, 但为了兼容性, 应使用以下宏来对设备号操
    作.

    MAJOR MINOR MKDEV:
    MAJOR从dev_t类型的设备号中提取主设备号,MINOR用来提取次设备号, MKDEV将主设备号和次设备号合成
    dev_t类型的设备号

    2.4.2 设备号的分配和管理
    
    .register_chrdev_region
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    参数from表示设备号, count是连续设备编号的个数,表示驱动所管理同类设备的个数,name表示驱动名,

    static struct char_device_struct *chrdevs[CHRDEV_MAJOR_HASH_SIZE];用来管理和分配设备号。
    数组中的每一项都指向一个struct char_device_struct类型的指针。

    register_chrdev_region函数的功能是将当前设备驱动程序要使用的设备号记录到chrdevs数组中, 有了这种
    记录,系统就可以避免不同的设备驱动程序使用同一个设备号了。

    这个过程在__register_chrdev_region中实现, 它首先分配struct char_device_struct 对象cd并初始化,
    然后开始搜索chrdevs数据, 搜索主设备号。

    .alloc_chrdev_region函数
    这个函数也是调用__register_chrdev_region, 相对于register_chrdev_region, alloc_chrdev_region在
    调用__register_chrdev_region时, 传入第一个参数为0, 这样在代码中就会走另一个逻辑, 使用一个for
    循环从chrdevs数组的最后一项向前扫描, 如果发现第i项为null, 就把该项的索引值作为分配的主设备号返
    回给驱动程序,并生成一个struct char_device_struct节点, 将其加入到chrdevs[i]对应的哈希链表中。

    设备号做为一种系统资源, 在设备驱动被卸载时, 需要通过unregister_chrdev_region将设备号释放。

2.5 字符设备的注册
    
    完成了设备的初始化阶段后, 就可以把它加入到系统中, 以供别的模块使用它。

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    {
        p->dev = dev;
        p->count = count;
        return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
    }

    参数p为要加入到系统的字符设备对象指针, dev为设备的设备号, count表示从设备号开始连续的设备数量。
    cdev_add的核心功能通过kobj_map函数实现, 后者通过操作一个全局变量cdev_map来把设备*p加入到其中的
    哈希链表中。

    static struct kobj_map *cdev_map;
    这是一个struct kobj_map指针类型的全局变量, 在系统启动其间由chrdev_init函数负责初始化。

    struct kobj_map {
    struct probe {
        struct probe *next;
        dev_t dev;
        unsigned long range;
        struct module *owner;
        kobj_probe_t *get;
        int (*lock)(dev_t, void *);
        void *data;
    } *probes[255];
    struct mutex *lock;
    };

    通过要加入的主设备号来获得probes数组的索引值, 然后把一个类型为struct probe的节点对象加入到prob
    es[i]所管理的链表中, 实现加入的方法和前边设备号管理相同, 通过主设备号来获得probes数组中的索引值
    , 然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中。
2.6 设备节点的生成
    设备文件是特殊的文件类型, 存在的意义就是沟通用户空间程序和内核空间程序。
    mknod命令最终是通过调用mknod函数实现, 调用的参数有两个, 一个是设备文件名, 一个是设备号, 设备
    文件名主要在用户空间使用, 内核空间刚使用inode来表示相应的文件, mknod命令是通过系统调用sys_mkno
    d进入内核空间.

    sys_mknod的最后一个参数dev, 是由用户空间mknod命令构造出的设备号, sys_mknod系统调用将通过/dev目
    录上挂载的文件系统来为/dev/demodev生成一个新的inode, 设备号将被记录在这个新的inode对象上。

    sys_mknod首先在根文件系统ext3的根目录"/"下寻找dev目录对应的inode, ext3文件系统会通过inode编号得
    到inode结构在内存中的实际地址, 接下来会通过dev的inode结构中的i_op成员指针指向的ext3_dir_inode_
    operations, 来调用该对象中的mknod方法, 导致会调用ext3_mknod被调用.

    ext3_mknod主要作用是生成一个新的inode, ext3_mknod中会调用一个和设备驱动程序关系密切的init_speci
    al_inode函数, 这个函数的主要功能是为新生成的inode初始化其中的i_fop和i_rdev成员, 设备文件节点的
    inode中的i_rdev成员用来表示该inode所对应设备的设备号, 通过参数rdev为其赋值。

    i_fop成员的初始化根据是字符设备还是块设备而有不同的赋值, 对于字符设备fop指向def_chr_fops, 后者
    主要定义一个open操作, 相对于字符设备, 块设备的def_blk_fops定义则要复杂。

2.7 字符设备文件的打开操作
    用户空间程序调用open, 将发起一个系统调用, 通过sys_open函数进入内核空间, 其中一系列掉用关系如下:

    sys_open --- do_sys_open --- do _filp_open --- do_filp_open --- nameidata_to_filp ---

    __dentry_open --- chrdev_open

    do_sys_open首先通过get_unused_fd_flags为本次的open分配一个未使用过的文件描述符.
    do_filp_open函数会首先查找设备文件所对应的inode, 之后会调用get_empty_filp, 为每个打开的文件分配
    一个新的struct file类型内存空间。 内核用struct file对象来描述进程打开的每一个文件的视图, 即使打
    开同一文件, 内核也会为之生成一个新的struct file对象, 用来表示当前操作的文件相关信息.

    这个结构中与设备驱动程序关系最紧密的是f_op, f_flags, f_count和private_data成员.
    f_op是指针struct file_operations, f_flags用于记录当前文件被open时所指定的打开模式, 将影响后续的
    read/write等函数的行为模式.
    f_count用于对struct file对象的使用计数, 当close一个文件时, 只有struct file对象中的f_count为0才
    真正执行关闭private_data常被用来记录设备驱动程序自身定义的数据。

    进程为文件操作维护一个文件描述符表(current->files->fdt), 设备文件打开, 会得到一个fd, 然后用fd作
    为进行维护文件描述符表(struct file*类型数组)的索引值, 将新分本的struct file空间地址赋值给它.

    在do_sys_open后半部分, 会调用__dentry_open将设备对应节点的inode中的i_fop赋值给filp->f_op(即是
    def_chr_fops), 然后调用i_fop中的open, 于是chrdev_open(def_chr_fops)就被调用到, 该函数灰常重要.

static int chrdev_open(struct inode *inode, struct file *filp)
{
    struct cdev *p;
    struct cdev *new = NULL;
    int ret = 0;

    spin_lock(&cdev_lock);
    p = inode->i_cdev;
    if (!p) {
    struct kobject *kobj;
    int idx;
    spin_unlock(&cdev_lock);
    kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
    if (!kobj)
        return -ENXIO;
    new = container_of(kobj, struct cdev, kobj);
    spin_lock(&cdev_lock);
    /* Check i_cdev again in case somebody beat us to it while
       we dropped the lock. */
    p = inode->i_cdev;
    if (!p) {
        inode->i_cdev = p = new;
        list_add(&inode->i_devices, &p->list);
        new = NULL;
    } else if (!cdev_get(p))
        ret = -ENXIO;
    } else if (!cdev_get(p))
    ret = -ENXIO;
    spin_unlock(&cdev_lock);
    cdev_put(new);
    if (ret)
    return ret;

    ret = -ENXIO;
    filp->f_op = fops_get(p->ops);
    if (!filp->f_op)
    goto out_cdev_put;

    if (filp->f_op->open) {
    ret = filp->f_op->open(inode,filp);
    if (ret)
        goto out_cdev_put;
    }   

    return 0;

out_cdev_put:
    cdev_put(p);
    return ret;
}

    函数首先通过kobj_lookup在cdev_map中用inode->i_rdev来查找设备号对应的设备new, 并将i_rdev对应到在
    cdev_map中找到的设备对象cdev,  然后通过filp->f_op = new->ops,  这行代码将设备对象new中的ops指针
    赋值给filp对象中的f_op成员, 此时展示了驱动程序中实现的struct file_operations  与filp关联起来。
    接下来检测驱动是否实现了open, 如果实现就调用之.

!!!!!!此时驱动中的filp_operations替换掉了之前赋值的def_chr_ops, filp是每个进程单独拥有的文件描述实例

    内核每次打开一个设备文件时, 都会产生一个文件描述符fd和一个新的struct file对象filp来跟踪对文件的
    这一次操作, 在打开文件时, 内核会将filp和fd关系起来, 同时会将cdev中的ops赋值给filp->fop, 最后sys
    _open纱线充调用将设备文件描述符fd返回给用户空间, 如此用户空间的read, write就会对应到驱动上实现的
    函数了.

    通过以上过程, 可以发现设备号在其中的重要作用, cdev_add把一个设备加入到系统时, 需要一个设备号来标
    记对象在cdev_map中的位置信息, 当mknod生成设备文件节点时, 也需要在命令行中提供设备号信息, 内核会
    将该设备号信息记录到设备文件节点对应的inode的i_rdev成员中, 当打开一个设备时, 系统会根据设备文件
    对应的inode->i_rdev信息在cdev_map中寻找设备, 所以在这个过程中务必要保证文件节点的inode->i_rdev数
    据和设备驱动程序使用的设备号完全一致.

    sys_close流程
    sys_close调用filp_close, 首先判断filp中的f_count成员是否为0, 如果设备驱动定义了flush函数, 那么在
    release函数被调用前, 会先调用flush, 这为了确保在把文件关闭前缓存在系统中的数据被真正写回硬件中.

!!!!!!flush函数, 从来没有实现过.

    函数最后调用fput, 如果f_count为0, 就调用release函数, 接下来是系统资源释放.

本文 欢迎转载,

原文地址: blog.csdn.net/dyron

2.7 本章小结
    
    字符设备的基本结构, 比较简单了, 不写什么了.

抱歉!评论已关闭.