【嵌入式】C語言高級編程-container_of宏(04)

00. 目錄

01. typeof 關鍵字

GNU C 擴展了一個關鍵字 typeof,用來獲取一個變量或表達式的類型。這裏使用關鍵字可能不太合適,因爲畢竟 typeof 還沒有被寫入 C 標準,是 GCC 擴展的一個關鍵字。爲了方便,我們就姑且稱之爲關鍵字吧。

通過使用 typeof,我們可以獲取一個變量或表達式的類型。所以 typeof 的參數有兩種形式:表達式或類型

示例:

int i ;

//typeof(i)等價於int 
//等價於 int j = 20;
typeof(i) j = 20;

//等價於int * a;
typeof(int *) a;

int f();

//typeof(f())等價於int
//等價於int k;
typeof(f()) k;

程序示例

#include <stdio.h>

int main(void)
{
    int i = 1;

    //等價於 int j = 6;
    typeof(i) j = 6;

    int *p = &j;
    
    //等價於int * q = &i;
    typeof(p) q = &i;

    printf("j = %d\n", j);
    printf("*p = %d\n", *p);
    printf("i = %d\n", i);
    printf("*q = %d\n", *q);

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
j = 6
*p = 6
i = 1
*q = 1

typeof高級用法

typeof (int *) y;   // 把 y 定義爲指向 int 類型的指針,相當於int *y;
typeof (int)  *y;   //定義一個執行 int 類型的指針變量 y
typeof (*x) y;      //定義一個指針 x 所指向類型 的指針變量y
typeof (int) y[4];  //相當於定義一個:int y[4]
typeof (*x) y[4];   //把 y 定義爲指針 x 指向的數據類型的數組
typeof (typeof (char *)[4]) y;//相當於定義字符指針數組:char *y[4];
typeof(int x[4]) y;  //相當於定義:int y[4]

02. typeof與宏結合

使用 typeof 關鍵字來直接獲取參數的數據類型。

#define MAX(x, y) ({    \
    typeof(x) _x = x;   \
    typeof(y) _y = y;   \
    (void)(&_x == &_y);  \
    _x > _y ? _x : _y;  \
    })

有了這個思路,我們同樣也可以將以前定義的一些宏通過這種方式改寫,這樣 SWAP 宏也可以支持多種類型的數據了。

linux-3.5/include/linux/kernel.h

/*
 * swap - swap value of @a and @b
 */
#define swap(a, b) \
    do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)

03. typeof在內核源碼中應用

關鍵字 typeof 在 Linux 內核中被廣泛使用,主要用在宏定義中,用來獲取宏參數類型。比如內核中,min/max 宏的定義:

linux-3.5/include/linux/kernel.h

/*
 * min()/max()/clamp() macros that also do
 * strict type-checking.. See the
 * "unnecessary" pointer comparison.
 */
#define min(x, y) ({                \
    typeof(x) _min1 = (x);          \
    typeof(y) _min2 = (y);          \
    (void) (&_min1 == &_min2);      \
    _min1 < _min2 ? _min1 : _min2; })

#define max(x, y) ({                \
    typeof(x) _max1 = (x);          \
    typeof(y) _max2 = (y);          \
    (void) (&_max1 == &_max2);      \
    _max1 > _max2 ? _max1 : _max2; })

#define min3(x, y, z) ({            \
    typeof(x) _min1 = (x);          \
    typeof(y) _min2 = (y);          \
    typeof(z) _min3 = (z);          \
    (void) (&_min1 == &_min2);      \
    (void) (&_min1 == &_min3);      \
    _min1 < _min2 ? (_min1 < _min3 ? _min1 : _min3) : \
        (_min2 < _min3 ? _min2 : _min3); })

