簡介 container_of(ptr, type, member)是內核中的經典函數之一。該函數的作用是:根據結構體中一個成員的地址,找到結構體的地址。這個函數是內核實現面向對象的基礎設施,且最近在學習中經常見到這個函數,於是筆者在內核中查看了該函數的實現,故在此記錄。本文原本是為了展示conta ...
簡介
container_of(ptr, type, member)
是內核中的經典函數之一。該函數的作用是:根據結構體中一個成員的地址,找到結構體的地址。這個函數是內核實現面向對象的基礎設施,且最近在學習中經常見到這個函數,於是筆者在內核中查看了該函數的實現,故在此記錄。本文原本是為了展示container_of
的實現,但寫著寫著,發現有些內建函數與GNU C拓展的使用,所以就順便查了資料,也一併記錄於此,寫得比較亂,請大家諒解。
基礎知識
結構體在記憶體中的分佈,是按照成員的順序分配記憶體,同時保持記憶體對齊的要求
實現分析
源碼
該函數在5.17.5中的實現在include/linux/container_of.h
中
5.16之前,這個巨集都被放在
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) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), ((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
參數
- ptr:成員指針
- type:結構體類型
- mem:成員在結構體里的名稱
第一行:賦值
將傳入的成員變數的地址,轉換為void *
類型,並賦給另一個值。這個操作筆者沒有理解,所以找了以前版本的源碼來進行分析,在2.6.23里,他的實現是這樣的:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
這個版本中,第一行的作用其實相當於賦值+檢查,考慮如果傳進來的指針類型和member不一致,編譯器會報warning。
在使用查看了相關的log之後,發現這個巨集是在提交c7acec713d14c
被改變的,改變的原因是:如果結構體內引入了一個非const數組成員,那麼這個指針就會產生變數賦值給常量的問題,這會在gcc-4.9中產生一個warning: initialization from incompatible pointer type
。這一筆改動抽離出了類型檢查,但__mptr
仍留在原處,筆者實在不清楚這個操作的深意,又或許只是歷史遺留問題?
第二行:檢查
static_assert(__same_type(*(ptr), ((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
這個地方在5.16後修改成static_assert,之前使用的是
BUILD_BUG_ON()
這個巨集,他和static_assert被定義在同一個文件里,感興趣的朋友們可以去看一看相關實現,根據commit message顯示,使用static_assert
可以給出更加直接的錯誤提示,並且在理論上可以提升一點點的build速度(commit message里寫了a tiny bit faster
)
一個斷言,用於檢查ptr
和member
的類型一致性。這個斷言函數static_assert()
我們先放在一邊,來分析一下這個斷言的第一個參數:內部使用了__same_type()
這個巨集,來看看這個巨集的實現:
/* Are two types/vars the same type (ignoring qualifiers)? */
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
這個巨集使用了兩個函數:__builtin_types_compatible_p()
和typeof()
。
typeof()
想必大家都比較熟悉了,它是一個GNU C的拓展,作用是獲取變數的類型。文檔地址:https://gcc.gnu.org/onlinedocs/gcc/Typeof.html
__builtin_types_compatible_p(type1, type2)
是一個GNU C的內建函數,用於比較兩個類型是否相等,若相等則返回1,不等則返回0。需要註意的是,這個函數的參數並不是表達式,而是變數類型,所以需要使用typeof()
先取得變數類型後再傳入。文檔地址:https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
最後我們再來看一看static_assert()
,這個函數的實現位於include/linux/build_log.h
,源碼如下:
#define static_assert(expr, ...) __static_assert(expr, ##__VA_ARGS__, #expr)
#define __static_assert(expr, msg, ...) _Static_assert(expr, msg)
關於C巨集定義中#
符號的用法,可以總結為以下兩點:
- 前加
##
,轉換為合法標識符
#define to_symbol(x) T_##x
// 下麵這句等效於 int T_1 = 10;
int to_symbol(1) = 10;
- 前加
#
,轉換為字元串
#define to_string(x) #x
// 下麵這句等效於 "a+b+c"
to_string(a+b+c);
那##__VA_ARGS__
又是什麼呢?它的功能有兩個:
- 如果可變參數列表為空,使編譯器忽略它以及它前面的逗號
- 如果可變參數列表不為空,編譯器將其替換為可變參數列表
接著再來看一看_Static_assert(expr, msg, ...)
,這是一個C11特性,用來在編譯時測試expr
的正確性,如果正確則什麼都不會發生,如果錯誤,則列印指定信息msg
。文檔地址:https://www.gnu.org/software/gnulib/manual/html_node/assert_002eh.html
綜上所述,第二行的作用就是:判斷傳入的ptr
和member
(或者void
)是否為同一類型,若否,則列印"pointer type mismatch in container_of()"
第三行:定址
這一行真正用於獲取結構體的地址。
((type *)(__mptr - offsetof(type, member)));
看上去很簡單!就是用傳進來的成員變數地址值減去它在結構體里的偏移值嘛!
邏輯上來講確實很簡單,但是如何實現呢?如何在不同的對齊下讓這個函數均能成功運行呢?讓我們帶著這個疑問走進offsetof()
這個巨集:
// At include/linux/stddef.h
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
也很簡單對吧?把0地址轉換成結構體類型指針,然後利用這個特殊的結構體指針獲取member
,然後再對member
取地址,得到的這個值就是member
相對於0地址的偏移值,這個偏移值不就是member相對於結構體首地址的偏移值嘛!
看到這裡,如果你和筆者一樣是內核初學者,你可能會和筆者一樣驚訝:0地址還能這麼用?!!筆者也是在發出了這樣的感嘆之後,才決定記錄下這篇隨筆。
offsetof
這個巨集還有另一個實現,即調用GNU C的內建函數__builtin_offsetof
,本質上和上面的定義是一致的。
總結
這個巨集包括了三步:賦值、檢查、定址。筆者分析了2.6.23中的賦值操作目的與最新的5.17.5中的檢查和定址操作。
在最後希望詢問看到這篇文章的朋友們一個問題:為什麼最新的版本還需要賦值給__mptr
,能否在第三行中直接使用(void *)ptr
代替__mptr
?
原創文章,如有錯漏,敬請補充指正,如對於文章風格有建議,請在評論區直接提出,感謝。