就內核的container_of()函數,發表一下個人見解。
在內核中,多處地方用到container_of()這個函數(如內核鏈表),
其定義在內核代碼 include/linux/kernel.h中:
/**
* 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) );})
作用---根據結構體成員member的地址ptr,求出結構體type的起始地址。
它是如何實現的呢?下面一步一步來拆解。
下面一步一步來拆解。
一、offsetof()函數
其中的offsetof()函數,在include/linux/stddef.h中,定義如下:
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
#endif /* __KERNEL__ */
只需看這行:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
這是一個宏函數,其作用---求成員MEMBER在結構體TYPE中的偏移。
(TYPE *)0這種奇葩寫法是什麼意思呢?
如有以下結構體:
struct demo
{
char c;
short s;
int i;
};
其中各成員的對齊方式如下:(有疑問者請自行了解結構體字節對齊規則)
char - 1字節對齊;short - 2字節對齊;int - 4字節對齊;
結構體在內存中地址圖示(p爲結構體的首地址)
由圖可知:某成員相對結構體的偏移=該成員地址-結構體首地址,例:偏移量(s) = 地址(s) - p
由此,想象將該結構體整體移到0地址處呢?
那麼p=0,成員偏移量=成員的址址,問題秒簡化。
如何將結構體整體移至0地址處?--- 將0地址進行類型強轉換,即(struct demo *)0,這樣是虛擬0地址處有struct demo結構體,只可讀而不可寫,因爲C語言是允許在任何地址進行任何類型轉換,讀動作是沒問題的,而寫動作責要謹慎且後果自負。
由上,成員s的偏移量=s的地址:&((struct demo *)0)->s,再將其強轉化爲size_t類型:
(size_t)&((struct demo *)0)->s;
這樣,是不是就跟offsetof()這個宏函數一樣了。
二、container_of()函數
1、邏輯圖示
container_of(ptr, type, member)函數的作用:由結構體成員member的地址ptr,求出結構體type的起始地址。
其參數:
* @ptr: 指向成員member的指針(或member的址址)
* @type: 包含成員member的結構體類型
* @member: 成員的名字
由圖示更好理解:
container_of()函數所實現的就是求出圖中的“?”,即結構體的起始地址。
已知成員member的地址ptr,求結構體的首地址,只需知道ptr與結構體首地址的偏移量,而這恰恰就是offsetof()宏函數了。
那麼,結構體的首地址 = ptr - offsetof(type,member) ,加上類型轉換等優化,變成:
(type *)(ptr - offsetof(type,member) );
就這樣,收工?
2、優化/學習/嚴謹
與container_of()原定義有點出入,其多出一行:
const typeof( ((type *)0)->member ) *__mptr = (ptr);
typeof()函數:獲取變量(member)的類型;
這行的意思:定義一個與member同類型的指針__mptr指向ptr,後面用__mptr而不直接用ptr了。
這行好像是多此一舉,並無大用???
原因看這裏:若container_of()的參數ptr有誤,即ptr與member的類型不匹配,編譯時便會提示warnning信息,以引起注意;若無此語句,便無warning引起注意,後果可大可小!!!這便是編程的嚴謹性體現之處了。
3、宏定義小知識
注意到container_of()宏函數的外圍是用({ })雙括號包起來的,這有什麼用呢?
首先,要明白宏定義只是在預處理階段作簡單的文本替換。
()小括號:
()的運算優先級高,能將此宏包起來作爲一個整體而不被拆分,以免造成意料之外的影響。一般宏定義含多級運算或多表達式等都建議用()包起來。
{}大/花括號:
其作用較複雜,(個人觀點,歡迎糾正)
1、語法要求:假如去掉{},調用container_of()則變成:
(
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );
)
細數()是否成對出現,發現一對()會被“ ; ”隔斷,猶如:( foo(); ) 這樣,語法上是不允許一對()被“ ; ”隔斷的。加上{}則表示這應當成一個整體(複合語句)。
2、複合語句:{}將多個語句包起來,在程序中應看作爲一條語句執行,且以最後一條語句的執行結果作爲其值,可用於賦值。
3、限定作用域:{}裏面定義的變量只在{}中有效,若無{}且多次調用該宏,則會導致變量被重複定義。
4、形式作用:宏函數最好要有函數的樣子,就是用{}包起來表示,使更像函數。
三、結構體偏移量的應用
結構體中的偏移量相關應用,
1、某成員在結構體中的相對偏移:參考offsetof()函數,巧用0地址指針,如(size_t)&((struct demo *)0)->s,訪問某成員時可避免寫死相對偏移量,便於修改/維護/移植等。
(size_t)&((struct demo *)0)->s // 結構體某成員在結構體中的偏移量
2、知道結構體某成員的地址,想要得到結構體的首地址:參考container_of(),如內核鏈表中就有應用。
希望此博文能對你有積極影響,水平有限,歡迎指點。