在寫Linux驅動的過程中經常是一個結構體套一層結構體,而在某些函數中傳入的參數是子結構體指針,但是我們又需要獲取的其外層結構體的數據,Linux爲我們提供了container_of宏定義來爲我們解決這個問題。
container_of宏定義就是用來通過內層結構體的指針獲取外層結構體指針,宏定義很巧妙,我等凡人想不出來。下面就詳細分析其實現原理吧。
container_of
宏定義需要用到offsetof
宏,現在offsetof
宏的功能已經變成一個GCC的內建函數了__builtin_offsetof (TYPE, MEMBER)
,但offsetof宏實際定義可由第二行定義實現。
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) );})
offsetof(TYPE, MEMBER)
的操作過程:
首先分析offsetof宏,其功能是獲得成員MEMBER
在TYPE
結構中的偏移量。
- 宏參數
TYPE
:表示MEMBER成員所在結構體的數據類型; - 宏參數
MEMBER
:TYPE結構體的數據成員MEMBER; - 根據運算符的優先級最內層的
((TYPE *)0)
:表示將0地址轉換爲TYPE類型的指針 ((TYPE *)0)->MEMBER
:通過0地址,類型爲TYPE的指針,指向成員MEMBER&((TYPE *)0)->MEMBER
:對MEMBER成員進行取地址運算,那麼地址值就是MEMBER成員在TYPE結構中的偏移量(size_t)&((TYPE *)0)->MEMBER
:將地址值強制轉爲size_t類型的整形數值,即偏移量。
container_of(ptr, type, member)
的操作過程:
- 首先說明關鍵字
typeof
,可以獲得一個數據的類型 - 宏參數
ptr
:表示指向member成員的指針; - 宏參數
type
:表示member成員所在結構體的數據類型; - 宏參數
member
:type結構體的數據成員member; - container_of宏被封裝成一個語句塊,包含兩條語句,define預定義返回最後一句話的值
- 第一句:
const typeof( ((type *)0)->member ) *__mptr = (ptr);
- 根據運算符的優先級最內層的
((type *)0)
:表示將0地址轉換爲type
類型的指針 ((type *)0)->member
:將0地址指針指向member
成員typeof( ((type *)0)->member )
:獲得member
的數據類型const typeof( ((type *)0)->member ) *__mptr
:定義一個__mptr
指針變量,指針數據類型爲member
的數據類型const typeof( ((type *)0)->member ) *__mptr = (ptr);
:將宏參數變量ptr
賦值給變量__mptr
;此時變量__mptr
中保存的是member
成員的地址- 第二句:
(type *)( (char *)__mptr - offsetof(type, member) );
- 將
__mptr
強制轉換爲char*
指針 ( (char *)__mptr - offsetof(type, member) )
:用強制轉換後的指針值減去member
在type
中的偏移量,得到的值就是member
成員所在結構體的地址,此時仍然是char*
指針(type *)( (char *)__mptr - offsetof(type, member) );
將最後的得到的地址轉換爲(type *)
指針,即得到member
成員所在結構體的指針。
仔細分析會發現,其實根本沒有必要定義第一句,只需一句就可以實現:
#define container_of(ptr, type, member) ({ \
(type *)( (char *)(ptr) - offsetof(type,member) );})
但是爲什麼會定義第一句呢?其實主要爲了對ptr
與member
做類型檢查,如果用typeof
求出來的類型和ptr
不一致,那麼在賦值操作時,編譯器會報錯。
可能會有仁兄有疑問:0地址訪問數據,不會出現非法地址訪問的錯誤嗎?
仔細分析,這裏並沒有發生0地址的附近數據的訪問,仔細看看僅僅發生了&
取地址、typeof
取類型操作,因此不會存在非法訪問內存的問題。
新的內核對此又有了新的定義:
/* Are two types/vars the same type (ignoring qualifiers)? */
#ifndef __same_type
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
#endi
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member)));
有2個新變化:
第一,用void *
取代了char *
來做減法,
第二,用__same_type
宏來做類型檢測,錯了有明確的信息提示。
好了,以後繼續把鏈表相關的操作也搬上來。