#define max3(x, y, z) ({            \
    typeof(x) _max1 = (x);          \
    typeof(y) _max2 = (y);          \
    typeof(z) _max3 = (z);          \
    (void) (&_max1 == &_max2);      \
    (void) (&_max1 == &_max3);      \
    _max1 > _max2 ? (_max1 > _max3 ? _max1 : _max3) : \
        (_max2 > _max3 ? _max2 : _max3); })

/**
 * min_not_zero - return the minimum that is _not_ zero, unless both are zero
 * @x: value1
 * @y: value2
 */
#define min_not_zero(x, y) ({           \
    typeof(x) __x = (x);            \
    typeof(y) __y = (y);            \
    __x == 0 ? __y : ((__y == 0) ? __x : min(__x, __y)); })

內核中定義的宏跟我們上面舉的例子有點不一樣,多了一行代碼:

(void) (&_max1 == &_max2);

看起來是一句廢話,其實用得很巧妙!它主要是用來檢測宏的兩個參數 x 和 y 的數據類型是否相同。如果不相同,編譯器會給一個警告信息,提醒程序開發人員。

warning:comparison of distinct pointer types lacks a cast

讓我們分析一下,它是怎麼實現的:語句 &_max1 == &_max2 用來判斷兩個變量 _max1 和 _max2的地址是否相等,即比較兩個指針是否相等。&_max1 和 &_max2分別表示兩個不同變量的地址,怎麼可能相等呢!既然大家都知道,內存中兩個不同的變量地址肯定不相等,那爲什麼還要在此多此一舉呢?妙就妙在,當兩個變量類型不相同時,對應的地址,即指針類型也不相同。比如一個 int 型變量,一個 char 變量,對應的指針類型,分別爲 char * 和 int *,而兩個指針比較,它們必須是同種類型的指針,否則編譯器會有警告信息。所以,通過這種“曲線救國”的方式,這行程序語句就實現了這樣一個功能:當宏的兩個參數類型不相同時,編譯器會及時給我們一個警告信息,提醒開發者。

看完這個宏的實現,不得不感嘆內核的博大精深!每一個細節,每一個不經意的語句,細細品來,都能學到很多知識,讓你的 C 語言功底更加深厚。

04. container_of 宏分析

知道了 container_of 宏的用法之後,我們接着去分析這個宏的實現。作爲一名 Linux 內核驅動開發者,除了要面對各種手冊、底層寄存器,有時候還要應付底層造輪子的事情,爲了系統的穩定和性能,有時候我們不得不深入底層,死磕某個模塊,進行分析和優化。底層的工作雖然很有挑戰性,但有時候也是很枯燥的,不像應用開發那樣有意思。所以,爲了提高對工作的興趣,大家表面上雖然不說自己牛 X,但內心深處,一定要建立起自己的職位優越感。人不可有傲氣,但一定要有傲骨:我們可不像應用開發,知道 API 接口、讀讀文檔、完成功能就 OK 了。作爲一名底層開發者,要時刻記住,要和寄存器、內存、硬件電路等各族底層羣衆打成一片。從羣衆中來,到羣衆中去,急羣衆所急,想羣衆所想,這樣才能構建一個穩定和諧的嵌入式系統:穩定高效、上下通暢、運行365個日出也不崩潰。

container_of 宏的實現主要用到了我們上兩節所學的知識:語句表達式和 typeof,再加上結構體存儲的基礎知識。爲了幫助大家更好地理解這個宏,我們先複習下結構體存儲的基礎知識。

結構體內存佈局

我們知道,結構體作爲一個複合類型數據,它裏面可以有多個成員。當我們定義一個結構體變量時,編譯器要給這個變量在內存中分配存儲空間。除了考慮數據類型、字節對齊因素之外,編譯器會按照結構體中各個成員的順序,在內存中分配一片連續的空間來存儲它們。

程序示例

struct student
{
    int id;
    char sex;
    int age;
};

int main(void)
{
    struct student s = {1, 'M', 18};

    printf("&s = %p\n", &s);
    printf("&s.id = %p\n", &s.id);
    printf("&s.sex = %p\n", &s.sex);
    printf("&s.age = %p\n", &s.age);

    
    return 0;
}

