目前為止最詳盡解釋Linux內核源碼中的container_of宏及其標准C版本實現。
在Linux內核源碼文件 include/linux/kernel.h中,定義了container_of宏,源碼如下:
/**
* 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) );})
對於這個宏的准確理解是進入Linux內核源碼分析的必不可少條件,我自己百度了一下,有很多博文對此進行了解釋,然而沒有一個能解釋的十分清楚。於是google了一下,參考了一些英文資料,終於把這個問題搞清楚了,記錄下來供自己和大家參考。
這個宏的唯一目的就是根據一個結構體實例的成員所在的內存地址,推算出該結構體的起始內存地址。在C語言中,已知結構體定義的情況下,編譯器負責安排結構體實例的內存布局,當然編譯器對於每個成員變量在結構體中的偏移量非常清楚。
struct Student
{
int age;
float score;
char* name;
};
根據成員變量的地址來計算結構體的起始地址也就非常簡單了:成員變量地址 - 成員變量在結構體中的偏移量。
總之,在C語言中,編譯器在編譯期間能夠確定成員變量在結構體中的偏移量。
盡管C語言編譯器對成員變量的內存偏移了如指掌,然而C語言標准中並沒有提供一個非常直觀的語法來讓程序員獲取此偏移量。也許C語言設計者認為這種需求主要實在編譯器內部,對於C程序員來說並不常用。為此需要一個小小的技巧,即假設結構體起始地址為A,那麼(成員變量的內存地址 - A)就是偏移量了。更進一步,令A=0,那麼此時成員變量的內存地址==偏移量,寫成代碼如下:
size_t offset_of_score = (size_t) &((struct Student*)0)->score;
雖然0是一個非法指針,然而此處並沒有真正對其進行內存訪問(運行期),只是利用其進行計算(編譯器),所以不會造成任何程序錯誤。
也許獲取成員偏移量這種需求的增多,GCC編譯器的新版本中提供了專門的語法結構__copiler_offsetof(TYPE,MEMBER) 來對此進行支持。
為了兼容不同的GCC版本,Linux源碼文件include/stddef.h中定義了offsetof宏,如下:
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE, MEMBER) __compiler_offsetof(TYPE, MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#endif
先給出一個簡單的例子:
#include <stdio.h>
int main(int argc, char** argv)
{
int x = ({1;2;});
printf("x = %d\n", x);
return 0;
}
上面例子的輸出結果是 x = 2。
當然這個語法是GCC特有的,上述代碼在VC++中無法成功編譯。
乍一看,好像與標准C中的逗號運算符類似,在標准C中:
#include <stdio.h>
int main(int argc, char** argv)
{
int x = (1,2);
printf("x = %d\n", x);
return 0;
}
效果與前面一樣。
然而GCC的這個擴展支持的功能要遠遠大於逗號運算符。因為({})裡面可以有任意的語句。如
#include <stdio.h>
int main(int argc, char** argv)
{
int x = ({int a = 3; printf("hello\n"); 2;});
printf("x = %d\n", x);
return 0;
}
只要({})裡面最後一個表達式為一個值,這個值就是最終的結果。
這是一個非標准的GNU C擴展,用於得到一個變量的數據類型。如:
int x = 100;
typeof(x) y = 200;
此時, typeof(x) 和 int 是等價的。需要注意的是typeof()並不是一個宏,而是編譯器內建構件,所以typeof(x)並不是等於字符串”int”。編譯器看到它時,自動推算變量x的數據類型,這個是在編譯器確定的,要與高級語言的運行時類型識別區分開來。
由於是一個GCC擴展,所以同樣上述代碼在標准C中無法編譯通過。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
現在再看上述代碼,是否已經豁然開朗?
#include <stdio.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
#define container_of(ptr, type, member) ({ \
const typeof( ((type*)0)->member ) *__mptr = (ptr); \
(type*)( (char*)__mptr - offsetof(type,member) ); })
struct Student
{
int age;
float score;
char* name;
};
int main(int argc, char** argv)
{
struct Student s;
printf("the address of s is: %p\n", &s);
struct Student *pStu = container_of(&s.score, struct Student, score);
printf("the address of s is calculated as: %p\n", pStu);
return 0;
}
也許你還存在一個疑問,({})中的第一個語句有啥用?
const typeof( ((type *)0)->member ) *__mptr = (ptr);
的確,這個語句並不影響最後的計算結果。其作用是為了檢查ptr是否是member對應的類型指針。假如程序員不小心把代碼寫成了如下:
struct Student *pStu = container_of(&s.age, struct Student, score);
那麼container_of宏的第一個語句就被解析出:
const float * __mptr = &s.age;
由於age是int類型,而把一個int*賦值給float*變量是不合法的,所以編譯器會發出警告,以提示程序員是否寫錯了代碼。當然這種保護的作用有限,假如age類型也為float,則無法被編譯器發現。不論如何,這種不會帶來計算負擔的檢查還是值得的。
學習Linux內核源碼的一個好處就是把其中的一些編程技巧應用到平時的項目中。然而Linux內核之外的世界裡很少使用這麼多的GCC擴展,那麼是否我們可以用標准C來實現類似功能的container_of呢?
把typeof(),({}),擴展去掉後,我們就得到了一個使用標准C編寫的簡化版本的container_of。
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) (type*)( (char*)(ptr) - offsetof(type, member) )
由於是兼容標准C,所以在GCC,VC++等主流編譯器下均可以順利編譯通過。