字符设备驱动

字符设备驱动

字符设备驱动是一种驱动编写的框架,使用设备号来标识唯一的设备,cdev结构抽象为这个字符设备(主要包括file_operations操作函数),以文件的形式提供给用户空间。用户最直观的感受就是通过读写操作字符设备文件(设备节点),可以对应到内核驱动实现的文件接口函数。

参考:Linux设备管理(二)_从cdev_add说起(超详细) - 知乎 (zhihu.com)

内核版本:4.1.15

1.字符设备驱动的编写流程

在应用层面,字符设备驱动提供的框架就是,驱动程序编写这实现一些file_operations接口,比如read等,然后在用户空间需要有一个字符设备文件作为设备节点,用户程序可以对该设备节点进行read等操作,最终实际执行驱动中实现的函数,从而达到设备即文件,通过文件控制设备的效果。下面是应用的必要步骤:

  • 设备号的分配和释放:设备号的分配采用动态分配和静态分配两种方式,可以一次申请多个,接口定义如下:

    #include <linux/fs.h>
    ​
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char  *name);
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    ​
    void unregister_chrdev_region(dev_t from, unsigned count);
  • 字符设备注册和添加:我们需要初始化一个cdev结构,主要填充ownerops域;然后将其添加到内核中:

    #include <linux/cdev.h>
    ​
    void cdev_init(struct cdev *cdev, const struct file_operations *fops);
    int cdev_add(struct cdev *p, dev_t dev, unsigned count);
    ​
    void cdev_del(struct cdev *p);
  • 创建设备节点:有两种方式

    • 在用户空间根据设备号mknod手动创建,一般创建在/dev目录下

      mknod /dev/【设备名】 c 【主设备号】 【次设备号】 
      • 让内核的设备统一模型自己去维护。在设备统一模型中,每一个设备在进行变动的时候,会通过kobject_uevent上报自己的变动事件,其会往上找到一个kset进行用户空间的上报;上报方式有netlinkkmod两种,这里采用kmod,会将上报信息放到环境变量中,调用用户空间的uevent_helper程序,这里指定为mdev程序;mdev程序会解析上报的事件进行不同的操作,对于add添加操作来说,会为其在/dev创建目录下创建对应的设备节点。

        因此,我们要做的是将我们的驱动维护在设备统一模型中,简单的办法就是注册一个class因为其是一个kset可以上报事件,在该类中以该设备号注册一个设备,这样这个设备号就被维护在系统中的,在设备注册的时候mdev会创建设备节点,注销的时候会删除设备节点。

另:关于字符设备的注册还有另一个老接口,但是不推荐使用:

#include <linux/fs.h>
​
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops);
​
static inline void unregister_chrdev(unsigned int major, const char *name);

该接口有以下几个特性:

  • 支持动态分配和静态分配:传入major0是采用动态分配,否则静态分配

  • 没有次设备号个数的入口参数,其默认将所有次设备号分配出去,这样会造成浪费

  • 函数内部已经做了关于cdev的事情,包括初始化和cdev_add,所以只用掉这一个函数即可完成字符设备初始化

2.字符设备的内涵

字符设备驱动的大概框架如下,用户空间对设备节点进行open,文件系统会找到文件对应的inode指针,查看是否有对应的cdev结构,如果没有,则会在cdev_map哈希表中使用主设备号进行查找,devrange进行匹配,将找到的cdev结构赋值到inode中,然后用inode更新filp的相关项,最后filp->ops即为驱动程序定义的cdev->ops,后期的readwrite等操作就直接调用filp->ops即可掉入到cdev->ops中。

下来我们从cdev结构以及cdev_add入手,看看字符设备是怎样在内核中运作的。

2.1 cdev结构

cdev结构中保存着ops操作函数接口,这个是其最重要的信息,其他域大都主要是用于维护和组织内核中的结构的。

"incldue/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 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 (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        ...
};

2.2cdev_add

这个函数是将cdev结构注册到内核中,因此很关键,是解决cdev在内核中如何运作的入口点。我们可以看到字符设备框架中有一个静态全局变量cdev_map,其是一个哈希表,所说的的cdev添加内核中就是指加入到这个哈希表中。

cdev_add函数:
"fs/char_dev.c"
​
// 静态全局的struct kobj_map结构,是一个哈希表,使用主设备号作为键值,dev和range进行匹配,查找其cdev结构
static struct kobj_map *cdev_map;
 