執行結果

deng@itcast:~/tmp$ ./a.out  
&s = 0x7ffd3562d00c
&s.id = 0x7ffd3562d00c
&s.sex = 0x7ffd3562d010
&s.age = 0x7ffd3562d014

從運行結果我們可以看到,結構體中的每個成員變量,從結構體首地址開始,依次存放。每個成員變量相對於結構體首地址,都有一個固定偏移。比如 sex 相對於結構體首地址偏移了4個字節。age的存儲地址,相對於結構體首地址偏移了8個字節。

計算結構體成員在結構體中的偏移
一個結構體數據類型,在同一個編譯環境下,各個成員相對於結構體首地址的偏移是固定的。我們可以修改一下上面的程序,當結構體的首地址爲0時,結構體中的各成員地址在數值上等於結構體各成員相對於結構體首地址的偏移。

程序示例

struct student
{
    int id;
    char sex;
    int age;
};

int main(void)
{
    printf("&id: %p\n", &((struct student*)0)->id);
    printf("&id: %p\n", &((struct student*)0)->sex);
    printf("&id: %p\n", &((struct student*)0)->age);
    
    return 0;
}

執行結果

deng@itcast:~/tmp$ ./a.out  
&id: (nil)
&id: 0x4
&id: 0x8

因爲常量指針爲0,即可以看做結構體首地址爲0,所以結構體中每個成員變量的地址即爲該成員相對於結構體首地址的偏移。container_of 宏的實現就是使用這個技巧來實現的。

計算偏移的宏實現

那如何計算結構體某個成員在結構體內的偏移呢?內核中定義了 offset 宏來實現這個功能,我們且看它的定義:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

這個宏有兩個參數,一個是結構體類型 TYPE,一個是結構體的成員 MEMBER,它使用的技巧跟我們上面計算0地址常量指針的偏移是一樣的:將0強制轉換爲一個指向 TYPE 的結構體常量指針,然後通過這個常量指針訪問成員,獲取成員 MEMBER 的地址,其大小在數值上就等於 MEMBER 在結構體 TYPE 中的偏移。

container_of 宏的實現

有了上面的基礎,我們再去分析 container_of 宏的實現就比較簡單了。知道了結構體成員的地址,如何去獲取結構體的首地址?很簡單,直接拿結構體成員的地址,減去該成員在結構體內的偏移,就可以得到該結構體的首地址了。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:        the pointer to the member.
 * @type:       the type of the container struct this is embedded in.
 * @member:     the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

從語法角度,我們可以看到,container_of 宏的實現由一個語句表達式構成。語句表達式的值即爲最後一個表達式的值:

(type *)( (char *)__mptr - offsetof(type,member) );

最後一句的意義就是,拿結構體某個成員 member 的地址,減去這個成員在結構體 type 中的偏移,結果就是結構體 type 的首地址。因爲語句表達式的值等於最後一個表達式的值,所以這個結果也是整個語句表達式的值,container_of 最後就會返回這個地址值給宏的調用者。

因爲結構體的成員數據類型可以是任意數據類型,所以爲了讓這個宏兼容各種數據類型。我們定義了一個臨時指針變量 __mptr,該變量用來存儲結構體成員 MEMBER 的地址,即存儲 ptr 的值。那如何獲取 ptr 指針類型呢,通過下面的方式:

const typeof( ((type *)0)->member ) *__mptr = (ptr);

我們知道,宏的參數 ptr 代表的是一個結構體成員變量 MEMBER 的地址,所以 ptr 的類型是一個指向 MEMBER 數據類型的指針,當我們使用臨時指針變量 __mptr 來存儲 ptr 的值時,必須確保 __mptr 的指針類型是一個指向 MEMBER 類型的指針變量。typeof( ((type *)0)->member )表達式使用 typeof 關鍵字,用來獲取結構體成員 member 的數據類型,然後使用該類型,使用 typeof( ((type *)0)->member ) *__mptr 這行程序語句,就可以定義一個指向該類型的指針變量了。

