0
点赞
收藏
分享

微信扫一扫

V4L2框架-control的数据结构

践行数据分析 2022-03-12 阅读 64
数据结构

话不多说,直接进入正题,本文章是基于 linux-4.4.138 内核来探讨的。

几个结构体之间的关系
struct v4l2_ctrl:control 的结构体抽象,一个 control 就用一个实例化的 v4l2_ctrl 变量来表示。
struct v4l2_ctrl_ref:一个实例化的 v4l2_ctrl 的引用,可以看到该结构体里面包含了一个 struct v4l2_ctrl * 类型的指针变量成员,该指针成员指向的就是与之一一对应的 v4l2_ctrl 实例化对象。
struct v4l2_ctrl_handler:control 的集合,就比如一个设备它有很多个 control,这些众多的 contorl 被实例化为一个个的 v4l2_ctrl 变量,然后一一对应一个 v4l2_ctrl_ref 实例化对象,最后所有的 v4l2_ctrl_ref 都归属到同一个 v4l2_ctrl_handler 实例化对象中,并且受到 v4l2 框架与设备驱动的管理。

结构体之间的关系
结构体之间的关系
图里面的关系是众多关系中比较简单的一种,其实在 v4l2_ctrl_ref 数组内部还有其它「乱七八糟」的关系,这些后面说到。

 

control 集合的初始化
control 集合的实例化表示就是 struct v4l2_ctrl_handler,之前的文章里面有说过,就不再详述了,该实例一般需要调用一个函数进行初始化,其名曰:v4l2_ctrl_handler_init。这个函数在代码里面是一个宏定义,我就选取宏定义的实体 v4l2_ctrl_handler_init_class 函数来进行说明。

该函数的参数有这么几个:

hdl:struct v4l2_ctrl_handler * 类型,指向将要初始化的 control 集合的实例化对象。
nr_of_controls_hint:预设的 control 的数量,由用户传入,一般来说,某个模块需要的 control 对于驱动编写者来说都是事先知道的,这个值就是事先规划好的该模块应该有的 control 数量。
key:struct lock_class_key * 类型,是内核用来实现死锁检测机制的关联结构体之一,具体的原理没有去深究过,但是可以在代码中看到它与 hdl->lock 进行了某种关联,也就是说它是为了检测 hdl-lock 的死锁而服务的。
name:只读字符串,表示该死锁检测的名字。


一般情况下,后面两个参数都为空,函数实体:

/* Initialize the handler */
int v4l2_ctrl_handler_init_class(struct v4l2_ctrl_handler *hdl,
                 unsigned nr_of_controls_hint,
                 struct lock_class_key *key, const char *name)
{
    hdl->lock = &hdl->_lock;
    mutex_init(hdl->lock);
    lockdep_set_class_and_name(hdl->lock, key, name);
    INIT_LIST_HEAD(&hdl->ctrls);
    INIT_LIST_HEAD(&hdl->ctrl_refs);
    hdl->nr_of_buckets = 1 + nr_of_controls_hint / 8;
    hdl->buckets = kcalloc(hdl->nr_of_buckets, sizeof(hdl->buckets[0]),
                   GFP_KERNEL);
    hdl->error = hdl->buckets ? 0 : -ENOMEM;
    return hdl->error;
}


最后两个参数就略过不表,因为一般情况也没用到,跟本文的主题无关。该函数里面有一个语句:hdl->nr_of_buckets = 1 + nr_of_controls_hint / 8; 这表明「桶」的数量 nr_of_buckets 是 1 加上计划的 control 数量除以 8,这里就可以猜测到一个「桶」里面将来最多可以放置 8 个 controls。

再看下 buckets 成员的类型,是 struct v4l2_ctrl_ref ** 类型的,表明这个参数是用来存放「桶」的地址,一共有 nr_of_buckets 个桶会被依次记录到 buckets[n] 数组项内。用图来表示就是:

control buckets
control 的 buckets 排列
从前面可以得出,每一个桶里面最多存放 8 个成员(control),至于为什么是 8,我估计是根据内核定义的总的 conrol 值理论计算加上实际实践的结果得到的,我也没有去验证它是不是最优的数量,毕竟如果用户自定义了一大堆的 control 的话,这个 8 还真不一定是最优解。

 

在 struct v4l2_ctrl_handler 结构体类型内部还有一个 cached 成员,其类型是 struct v4l2_ctrl_ref *,这是一个非常小的优化点,它里面存放的是最后一次使用到的 contorl,就是在用户调用 ioctl 的时候首先找下这个里面存放的 control 值是不是用户想要用的值,如果是的话直接拿来用就行,是非常简单的一个优化。

新增一个 control
现在看下在 V4L2 框架内部的代码里面是如何新增一个 control 的,拿其中一个函数来举例,就是它了:v4l2_ctrl_new_std,但是不论是什么菜单类型的,自定义菜单类型还是啥的,最终都会调到一个地方,那就是 handler_new_ref 函数,过程就不说了,很简单追踪下就好。这个函数在 v4l2_ctrls.c 文件里面,去找找吧。

函数的最开始有这么几行小字:

u32 id = ctrl->id;
u32 class_ctrl = V4L2_CTRL_ID2CLASS(id) | 1;
int bucket = id % hdl->nr_of_buckets;    /* which bucket to use */

第一个就不说了,第二个是把 ctrl 转换为一个 class 值,按照 V4L2 的说法,每 0xFFFF 个 control 当中就有一个 class,它应该就是一个分类吧,凡是 0xNNNNN0001(N就是任意值) 的 ID 都是一个 class,比如 V4L2_CID_USER_CLASS 就是 0x00980001。最后一个是根据 contorl id 的值找到对应的「桶」,分别放在不同的「桶」里面方便查找,注意这个计算的方式是取模运算,而不是除法运算,按照通常的理解应该是除法,这其实是一个优化。

假设说现在有八个 id 值是从 1~8 的 contorl,如果是采用除法来进行「桶」查找的话,这八个 contorl 都会被放到第一个「桶」里面(下标为0),这样子就失去了「桶」的优化作用(想象一下桶排序的原则),而如果是取模的话这八个 contorl 就会均匀分布在八个「桶」里面,这就有点桶排序的意思了,查找的时候也非常快。但是也有一种情况,那就是八个 contorl id 值分别为 1,9,17,25,33,41,49,57,那就会全部被放到第二个「桶」里面(下标为1),不过实际使用当中更倾向于连续的 contorl 更加常见,这就是 contorl 类的意义,类使得关联性很强(功能类似)的 contorl 连续分布在一个 id 值段,这样高概率出现连续 id 值的 contorl 被用户使用。

不关心的先略过,下面有一个插入的代码:

/* 如果 ctrl_refs 为空或者新的 contorl 的 id 值比 ctrl_refs 链表尾部的 contorl
* id 值还要大,那就把新的 [v4l2_ctrl_ref] 实例化对象 [new_ref] 插入到 [ctrl_refs] 链表结尾。
*/
if (list_empty(&hdl->ctrl_refs) || id > node2id(hdl->ctrl_refs.prev)) {
    list_add_tail(&new_ref->node, &hdl->ctrl_refs);
    goto insert_in_hash;
}

/* 否则遍历 [ctrl_refs] 链表,直到找到第一个 id 值比将要插入的 contorl 的 id 大的
* [v4l2_ctrl_ref] 实例化对象 [ref],然后把新的 [new_ref] 插入到这个 [ref] 前面。
*/
list_for_each_entry(ref, &hdl->ctrl_refs, node) {
    if (ref->ctrl->id < id)
        continue;
    /* Don't add duplicates */
    if (ref->ctrl->id == id) {
        kfree(new_ref);
        goto unlock;
    }
    list_add(&new_ref->node, ref->node.prev);
    break;
}