// 主要做的是将cdev加入到cdev_map哈希表中
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
        int error;
        
        // cdev结构赋值
        p->dev = dev;
        p->count = count; 
    
        // 将cdev结构加入到cdev_map全局字符设备哈希表中,则后来可以通过哈希的键值找到对应的cdev结构
        error = kobj_map(cdev_map, dev, count, NULL,
                         exact_match, exact_lock, p);
        if (error)
                return error;
    
        kobject_get(p->kobj.parent);
​
        return 0;
}

下面展开以下比较重点的地方:

struct kobj_map结构

struct kobj_map是一个哈希映射表,其有一个长度256probe的内存单元,使用拉链法解决哈希冲突;后文可以看到我们以主设备号取余作为索引可以指向对应的probe,再在probe这条链上匹配符合的probe项;每个probe可以在data中保存存储在哈希表中的数据。定义如下:

"drivers/base/map.c"
    
struct kobj_map {
        struct probe {
                struct probe *next;     // 解决哈希冲突问题,如果一个多个设备号都映射到这个地方就以一定的顺序链到这后面
                dev_t dev;              // cdev_add传入的起始设备号 以该值和下面的range一起匹配匹配,若查找的设备号位于[dev, dev+range-1]之间,则返回该probe中的data
                unsigned long range;    // cdev_add传入的count
                struct module *owner;
                kobj_probe_t *get;
                int (*lock)(dev_t, void *);
                void *data;             // 哈希表中存的数据,在后面可以看到它指向我们的cdev结构,很重要
        } *probes[255];
        struct mutex *lock;
};
kobj_map函数:

该函数将cdev结构存储在cdev_map哈希表中。其中:

  • 键值:主设备号,最终匹配使用设备号,看设备号是否在probe[dev, dev+range-1]区间里

  • 哈希函数:主设备号%256

  • 数据:cdev

  • 解决冲突:拉链法,最终在链上使用devrange进行匹配

    "drivers/base/map.c"
    ​
    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;   // 计算用到的主设备号有几个,多个主设备号要一次插入多个probe
            unsigned index = MAJOR(dev);    // 起始索引
            unsigned i;
            struct probe *p;                // 所要插入的probe们
    ​
            if (n > 255)
                    n = 255;
    ​
            p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL); // 动态分配内存
            if (p == NULL)
                    return -ENOMEM;
    ​
            // 初始化每一个probe,注意p->data是传入的data,在cdev_add函数中我们可以看到该参数是cdev指针
            for (i = 0; i < n; i++, p++) {
                    p->owner = module;
                    p->get = probe;
                    p->lock = lock;
                    p->dev = dev;       // dev保存起始设备号
                    p->range = range;   // range保存有几个设备,则范围为[dev, dev+range-1]
                    p->data = data;
            }
        
            // 对于每个probe,将其插入到哈希的映射位置链表上
            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;
            }
            mutex_unlock(domain->lock);
            return 0;
    }

综上,cdev_add函数的作用就很清楚了,其就是将cdev结构添加到dev_map全局哈希表中,使内核其他地方可以通过设备号寻找到cdev结构,从而能够访问到cdev结构中驱动这编写的ops接口函数。下面我们就看看内核是怎么把对设备节点处的文件操作转变为对驱动定义的cdev->ops的函数调用的。

2.3cdev->ops的调用

当文件被创建的时候,内核会创建相应的inode,字符设备是一个特殊的文件,其inode初始化的时候的相关工作如下:

init_special_inode函数:

可以看到如果是字符设备的话,操作函数集合被赋予def_chr_fops

