//獲取結構體成員相對於結構體的偏移
#define offsetof(TYPE,MEMBER) ((int) &((TYPE *)0)->MEMBER)
//通過獲取結構體中的某個成員,反推該結構體的指針
#define container_of(ptr, type , member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr) ; \
(type *)((char *)__mptr - offsetof(type,member)) ;})
工作原理:
先用typeof獲取變量的數據類型,也就是member成員的類型,然後將member這個成員 的指針轉成自己類型的指針,再從offsetof相減,就得到整個結構體變量的首地址了,再將該地址強制轉化爲type *。
1、container_of宏第一步是做類型檢查的,也就是檢查ptr是否是指向結構成員member的,如果我們用typeof求出來的類型和ptr不一致,那麼編譯器會報錯。爲啥要做這個檢查呢?因爲ptr和member都是人工輸入的參數,宏要保證它們是結構體成員和其相關聯的指針,這個宏纔有意義,所以類型檢查是必須的。
2、第二步相減時,把mptr指針強轉成(char *)是因爲,char指針減法只移一個字節,如果這樣纔可能得出準確的地址,否則,改爲int類型,在減1就移動4個就亂了。
我們定義一個變量的格式是:修飾符+變量類型+變量名 = 右值;
修飾符 變量類型 變量名 右值
const typeof( ((type*)0)->member ) *__mptr = (ptr) ;
現在看明白了嗎,拋開具體細節,“typeof( ((type*)0)->member )”代表的是一種數據類型,那麼它是什麼樣的數據類型呢?
((type*)0):它把0轉換爲一個type類型(也就是宿主結構體類型),爲什麼要這樣做,且看後文
((type*)0)->member:這個0指針指向結構體中的member成員
typeof是gcc的c語言擴展保留字,用於獲取變量的類型
typeof( ((type*)0)->member ) *:得出member的數據類型
所以,第2行的結果就是定義一個指向member的指針,並賦值爲ptr
在linux 內核編程中,會經常見到一個宏函數container_of(ptr,type,member), 但是當你通過追蹤源碼時,像我們這樣的一般人就會絕望了(這一堆都是什麼呀? 函數還可以這樣定義??? 怎麼還有0呢??? 哎,算了,還是放棄吧。。。)。 這就是內核大佬們厲害的地方,隨便兩行代碼就讓我們懷疑人生,凡是都需要一個過程,慢慢來吧。
其實,原理很簡單: 已知結構體type的成員member的地址ptr,求解結構體type的起始地址。
type的起始地址 = ptr - size (這裏需要都轉換爲char *,因爲它爲單位字節)。
到此,該函數已經講完,是不是很簡單??? 其實也不是,這裏並沒有提到size如何計算,而令我們頭暈的正是這裏。
好吧,先上container of函數原型:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
其次爲 offserof 函數原型:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
怎麼樣,是不是很炫? 好吧,下面開始揭開面紗:
(一)0 指針的使用 (自己給的名字,不知有木問題)
讓事實說話:
#include<stdio.h>
struct test
{
char i ;
int j;
char k;
};
int main()
{
struct test temp;
printf("&temp = %p\n",&temp);
printf("&temp.k = %p\n",&temp.k);
printf("&((struct test *)0)->k = %d\n",((int)&((struct test *)0)->k));
}
編譯運行,可以得到如下結果:
&temp = 0xbf9815b4
&temp.k = 0xbf9815bc
&((struct test *)0)->k = 8
什麼意思看到了吧,自定義的結構體有三個變量:i,j,k。 因爲有字節對齊要求,所以該結構體大小爲4bytes * 3 =12 bytes. 而&((struct test *)0)->k 的作用就是求 k到結構體temp起始地址的字節數大小(就是我們的size)。在這裏0被強制轉化爲struct test *型, 它的作用就是作爲指向該結構體起始地址的指針,就是作爲指向該結構體起始地址的指針,就是作爲指向該結構體起始地址的指針, 而&((struct test *)0)->k 的作用便是求k到該起始指針的字節數。。。其實是求相對地址,起始地址爲0,則&k的值便是size大小(注:打印時因爲需要整型,所以有個int強轉)所以我們便可以求我們需要的 size 了 。 好吧,一不小心把 offsetof() 函數的功能給講完了:::
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
這次再看就順眼了吧(底層爲什麼是這樣我還是不懂。。。只知道這樣確實可以) , 所以offsetof()的作用就是求我們夢寐以求的size, 並以size_t形式返回(size_t: 無符號整型)。
(二) 內核編程的嚴謹性
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
這裏我們只看第二行:
const typeof( ((type *)0)->member ) *__mptr = (ptr);
它的作用是什麼呢? 其實沒什麼作用(勿噴勿噴,讓我把話說完),但就形式而言 _mptr = ptr, 那爲什麼要要定義一個一樣的變量呢??? 其實這正是內核人員的牛逼之處:如果開發者使用時輸入的參數有問題:ptr與member類型不匹配,編譯時便會有warnning, 但是如果去掉改行,那個就沒有了,而這個警告恰恰是必須的(防止出錯有不知道錯誤在哪裏)。。。這嚴謹性可以吧
typeof( ((type *)0)->member )
它的作用是獲取member的類型僅此而已。至此基本結束
(三) 總結
container_of(ptr, type,member)函數的實現包括兩部分:
1. 判斷ptr 與 member 是否爲同意類型
2. 計算size大小,結構體的起始地址 = (type *)((char *)ptr - size) (注:強轉爲該結構體指針)
現在我們知道container_of()的作用就是通過一個結構變量中一個成員的地址找到這個結構體變量的首地址。
container_of(ptr,type,member),這裏面有ptr,type,member分別代表指針、類型、成員。
#include <stdio.h>
#include <stdlib.h>
//獲取結構體成員相對於結構體的偏移
#define offsetof(TYPE,MEMBER) ((int) &((TYPE *)0)->MEMBER)
//通過獲取結構體中的某個成員的,反推該結構體的指針
#define container_of(ptr, type , member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr) ; \
(type *)((char *)__mptr - offsetof(type,member)) ;})
#pragma pack(4)
struct ptr
{
char a ;
short b ;
int c ;
double d ;
};
#pragma pack()
int main(void)
{
struct ptr Pt ;
struct ptr *pt ;
printf("ptr:%d\n",sizeof(struct ptr));//16
//獲取結構體的首地址
printf("ptr:%p\n",&Pt); //0028FEA8
Pt.a = 'a';
Pt.b = 2 ;
Pt.c = 4 ;
Pt.d = 12.04 ;
//通過container_of獲取結構體的首地址
pt = container_of(&Pt.c, struct ptr , c);
printf("pt:%p\n",pt); //0028FEA8
printf("a:%c\n",pt->a) ; //'a'
printf("b:%d\n",pt->b) ; //2
printf("c:%d\n",pt->c) ; //4
printf("d:%.2lf\n",pt->d);//12.04
return 0 ;
}