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
上面
(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
)
- ptr: 結構體變數的指標(比如上面的
-
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
歡迎補充指正.