V4L2框架-control的資料結構
本篇文章寫一下 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 例項化物件的地址,下一次如果又用到了的話就直接取用即可。