文档章节

Kernel字符设备驱动框架

y
 yepanl
发布于 05/24 00:12
字数 1969
阅读 10
收藏 0

    Linux设备分为三大类:字符设备,块设备和网络设备,这三种设备基于不同的设备框架。相较于块设备和网络设备,字符设备在kernel中是最简单的,也是唯一没有基于设备基础框架(device结构)的设备,因此字符设备无法通过sysfs来访问。那么,用户是如何来访问字符设备的?答案是通过设备节点来访问字符设备。从这个角度来说,字符设备的本质就是提供用户接口,使得用户可以通过 VFS 来访问设备节点,从而达到访问字符设备的目的。下面开始分析字符设备框架的实现。

    1,字符设备数据结构:

struct cdev {
    struct kobject kobj;    // 虽然内嵌了kobject结构,但是没有加入kset链表,因此不会在sysfs中生效
    struct module *owner;    // 所属模块
    const struct file_operations *ops;    // 文件操作接口,用户通过该接口访问字符设备驱动
    struct list_head list;    // 设备链表节点
    dev_t dev;    // 设备号,由主从设备号组成。设备号是设备节点和cdev之间的纽带
    unsigned int count;    // 从设备个数
};

    2,字符设备注册 register_chrdev():

    static inline int register_chrdev(unsigned int major, const char *name,
                  const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}

int __register_chrdev(unsigned int major, unsigned int baseminor,
              unsigned int count, const char *name,
              const struct file_operations *fops)
{
    struct char_device_struct *cd;
    struct cdev *cdev;
    int err = -ENOMEM;

    cd = __register_chrdev_region(major, baseminor, count, name);    // 注册设备占用的主从设备号范围
    if (IS_ERR(cd))
        return PTR_ERR(cd);

    cdev = cdev_alloc();    // 分配cdev结构
    if (!cdev)
        goto out2;

    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);    // 初始化cdev

    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);    // 执行cdev的注册
    if (err)
        goto out;

    cd->cdev = cdev;    // 将cdev关联到设备号结构

    return major ? 0 : cd->major;
out:
    kobject_put(&cdev->kobj);
out2:
    kfree(__unregister_chrdev_region(cd->major, baseminor, count));
    return err;
}
    从上面可以看出,注册一个字符设备包含两个步骤:1,申请/注册设备号范围。2,注册cdev

    (1)申请/注册设备号范围

        用户通过设备号访问字符设备,字符设备号的分配就需要统一管理起来,防止同一个设备号被重复分配。字符设备号的分配通过__register_chrdev_region()接口完成。

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

static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor,
               int minorct, const char *name)
{
    struct char_device_struct *cd, **cp;
    int ret = 0;
    int i;

    cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);    // 申请 char_device_struct结构
    if (cd == NULL)
        return ERR_PTR(-ENOMEM);

    mutex_lock(&chrdevs_lock);

    /* temporary */
    if (major == 0) {    // 如果没有指定主设备号,由系统动态分配一个主设备号
        for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
            if (chrdevs[i] == NULL)
                break;
        }

        if (i == 0) {
            ret = -EBUSY;
            goto out;
        }
        major = i;
    }

    // 初始化该char_device_struct

    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name));

    i = major_to_index(major);    // chrdevs本质上是个hash数组,计算hash code

    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major ||
            ((*cp)->major == major &&
             (((*cp)->baseminor >= baseminor) ||
              ((*cp)->baseminor + (*cp)->minorct > baseminor))))
            break;

    /* Check for overlapping minor ranges.  */
    if (*cp && (*cp)->major == major) {
        int old_min = (*cp)->baseminor;
        int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
        int new_min = baseminor;
        int new_max = baseminor + minorct - 1;

        /* New driver overlaps from the left.  */
        if (new_max >= old_min && new_max <= old_max) {
            ret = -EBUSY;
            goto out;
        }

        /* New driver overlaps from the right.  */
        if (new_min <= old_max && new_min >= old_min) {
            ret = -EBUSY;
            goto out;
        }
    }

    cd->next = *cp;
    *cp = cd;    // 将新的char_device_struct加入hash数组链表

    mutex_unlock(&chrdevs_lock);
    return cd;