由此可见,ctrl_refs 链表中的实例化对象 new_ref (也就是代表了一个 contorl)都是按照 id 值升序排列的。完成了 v4l2_ctrl_handler->ctrl_refs 链表的插入动作之后,还有最后一步,那就是把新的 ctrl_ref 放入到一个哈希表中,也就是前面新建的多个「桶」,代码如下:

insert_in_hash:
    /* Insert the control node in the hash */
    new_ref->next = hdl->buckets[bucket];
    hdl->buckets[bucket] = new_ref;

它的作用是先把开头使用 id % hdl->nr_of_buckets 索引到的「桶」里面的第一项 v4l2_ctrl_ref 地址赋值给新的new_ref 的 next 成员,然后把新的 new_ref 地址赋值给索引到的「桶」。这会造成怎样一个存储结果呢?一幅图说明一下:

插入 contorl
插入 contorl
「桶」内部的 v4l2_ctrl_ref 实例化对象使用 v4l2_ctrl_ref 的 next 成员进行链接,它没有大小的顺序,只按照先来后到的顺序进行排列。

 

查找 control
查找的操作就简单很多了,代码如下(在 find_ref 函数内部截取):

bucket = id % hdl->nr_of_buckets;

/* Simple optimization: cache the last control found */
if (hdl->cached && hdl->cached->ctrl->id == id)
    return hdl->cached;

/* Not in cache, search the hash */
ref = hdl->buckets ? hdl->buckets[bucket] : NULL;
while (ref && ref->ctrl->id != id)
    ref = ref->next;

if (ref)
    hdl->cached = ref; /* cache it! */
return ref;

先根据需要查找的 control 的 id 来获取「桶」的索引号。
去 cached 里面找一下,说不定一次性就找到了,如果找到的话就直接返回了。
否则的话到指定的「桶」里面从头到尾遍历一遍「桶」内部的 refs。
如果找到了,那么就把新找到的 refs 地址缓存在 cached 成员里面,然后返回。
数据结构
一个思考:从上面的整篇描述来看,其实这个非常像排序算法里面的「桶排序」,但是也有不少不同的地方。

桶排序中桶内部的数据也是有序的,而 contorl 框架里面为了简化代码复杂度,桶内部没有进一步排序,况且也没必要进行桶内的排序。
桶排序的内部数字是有重复的,或许有可能是负数,但是 contorl 框架限定了其 id 值是非负整数,并且不会重复,是全局唯一的。
它们的目的是相同的,都是排序之后方便查找。
总之,contorl 框架里面的这个做法可以看作是一个简化版的「桶排序」,由于本身 contorl 的数量不太可能非常庞大,并且数据的连续性也比较强,所以一个没有那么“复杂”的简化版「桶排序」算法就能很好的满足插入与查找的速度和空间消耗之间的平衡。

从上面也可以看出在 contorl 框架里面,一个个的实例化 v4l2_ctrl 对象被按照不同的方式建立了多套索引链表或数组结构。比如:

v4l2_ctrl_handler 里面的 ctrls 成员便将所有的实例化 control 对象按照先来后到的顺序串成一个双向链表,链表节点的类型就是 struct v4l2_ctrl 类型的指针。
v4l2_ctrl_handler 里面的 ctrl_refs 成员将所有的实例化 struct v4l2_ctrl_ref 对象按照其 id 值从小到大串联在一起,链表节点的类型是 struct v4l2_ctrl_ref 指针。
v4l2_ctrl_handler 里面的 buckets 成员将众多的 contorl 分成一个个的「桶」,通过对 contorl 的 id 进行取模运算来决定放在哪一个桶里面,桶内部的排序遵循先来后到原则。
另外还有一个小的优化就是在 v4l2_ctrl_handler 里面有一个 cached 成员,缓存上一次使用到的 contorl 实例化对象的地址,下一次如果又用到了的话就直接取用即可。
 

举报

相关推荐

0 条评论