1. 程式人生 > >linux核心之offset_of和container_of理解

linux核心之offset_of和container_of理解

無意間在騰訊課堂上看到有個老師講解linux核心連結串列,開始就講這兩個巨集。
這篇文章主要是為了記錄對這兩個巨集的使用和理解。

測試環境:

  • win10 64bit 家庭版
  • gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)

為了描述方便, 示例程式碼中會使用這樣的結構:

typedef struct Node {
    double d;
    int i;
} Node;

下面正式開始

巨集定義概述

offsetof:
巨集定義:

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

功能: 獲取結構體中成員變數MEMBER相對於結構體的地址偏移量

container_of:
巨集定義:

#define container_of(ptr, type, member) ({                  \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

功能: 獲取結構體成員所在的結構體變數的地址

為了有一個比較深刻的理解, 用程式碼逐步演示.

offsetof

  • (TYPE *)0理解
    這寫法比較少見, 因為平時寫得最多的可能是這樣:

    #include <stdio.h>
    
    int main()
    {
        // 地址:低 ---------------> 高
        // 小端儲存(16進位制):  45, 44, 43, 42
        int i = 0x42434445;
    
        int* pi = &i;
        char* pc = (char*)pi; // 型別轉換
    
        // 61ff24, 61ff24
        printf("%x, %x\n", pi, pc);
    
        // E, D, C, B
        printf
    ("%c, %c, %c, %c\n", *pc, *(pc + 1), *(pc + 2), *(pc + 3)); return 0; }

    上面(char*)pi是將一個int*轉成char*, 什麼意思呢?
    就是將pi處開始的記憶體用char型別來解釋, i變數佔了4個位元組的記憶體,
    其值按小端儲存分別為(16進位制): 45, 44, 43, 42, 此時pc的值就是45所在的記憶體地址,
    即*pc的值為'E'(ASCII為45), 從執行結果也可以看出.

    再來看(TYPE *)0, 假想0是某一個int型別指標p的值, 這不就是將p轉換成TYPE型別的指標嗎?
    有人可能說, 0地址處不是作業系統佔用了嗎? 這樣會不會報錯?
    不會, 因為只做了轉換, 並沒有向0地址處進行讀寫操作.
    那麼, (TYPE *)0相當於0地址處存了一個TYPE型別的結構體, &((TYPE *)0)->MEMBER就是求MEMBER成員的地址.
    再回頭看看offsetof巨集, 就是為了求出變數MEMBER相對於結構體的地址偏移量.

  • offsetof使用示例:

    Node n = {
        .d = 3.14,
        .i = 6,
    };
    
    Node* pNode = NULL;
    // Node中: d在第一個, i在d後面定義, 成員i的偏移為一個double的長度, 
    assert(offsetof(Node, i) == sizeof(double));
    // d定義在Node中第一個位置, 所以偏移為0
    assert(offsetof(Node, d) == 0);
    assert(&n.i == (char*)&n + sizeof(double));
    // 事實上, 由於成員d偏移為0, 所以d的地址和n的地址一樣
    assert(&n.d == &n);
    // i的地址偏移其實就是一個double
    assert(&n.i == (char*)&n + sizeof(double));
    

這個offsetof好像沒啥用啊? 平時不用它也工作得很好啊.
其實它是為了container_of服務的, 直接用它的情況確實很少.

container_of

  • container_of(ptr, type, member)
    先說說三個引數:

    • ptr: 結構體變數的指標(比如上面的 &n.i)
    • type: 結構體型別(如Node)
    • member: 結構體成員名(如i)
  • typeof 關鍵字
    typeof是GNUC的擴充套件語法, 用它可以獲取變數的型別, 如:

    int j = 0;
    typeof(j) j2 = 1;
    printf("%d\n", j2); // 1
    
  • ({})
    container_of定義中({stmt1; stmt2;})是GNUC的擴充套件語法, 其值是最後一個語句的運算結果. 如:

    int a = ({
        int i = 10;
        int j = 100;
        i > j ? i : j;
    });
    printf("%d\n", a); // 100
    
  • container_of實現原理

    #define container_of(ptr, type, member) ({                  \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})
    

    其中 const typeof( ((type *)0)->member ) *__mptr = (ptr);是為了做型別轉換, 暫時不管, 後面會說到.
    因為正常情況下我們給第一個引數傳遞的就是&n.i這樣值, 也就是對應成員變數的指標.
    那麼container_of最後的結果其實就相當於:
    (type *)( (char *)ptr- offsetof(type,member) );
    用成員變數的地址, 減去這個成員變數相對於其所在結構體的地址偏移, 不就是其所在結構體變數的地址嗎?
    看程式碼更好懂:

    Node n = {
        .d = 3.14,
        .i = 6,
    };
    
    Node* pNode = NULL;
    // 通過n.i的地址反推n的地址
    pNode = container_of(&n.i, Node, i);
    assert(pNode == &n);
    assert(pNode->i == 6);
    

嗯! container_of其實也好簡單嘛. 但是有些細節還是要說下.

  • 感覺沒用的第一句(const typeof... = (ptr))
    對於const typeof( ((type *)0)->member ) *__mptr = (ptr); 這個在正常使用時好像看不出來有什麼用.
    其實它的作用時,在編譯時就能知道ptr與member的型別是否匹配.

    char tmp = 'a';
    char* ptmp = &tmp;
    pNode = container_of(ptmp, Node, i);
    // assert(pNode == &n); // assert failed
    

    這樣在編譯時會產生警告:

    warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]|

    當然此時計算的pNode的值與&n不相等.

  • 為啥是(char )
    有人會問(包括我)為啥(char *)__mptr - offsetof(type,member)這裡要用char
    強轉一下.
    想像一下, 如果__mptr此時是int*, 然後offsetof(type,member)的值為4, 那麼就相當於回退了16, 而預期的是回退4.

    由於我的測試環境中, 一個int佔4個位元組, 一個double佔8個位元組, 則i相對於Node的偏移為8,
    所以&n.i - 2相當於i的地址值減8.

    assert(&n.i - 2 == &n); // success
    assert(&n.i - sizeof(double) == &n);  // failed
    

    所以char*強轉是為了保證回退時步長為1. 可以將原巨集中的(char *)__mptr - offsetof(type,member)改成__mptr - offsetof(type,member)試試就知道了.

注意

  • offsetof也提醒我們, struct一旦定義, 成員變數位置就不要亂動了, 可能會引起不必要的問題.
  • 記憶體對齊問題也要注意

參考:

https://ke.qq.com/course/223662#term_id=100264105
https://blog.csdn.net/npy_lp/article/details/7010752

歡迎補充指正.