out:
    mutex_unlock(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}

    (2)cdev注册:

    字符设备注册,本质上就是将cdev加入一个全局范围的链表上,当用户通过设备号查询设备时,从该链表上找到相应的cdev,调用 cdev 下的file_operations对设备进行操作。实际情况要比这复杂点,考虑到一个设备可能有多个子设备,即一次设备注册请求的设备号可能横跨多个主设备号,linux设计了一个大小为255的hash数组,以主设备号为hash key,以 kobj_map 为元素,将1个或连续的多个probe加入以主设备号为key的hash数组,其中每个probe都指向该cdev。当查询某个设备的时候,以主设备号为key查询hash数组的冲突链表,找到目标设备号落入的probe,取出关联的cdev。

struct kobj_map {
    struct probe {
        struct probe *next;    // hash冲突链表
        dev_t dev;    // 首个设备号
        unsigned long range;    // 子设备个数
        struct module *owner;
        kobj_probe_t *get;    // 获取cdev方法
        int (*lock)(dev_t, void *);
        void *data;    // 通常就是cdev
    } *probes[255];
    struct mutex *lock;
};

    设备加入hash数组的接口如下

int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
         struct module *module, kobj_probe_t *probe,
         int (*lock)(dev_t, void *), void *data)
{
    unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;    // 该设备横跨的主设备号个数
    unsigned index = MAJOR(dev);    // 起始主设备号
    unsigned i;
    struct probe *p;

    if (n > 255)
        n = 255;

    p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);    // 分配1个或多个连续的probe
    if (p == NULL)
        return -ENOMEM;

    for (i = 0; i < n; i++, p++) {
        p->owner = module;
        p->get = probe;
        p->lock = lock;
        p->dev = dev;
        p->range = range;
        p->data = data;
    }    // 每个probe都关联该cdev

    mutex_lock(domain->lock);
    for (i = 0, p -= n; i < n; i++, p++, index++) {
        struct probe **s = &domain->probes[index % 255];
        while (*s && (*s)->range < range)
            s = &(*s)->next;
        p->next = *s;
        *s = p;
    }    // 每个probe加入到相应的hash冲突链表

    mutex_unlock(domain->lock);
    return 0;
}

    查询设备hash数组的接口如下:

struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
    struct kobject *kobj;
    struct probe *p;
    unsigned long best = ~0UL;

retry:
    mutex_lock(domain->lock);
    for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {    // 根据主设备号找到hash冲突链表,循环访问链表上的每个元素,找到目标设备号落入的probe
        struct kobject *(*probe)(dev_t, int *, void *);
        struct module *owner;
        void *data;

        if (p->dev > dev || p->dev + p->range - 1 < dev)
            continue;    // 目标设备号不在该probe范围内,继续寻找下一个

        if (p->range - 1 >= best)
            break;
        if (!try_module_get(p->owner))
            continue;
        owner = p->owner;
        data = p->data;
        probe = p->get;
        best = p->range - 1;
        *index = dev - p->dev;    // 找到目的probe

        if (p->lock && p->lock(dev, data) < 0) {
            module_put(owner);
            continue;
        }
        mutex_unlock(domain->lock);
        kobj = probe(dev, index, data);    // 取出probe指向的 cdev
        /* Currently ->owner protects _only_ ->probe() itself. */
        module_put(owner);
        if (kobj)
            return kobj;    // 返回 cdev

        goto retry;
    }
    mutex_unlock(domain->lock);
    return NULL;
}

    (3)字符设备的打开:   

