天天看點

V4L2架構-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架構-control的資料結構

結構體之間的關系

圖裡面的關系是衆多關系中比較簡單的一種,其實在 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] 數組項内。用圖來表示就是:

V4L2架構-control的資料結構

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 位址指派給索引到的「桶」。這會造成怎樣一個存儲結果呢?一幅圖說明一下:

V4L2架構-control的資料結構

插入 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 執行個體化對象的位址,下一次如果又用到了的話就直接取用即可。

繼續閱讀