本篇文章写一下 V4L2 里面的众多 control 的组织方式,也就是它的数据结构。主要就是新建的 control 是如何存放的,以及在需要用到的时候如何查找。里面用到了类似于「桶」的概念,没错就是「桶排序」里面的那个桶,这种比较特殊的小优化为查找速度提供了不少的帮助。
话不多说,直接进入正题,本文章是基于 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] 数组项内。用图来表示就是:
从前面可以得出,每一个桶里面最多存放 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
地址赋值给索引到的「桶」。这会造成怎样一个存储结果呢?一幅图说明一下:
「桶」内部的 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 实例化对象的地址,下一次如果又用到了的话就直接取用即可。