static int chrdev_open(struct inode *inode, struct file *filp)
{
    const struct file_operations *fops;
    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);    // 找到cdev
        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;    // 第一次打开该字符设备,将cdev赋值给inode,下次打开时,不必再次查询字符设备链表

        } 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;
    fops = fops_get(p->ops);    // 取出cdev的file_operations
    if (!fops)
        goto out_cdev_put;

    replace_fops(filp, fops);    // 替换字符设备的file_operations
    if (filp->f_op->open) {
        ret = filp->f_op->open(inode, filp);    // 调用字符设备驱动的open()方法
        if (ret)
            goto out_cdev_put;
    }

    return 0;

 out_cdev_put:
    cdev_put(p);
    return ret;
}

    至此,我们已经分析了整个字符设备的框架:从字符设备的注册,到字符设备的查询和打开,框架架构简单明了。字符设备作为用户访问设备的向上接口,通常跟i2c,spi,pci等向下接口驱动同时工作,以组成一个完整的硬件字符设备。在这样的设备中,用户通过字符设备驱动接口获取设备,再通过i2c等驱动接口访问实际硬件。

© 著作权归作者所有

上一篇: Kernel DMA
y
粉丝 1
博文 79
码字总数 106002
作品 0
南京
程序员
私信 提问
linux分类驱动对字符设备框架压力的卸载

2.6内核引入了input字系统,usb子系统,misc子系统等一系列字符设备子系统,在熟练掌握这些子系统之后,我们来看一下linux内核设计这么些子系统的意义何在?可以连接的设备越来越多,这些设备...

晨曦之光
2012/04/10
115
0
platform_device和platform_driver的注册过程,及probe函数何时调用的分析

转载于http://blog.chinaunix.net/uid-7828352-id-3833188.html 参考资料:http://blog.csdn.net/xiafeng1113/article/details/8030248 参考资料:http://www.linuxidc.com/Linux/2017-08/1......

oqqHuTu12345678
2017/12/29
0
0
驱动框架2——内核驱动框架中LED的基本情况、初步分析

以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除。 一、内核驱动框架中LED的基本情况 1、相关文件(1)drivers/leds目录 驱动框架规定的LED这种硬件的驱动应该待的地方。 (2)d...

oqqHuTu12345678
2017/07/20
0
0
V4L2 driver -整体架构

我的uvc开源地址:gitee-uvc 字符设备驱动程序核心:V4L2本身就是一个字符设备,具有字符设备所有的特性,暴露接口给用户空间。 V4L2 驱动核心:主要是构建一个内核中标准视频设备驱动的框架...

yooooooo
2018/08/16
0
0
嵌入式Linux驱动程序开发

嵌入式Linux驱动程序开发 1.设备驱动程序的概念... 2 2.处理器与设备间数据交换方式... 2 21.查询方式... 2 2.2.中断方式... 3 2.3.直接访问内存(DMA)方式... 3 3.驱动程序结构... 3 3...

长平狐
2013/06/03
2.4K
1

没有更多内容

加载失败,请刷新页面

加载更多

kotlin中奖概率

fun main() { var wins = 0 var chance = 0.2 var playTime = 10// 1000万次 val n = 10000000 (1..n).forEach { if (play(chance, playTime)) win......

oschina4cyy
9分钟前
0
0
第十一讲:爬取猫眼网站上的前100名电影

本次我们来通过翻页爬取的方式爬取猫眼电影里面推荐的前100名电影,并存储到数据库。 1、我们登录猫眼,看下我们的数据在哪里 然后点击今日TOP100,看下具体的网页数据 最下面我们看到底部有...

刘日辉
9分钟前
7
0
git 设置全局用户名/邮箱和某个项目用户名/邮箱/密码

一、特定项目设置用户名/邮箱/密码的方法 找到项目所在目录下的 .git,进入.git文件夹,然后执行如下命令分别设置用户名和邮箱 git config user.name "dawn.he" git config user.email "1...

hexiaoming123
9分钟前
0
0
好程序员分享Linux重器vi编辑器

创建练习文件 [root@tianyun ~]# ll -a > list.txt 文件编辑器 gedit 文件编辑器 vi, vim, nano vi编辑器工作模式 命令模式: a. 光标定位 hjkl 0 $ gg G 3G 进入第三行 /string (n N 可以循...

好程序员IT
10分钟前
2
0
Git整理(五) git cherry-pick的使用

概述 git cherry-pick可以理解为”挑拣”提交,它会获取某一个分支的单笔提交,并作为一个新的提交引入到你当前分支上。 当我们需要在本地合入其他分支的提交时,如果我们不想对整个分支进行...

嘿嘿嘿IT
18分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部