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\n", 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关联起来。

posted @ 2018-10-18 15:21  tinylaker  阅读(361)  评论(0编辑  收藏  举报