• linux学习--字符设备驱动


    linux驱动有基本的接口进行注册和卸载,这里不再做详细说明,本文主要关注linux字符设备驱动框架实现的细节。

    1.字符设备驱动抽象结构

    字符设备驱动管理的核心对象是字符设备,从字符设备驱动程序的设计框架出发,内核为字符设备抽象出数据结构struct cdev,定义如下:

    include/linux/cdev.h
    struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
    };
    

    设备驱动程序中可以有两种方式来产生struct cdev对象,一种是静态定义的方式,另一种是在程序的执行期通过动态分配。而一个struct cdev对象在被加入系统前,应该被初始化,可以通过cdev_init接口完成,具体实现如下:

    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.设备号及设备节点

    由于struct cdev在添加到系统时需要设备号和设备节点的知识,所以此处先做个简介。

    2.1 设备号分配与管理

    linux系统中设备号由主设备号和次设备号组成,内核使用主设备号来定位设备驱动程序,用次设备号来管理同类设备。使用dev_t类型变量标记设备号,具体定义如下:

    include/linux/types.h 
    typedef __u32 __kernel_dev_t; 
    typedef __kernel_dev_t dev_t;
    

    在当前版本内核,dev_t的低20位表示次设备号,高12位表示主设备号,具体定义如下:

    include/linux/kdev_t.h
    #define MINORBITS20
    #define MINORMASK ((1U << MINORBITS) - 1)
    
    #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
    #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
    
    

    字符设备涉及到设备号分配的内核函数

    fs/char_dev.c
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    {
        struct char_device_struct *cd;
        dev_t to = from + count;
    
        dev_t n, next;
    
        for (n = from; n < to; n = next) {
            next = MKDEV(MAJOR(n)+1, 0);
            if (next > to)
                next = to;
            cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name);
            if (IS_ERR(cd))
                goto fail;
        }
    
        return 0;
    fail:
        to = n;
        for (n = from; n < to; n = next) {
            next = MKDEV(MAJOR(n)+1, 0);
            kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
        }
        return PTR_ERR(cd);
    }
    
    

    该函数第一个参数表示设备号,第二个参数表示连续设备编号个数,即驱动管理的同类设备个数,第三个参数表示设备的名称,该函数的核心是__register_chrdev_region,讨论该函数前需要先看下全局指针数组chrdevs,这个数组每一项都指向一个struct char_device_struct结构,具体定义如下:

    static struct char_device_struct
    {
        struct char_device_struct *next;
        unsigned int major;
        unsigned int baseminor;
        int minorct;
        char name[64];
        struct cdev *cdev;/* will die */
    } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
    
    

    __register_chrdev_region将当前设备驱动程序使用的设备号记录到这个数组中,首先分配一个struct char_device_struct的对象,然后对其初始化,完成后哈希遍历char devs并将这个对象添加到数组中。 在对字符设备初始化后,需要将该设备添加到内核当中,可以查看cdev_add函数定义。

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    {
        int error;
    
        p->dev = dev;
        p->count = count;
    
        error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);
        if (error)
            return error;
        kobject_get(p->kobj.parent);
    
    return 0;
    }
    

    cdev_add函数的核心是通过kobj_map实现,后者通过操作一个全局变量cdev_map把设备加入到哈希表中,cdev_map定义如下: static struct kobj_map *cdev_map;

    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;
    }; 
    

    简单的说,设备驱动程序通过调用cdev_add把它所管理的设备对象指针嵌入到一个struct probe节点对象中,然后再把该节点加入到cdev_map的哈希链表中。

    2.2 设备节点的生成

    设备节点的创建可以使用mknod指令在/dev目录下进行创建,例如mknod /dev/demodev c 2 0,如果该指令成功执行,将会在/dev目录下生成一个demodev的字符设备节点。

    而mknod实际上是通过系统调用sys_mknod进入内核空间,函数的原型asmlinkage long sys_mknod(const char __user *filename, umode_t mode, unsigned dev);

    mknod首先在根目录下寻找dev目录所对应的inode,通过inode编号得到该inode的内存地址,然后通过dev的inode结构中的i_op成员指针(ext3_dir_inode_operations),来调用该对象的mknod方法,这将导致ext3_mknod函数被调用。

    ext3_mknod函数会创建一个新的inode节点,然后调用init_special_inode函数与设备相关联,具体实现如下:

    void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
    {
        inode->i_mode = mode;
        if (S_ISCHR(mode)) {
            inode->i_fop = &def_chr_fops;
            inode->i_rdev = rdev;
        } else if (S_ISBLK(mode)) {
            inode->i_fop = &def_blk_fops;
            inode->i_rdev = rdev;
        } else if (S_ISFIFO(mode))
            inode->i_fop = &pipefifo_fops;
        else if (S_ISSOCK(mode))
            inode->i_fop = &bad_sock_fops;
        else
            printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"" inode %s:%lu
    ", mode, inode->i_sb->s_id,inode->i_ino);
    }
    

    这个函数主要是初始化inode的成员i_fop和i_rdev,其中i_rdev成员表示该inode所对应设备的设备号。

    3.打开设备文件

    假设已经实现了/dev/demodev的创建,我们再来看下用户空间open函数是如何打开设备文件的。

    首先看下linux用户空间open函数原型,详细说明可以使用man 2 open查看,其定义int open(const char pathname, int flags, mode_t mode);

    而struct file_operations结构中对应open函数原型int (*open) (struct inode *, struct file *)两者差别很大,那么用户态的open接口是如何一步步的调用到内核态的open函数呢?首先用户空间的open会产生系统调用,通过sys_open进入内核空间,而sys_open函数声明如下:

    asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);

    但是源码中搜寻不到sys_open函数实现,可以通过”SYSCALL_DEFINE3(open”搜索源代码,感兴趣的同学可以把这个宏展开,这里不再赘述,看下函数定义:

    SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
    {
        if (force_o_largefile())
        flags |= O_LARGEFILE;
    
        return do_sys_open(AT_FDCWD, filename, flags, mode);
    }
    
    //很明显do_sys_open才是代码核心,接着来看:
    long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
    {
        struct open_flags op;
        int lookup = build_open_flags(flags, mode, &op);
        struct filename *tmp = getname(filename);
        int fd = PTR_ERR(tmp);
    
        if (!IS_ERR(tmp)) {
            fd = get_unused_fd_flags(flags);
            if (fd >= 0) {
                struct file *f = do_filp_open(dfd, tmp, &op, lookup);
                if (IS_ERR(f)) {
                    put_unused_fd(fd);
                    fd = PTR_ERR(f);
                } else {
                    fsnotify_open(f);
                    fd_install(fd, f);
                }
            }
            putname(tmp);
        }
    
        return fd;
    }
    

    其中do_sys_open首先会调用get_unused_fd_flags为这次open操作分配一个文件描述符fd,get_unused_fd_flags实际上是对alloc_fd的封装。随后do_sys_open调用do_filp_open函数,后者会查询/dev/demodev设备文件inode,将inode结构中i_fop赋值给filp->f_op,然后调用i_fop中的open函数。在设备节点的生成时,inode->i_fop = &def_chr_fops,所以open函数直接调用chrdev_open。

    const struct file_operations def_chr_fops = {
        .open = chrdev_open,
        .llseek = noop_llseek,
    };
    
    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;
    }
    

    chrdev_open通过调用kobj_lookup在cdev_map中用inode->i_rdev来查找设备号对应的字符设备,成功找到设备后,通过filp->f_op = fops_get(p->ops)将cdev对象的ops赋值给file对象的filp成员,同时会把cdev对象保存到inode->i_cdev成员中,这时已经将file和file_operations关联起来。

    Author: xujinlong Email: xyxujinlong@163.com
  • 相关阅读:
    jQuery 回到顶部
    c# 获取客户端ip
    JS 新浪API获取IP归属地
    c#抓取网站数据
    ECLIPSE最常用快捷键排名
    BinarySearchTree示例——C++模板实现
    SICP 找零钱问题背后的思考
    关于解引用*和箭头操作符->的重载
    traits技法小计
    最大和子序列问题
  • 原文地址:https://www.cnblogs.com/tinylaker/p/9810707.html
Copyright © 2020-2023  润新知