"fs/inode.c"
    
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;   // 关键,其操作函数是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))
                ;       /* leave it no_open_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);
}
EXPORT_SYMBOL(init_special_inode);
def_chr_fops结构体

打开的时候调用chrdev_open

"fs/char_dev.c"
​
const struct file_operations def_chr_fops = { 
        .open = chrdev_open,        // 注意,文件打开会调用这个函数
        .llseek = noop_llseek, 
};
chrdev_open函数

该函数其实就是通过字符设备文件的设备号,来去cdev_map查找对应的ops,赋值给filp->ops,从而以后文件操作对应的ops都是驱动中定义的cdev->ops

"fs/char_dev.c"
​
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;  // inode->i_cdev存储该字符设备文件对应的cdev结构体,首次进来的时候为空,找到cdev后为其赋值以后编可直接通过该域访问
        if (!p) {           // 首次打开字符设备文件
                struct kobject *kobj;
                int idx;
                spin_unlock(&cdev_lock);
                kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);  // 传入设备号inode->i_rdev在哈希表中找到cdev,返回其kobj对象
                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;    // 赋值inode->i_dev为找到的cdev,下一次就可以不用找了
                        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;
        fops = fops_get(p->ops);        // 获取cdev中的ops
        if (!fops)
                goto out_cdev_put;
​
        replace_fops(filp, fops);       // 用cdev中的ops替换filp中的ops!!!
        if (filp->f_op->open) {
                ret = filp->f_op->open(inode, filp);    // 执行cdev->ops中的open
                if (ret)
                        goto out_cdev_put;
        }
    
       return 0;
​
 out_cdev_put:
        cdev_put(p);
        return ret;

最后,我们看一下是怎么在cdev_map中找到对应的cdevkobj的:

kobj_lookup函数

重点看注释的地方,发现匹配规则正如前文所说,会找到范围包含设备号的probe,返回它的data中的kobj

"drivers/base/map.c"
​
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);
        // 该循环通过主设备号找到了probes上对应的probe链表,然后遍历
        for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {
                struct kobject *(*probe)(dev_t, int *, void *);
                struct module *owner;
                void *data;
​
                // 范围判断,可以看到设备号不在[dev,dev+range-1]区间的话就继续查看下一个probe,反过来就是我们要查找设备号在其范围中的probe
                if (p->dev > dev || p->dev + p->range - 1 < dev)    
                        continue;
                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;
                if (p->lock && p->lock(dev, data) < 0) {
                        module_put(owner);
                        continue;
                }
                mutex_unlock(domain->lock);
                kobj = probe(dev, index, data);
                /* Currently ->owner protects _only_ ->probe() itself. */
                module_put(owner);
                if (kobj)
                        return kobj;
                goto retry;
        }
        mutex_unlock(domain->lock);
        return NULL;
}

3.设备号的认识

思考

之前一直没有思考过设备号究竟是什么,为什么字符设备一定要有设备号呢,而且还分什么主次设备号,今天在了解了字符设备的内涵之后,这些问题得以思考。

  • 设备号是什么主设备号作为cdev_map的键值,找到对应到256probe中的一项;设备号(dev range)作为设备的唯一标识,可以从这个probe对应的链表上匹配cdev_add时对应的cdev结构,是通过字符设备文件找到cdev结构的钥匙

  • 主设备号和次设备号的意义

    之前就听过,主设备号是驱动程序的唯一标识,主设备号和次设备号的结合是设备的唯一标识,这个其实是适用于早期版本的老接口,它仅有一个256长度的数组,数组用主设备号作为索引,有一个cdev结构,因而导致一个主设备号仅对应一个cdev结构,次设备号在查找cdev结构上根本没法出力,因而说主设备号是驱动程序的唯一标识。而因为设备节点是主次设备号确定的,因为设备号是设备的唯一标识,驱动程序编写者可以在驱动程序内部区分次设备号进行不同的操作。

    现在却不能说是这样的,关于我对代码的分析。我觉得,设备号是确定设备的唯一标识,因为它对应一个设备节点,这个还是一样的;而现在使用的cdev_map结构每一个probe可以使用devrange项来唯一的确定一次cdev_add操作,也就是说,驱动取决于cdev_add时候传入的cdev结构,一样的主设备号可以多次cdev_add进来不一样的cdev结构,实现不一样的驱动。总结来说,主设备号只决定在cdev_map的时候链接到哪一个项的链表中;每一次cdev_add确定一个驱动;如果cdev_add多个设备即cnt大于1,允许一个驱动对应多个设备,这里和老版本的次设备号意义相同。

设备号知识补充

设备号用于唯一的标识设备,是设备节点找到cdev字符设备驱动的唯一途径。所以在系统中,设备号必须唯一,且接受系统的管理,使用时需要申请分配和释放。设备号在linux中是dev_t类型的,它是个u32 32位无符号整型变量,因此可见设备号是有大小限制的,具体定义如下:

"include/linux/types.h"
​
typedef __kernel_dev_t          dev_t;
----typedef __u32 __kernel_dev_t;=

其分为主设备号和次设备号两部分,一般高12位为主设备号,低20位为次设备号,并且有一些宏用于进行设备号和主次设备号的转换,一般我们都是使用宏来进行操作,定义如下:

"include/linux/kdev_t.h"
​
#define MINORBITS       20
#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))

4.用户与内核通过文件进行交互的认识

驱动是写在内核态的,其最终是需要和用户态进行交互,接收用户态的控制,那么就需要一个接口,这个接口就是文件,文件的ops。所以,似乎所有的驱动好像基本上都需要套上一层框架,能为其提供文件接口的框架,例如字符设备驱动,到这里我对字符设备驱动在整个系统层次中的位置好像有一点理解了,它和块设备网络设备一起构成的驱动的上一层,为驱动提供文件接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值