//xk> 引子
首先看一個簡單的面試題:定義一個宏FIND(stru, e),求結構體stru中某個成員e相對於stru的位移量。
題目的解答很簡單:
#define FIND(stru, e) &(((stru *)0)->e)
將常量0強制類型轉化為stru *類型的指標。因為結構體的首地址為0,所以其成員的地址即為相對於結構體的位移量。
順便提一點,C++中不能直接用&對一個類的成員函數取地址,實現這個目的的技巧參見《C++編程思想》。C++中可以直接用&對一個類的資料成員取地址,因為資料成員和成員函數存放在不同的記憶體區。
//xk> 本文
Linux中廣泛運用容器機制。如果struct A中嵌入包含一個子結構B,則稱A是B的一個容器。
比如作為Linux標準資料結構的迴圈雙鏈表,是由一個一個的表頭串連起來的,而這些表頭就嵌入在雙鏈表所管理的各種類型的對象之中。注意這裡的表頭不是指鏈表的表頭,而是鏈表的元素,是被管理對象在鏈表中的引子,很多時候我們稱被管理對象為鏈表元素。這樣做有兩個好處:1. 被管理的對象可能類型迥異,而表頭的結構統一而簡單。2. 雙鏈表應用非常廣泛,因為表頭結構統一,核心可以提供一套標準的迴圈雙鏈表操作。
通過迴圈雙鏈表很容易找到鏈表上的某個表頭,我們關注的是怎樣通過嵌入在容器中的表頭,找到被管理的那個對象的首地址。
以迴圈雙鏈表為例,list_entry宏提供了這個功能
// list.h#define list_entry(ptr, type, member) container_of(ptr, type, member)
而container_of宏則是核心真正用來實現該目的的宏:根據嵌入的子結構成員返回容器首地址。
根據成員返回結構首地址,簡言之:成員的地址 - 成員相對於結構首地址的便宜量 = 結構首地址。linux核心提供offsetof宏來做這個事情
// kernel.h#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) // 訪問結構指標成員操作符-> 的優先順序高於 取地址操作符&
注意操作符優先順序:訪問結構指標成員操作符-> 高於 取地址操作符& 高於 強制類型轉換操作符()
到這裡都很簡單,不過下一步形式有點複雜:
// kernel.h/** * container_of 從結構的成員來獲得容器執行個體 * @ptr: 指向成員資料的指標 * @type: 容器結構的類型 * @member: 成員在容器內的名稱 */#define container_of(ptr, type, member) \({ \ const typeof( ((type *)0)->member) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type, member) ); \})
其核心依然很簡單,兩步走:
(1) 建立一個指標__mptr,其值等於ptr。其類型為member的類型。(《深入linux核心架構》說其類型為type,還舉了個例子,應該是錯的,指標不能隱式類型轉換,所以__mptr與ptr類型相同才能賦值)
(2) __mptr - 位移量即得到首地址。強制類型轉換(char *)是為了保證指標運算以位元組為單位。通常指標運算表示指向數組的下一個元素,因此指標 + 1的結果與指標的類型相關。強制類型轉換(type *)是因為這個宏的結果應該是容器結構的類型type,返回的是容器結構首地址嘛。
為啥不直接用ptr,而要產生一個const的__mptr呢?難道container_of宏可能修改ptr?