欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

linux学习--字符设备驱动

程序员文章站 2022-10-30 19:29:38
linux驱动有基本的接口进行注册和卸载,这里不再做详细说明,本文主要关注linux字符设备驱动框架实现的细节。 [TOC] 1.字符设备驱动抽象结构 字符设备驱动管理的核心对象是字符设备,从字符设备驱动程序的设计框架出发,内核为字符设备抽象出数据结构struct cdev,定义如下: 设备驱动程序 ......

目录

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关联起来。