1. 程式人生 > >V4L2框架-control的資料結構

V4L2框架-control的資料結構

[GithubPages]

本篇文章寫一下 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 數量。
  • keystruct 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;
  1. 先根據需要查詢的 control 的 id 來獲取「桶」的索引號。
  2. cached 裡面找一下,說不定一次性就找到了,如果找到的話就直接返回了。
  3. 否則的話到指定的「桶」裡面從頭到尾遍歷一遍「桶」內部的 refs。
  4. 如果找到了,那麼就把新找到的 refs 地址快取在 cached 成員裡面,然後返回。

資料結構

一個思考:從上面的整篇描述來看,其實這個非常像排序演算法裡面的「桶排序」,但是也有不少不同的地方。

  • 桶排序中桶內部的資料也是有序的,而 contorl 框架裡面為了簡化程式碼複雜度,桶內部沒有進一步排序,況且也沒必要進行桶內的排序。
  • 桶排序的內部數字是有重複的,或許有可能是負數,但是 contorl 框架限定了其 id 值是非負整數,並且不會重複,是全域性唯一的。
  • 它們的目的是相同的,都是排序之後方便查詢。

總之,contorl 框架裡面的這個做法可以看作是一個簡化版的「桶排序」,由於本身 contorl 的數量不太可能非常龐大,並且資料的連續性也比較強,所以一個沒有那麼“複雜”的簡化版「桶排序」演算法就能很好的滿足插入與查詢的速度和空間消耗之間的平衡。

從上面也可以看出在 contorl 框架裡面,一個個的例項化 v4l2_ctrl 物件被按照不同的方式建立了多套索引連結串列或陣列結構。比如:

  1. v4l2_ctrl_handler 裡面的 ctrls 成員便將所有的例項化 control 物件按照先來後到的順序串成一個雙向連結串列,連結串列節點的型別就是 struct v4l2_ctrl 型別的指標。
  2. v4l2_ctrl_handler 裡面的 ctrl_refs 成員將所有的例項化 struct v4l2_ctrl_ref 物件按照其 id 值從小到大串聯在一起,連結串列節點的型別是 struct v4l2_ctrl_ref 指標。
  3. v4l2_ctrl_handler 裡面的 buckets 成員將眾多的 contorl 分成一個個的「桶」,通過對 contorl 的 id 進行取模運算來決定放在哪一個桶裡面,桶內部的排序遵循先來後到原則。

另外還有一個小的優化就是在 v4l2_ctrl_handler 裡面有一個 cached 成員,快取上一次使用到的 contorl 例項化物件的地址,下一次如果又用到了的話就直接取用即可。


想做的事情就去做吧