還有一個需要注意的細節就是:在語句表達式的最後,因爲返回的是結構體的首地址,所以數據類型還必須強制轉換一下,轉換爲 TYPE* ,即返回一個指向 TYPE 結構體類型的指針,所以你會在最後一個表達之中看到一個強制類型轉換(TYPE *)。

05. container_of 宏應用

有了上面語句表達式和 typeof 的基礎知識,接下來我們就可以分析 Linux 內核第一宏:container_of。這個宏在 Linux 內核中應用甚廣。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:    the pointer to the member.
 * @type:   the type of the container struct this is embedded in.
 * @member: the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

GNU C 高端擴展特性的綜合運用,宏中有宏,不得不佩服內核開發者這天才般地設計。那這個宏到底是幹什麼的呢?它的主要作用就是:根據結構體某一成員的地址,獲取這個結構體的首地址。根據宏定義,我們可以看到,這個宏有三個參數,它們分別是:

  • type:結構體類型
  • member:結構體內的成員
  • ptr:結構體內成員member的地址

也就是說,我們知道了一個結構體的類型,結構體內某一成員的地址,就可以直接獲得到這個結構體的首地址。container_of 宏返回的就是這個結構體的首地址。

比如現在,我們定義一個結構體類型 student:

程序示例

#include <stdio.h>

struct student
{
    int id;
    char sex;
    int age;
};

int main(void)
{
    struct student s;

    printf("&s = %p\n", &s);

    printf("&s = %p\n", container_of(&s.sex, struct student, sex));

    return 0;
}

執行結果

deng@itcast:~/tmp$ gcc 7.c  
deng@itcast:~/tmp$ ./a.out  
&s = 0x7ffc101be72c
&s = 0x7ffc101be72c

在這個程序中,我們定義一個結構體類型 student,然後定義一個結構體變量 s,我們現在已經知道了結構體成員變量 s.sex的地址,那我們就可以通過 container_of 宏來獲取結構體變量 s的首地址。

這個宏在內核中非常重要。我們知道,Linux 內核驅動中,爲了抽象,對數據結構體進行了多次封裝,往往一個結構體裏面嵌套多層結構體。也就是說,內核驅動中不同層次的子系統或模塊,使用的是不同封裝程度的結構體,這也是 C 語言的面向對象思想。分層、抽象、封裝,可以讓我們的程序兼容性更好,適配更多的設備,但同時也增加了代碼的複雜度。

我們在內核中,經常會遇到這種情況:我們傳給某個函數的參數是某個結構體的成員變量,然後在這個函數中,可能還會用到此結構體的其它成員變量,那這個時候怎麼辦呢?container_of 就是幹這個的,通過它,我們可以首先找到結構體的首地址,然後再通過結構體的成員訪問就可以訪問其它成員變量了。

程序示例

struct student
{
    int id;
    char sex;
    int age;
};

int main(void)
{
    struct student s = {1, 'M', 18};

    int *p = &s.age;

    struct student *p1 = NULL;

    p1 = container_of(p, struct student, age);

    printf("&s = %p\n", &s);
    printf("id:%d\n", p1->id);
    printf("sex:%c\n", p1->sex);
    printf("age: %d\n", p1->age);


    return 0;
}

執行結果

deng@itcast:~/tmp$ ./a.out  
&s = 0x7ffd1ca8e6bc
id:1
sex:M
age: 18

在這個程序中,我們定義一個結構體變量 s,知道了它的成員變量 age 的地址 &stu.age,我們就可以通過 container_of 宏直接獲得 s結構體變量的首地址,然後就可以直接訪問 s結構體的其它成員 s->id和 s->sex。

06. 附錄

參考:C語言嵌入式Linux高級編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章