天天看點

核心第一宏

核心第一宏

list_entry()有着核心第一宏的美稱,它被設計用來通過結構體成員的指針來傳回結構體的指針。現在就讓我們通過一步步的分析,來揭開它的神秘面紗,感受核心第一宏設計的精妙之處。

整理分析的思路

list_entry()在核心源代碼/include/linux目錄下的list.h中被定義,如下:

#define list_entry(ptr, type, member) \
                container_of(ptr, type, member)
           

在list _entry的定義中,我們看到出現了另外一個宏container _of。而list _entry這個宏正是通過container _of去實作的。是以我們要先進入container _of,來看看它做了什麼。

container_of定義在/include/linux/kernel.h中,定義如下:

#define container_of(ptr, type, member) ({			\
		const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
		(type *)( (char *)__mptr - offsetof(type,member) );})
           

我們發現,在container_of的定義中,又出現一個新的宏offsetof。是以,在開始分析container _of之前,有必要先來搞清楚offsetof。

offsetof定義在/include/linux/stddef.h中,定義如下:

#define offsetof(TYPE, MEMBER)  ((size_t) &((TYPE *)0)->MEMBER)
           

我們可以看到,在offsetof的定義中,已經沒有再引入新的宏,是以,我們就以offsetof為突破口,進行分析。

正式分析

宏offsetof

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

單詞offset的意思是偏移量,是以我們可以顧名思義一下,宏offsetof的作用可能和偏移量有關。那麼,它要求誰的偏移量呢?

  • offsetof用于計算TYPE結構體中成員MEMBER的偏移量。

從offsetof的定義中可以看到,在&((TYPE *)0)->MEMBER中,有一個明顯的強制類型轉換((TYPE *)0)。在C語言中,強制類型轉換有兩種文法:

1.(TYPE)var_name; //變量名形式,如(int)i;
2.(TYPE)varlue;   //值形式,如(type*)0;
           

定義中使用了第二種文法,将0值強制類型轉換成一個TYPE結構體的指針。通過這種強制類型轉換後,TYPE結構體的位址變成了0,那麼為什麼要做這種轉換?它的作用是什麼?

其實這麼做的目的隻有一個,就是為了更容易拿到成員的偏移量。我們知道,結構體類型在預編譯的時候,為了使CPU能夠對資料快速通路和有效節省存儲空間,有一個記憶體對齊的問題,就是結構體的每個成員在記憶體中的存儲都要按照一定的偏移量來存儲。是以會由于成員類型的不同,導緻每個成員的偏移量也不盡相同,是以我們就不能一勞永逸的來給所有成員設定一個固定的偏移值。那我們想要拿到一個成員的偏移量怎麼辦呢?我們就把這個重任交給了編譯器。我們可以指揮編譯器,讓它“交出”成員的偏移量。有一點我們必須清楚,編譯器在預編譯的時候,對每個成員的偏移量是心知肚明的,是以編譯器如果想要知道某個成員的位址,它隻需要用結構體的位址+成員的偏移量就可以得到該成員的位址。

核心第一宏
舉個簡單的例子:以上面的圖為例,如果上面結構體的位址p=1000,,成員C的偏移量(offset)是4,那成員C的位址pc就是1000+4=1004;

這個時候得到的1004是成員C的位址pc,但是我們想要的不是它的位址,而是它的偏移量,這個時候怎麼辦呢?最簡單的辦法就是,直接将結構體的位址變成0不就可以了嗎?0加一個數就等于這個數本身,這樣相加的結果正好就是成員的偏移量了。這就是為什麼定義中要通過強制類型轉換将結構體的位址變成0,舉個例子:

現在将結構體的位址p=0,成員C的偏移量(offset)還是4,0+4=4,得到的結果正好就是該成員的偏移量了。

是以我們讓編譯器執行&((TYPE *)0)->MEMBER這句話的時候,它做的就是這樣一個事情,它先将type類型結構體的位址變成0,然後再去加上成員MEMBER的偏移量,0+偏移量=偏移量,是以最後得到的結果就是成員的偏移量了。核心的設計者們,正是通過這種巧妙的設計,來指揮編譯器交出偏移量。

是以,當我們調用offsetof(TYPE, MEMBER)之後,就會得到成員MEMBER在TYPE結構體中的偏移量了

這裡有一點值得思考的是:&((TYPE *)0)->MEMBER中,結構體的位址通過強制類型轉換變成了0,

我們知道0位址是留給作業系統來使用的,這裡面的内容是不允許普通的程式來通路的。但是這裡卻将結構體位址變成了0,那直接使用0位址不會導緻程式崩潰嗎?

答案是程式是不會崩潰的,編譯器在執行&((TYPE *)0)->MEMBER的時候,并沒有真正去通路0位址中的内容,而隻是将這個0值當作加法運算中的一個加數來處理。形象的說,就是編譯器隻是摘掉了你房間的門牌号拿來作計算,并沒有開門去取放在屋子裡的任何東西。它在做完加法後就走人了,屋子裡的東西是完整無缺的。而之是以編譯器沒有進屋子取東西,是因為有“&”的存在,編譯器看到有“&”,就會明白我隻需要拿到位址就可以了。下面通過一個簡單的例子來說明:

核心第一宏

列印結果如下:

核心第一宏
根據列印結果可以看到:pst->j與&(pst->j)效果是不一樣的
pst->j		//沒有“&”,會通路變量中的内容,列印結果為成員變量中的内容
&(pst->j)	//有“&”,不會通路變量中的内容,隻拿位址,列印結果為成員的位址
           

至此,offsetof的作用我們已經知道了。在container_of的定義中,使用了offsetof,也就是說,在container _of的實作中,它需要用到offsetof來得到結構體某個成員的偏移量,那container _of的作用是什麼?它要偏移量有什麼用呢?接下來就讓我們一起進入container _of的世界吧。

宏container_of

#define container_of(ptr, type, member) ({			\
		const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
		(type *)( (char *)__mptr - offsetof(type,member) );})
           

在進入container _of的世界後,我們發現這裡有兩個“熟悉的陌生人”,分别是typeof和“({ })”。這兩個小夥伴,我們在C語言中是見不到它們的,這是因為他們都隻“生活”在GNU C編譯器中。為了能讓我們在認識container _of的旅程更加輕松,我們有必要花些時間來和typeof和“({ })”這兩個傑出的小夥伴交個朋友,認識一下他們。

typeof
  • typeof是GNU C編譯器的特有關鍵字
  • typeof隻在編譯期生效,用于得到變量的類型

舉個例子:

int i = 100;
typeof(i) j = i; <=> int j = i;  //這兩個語句的作用是等價的,變量i的類型是int,typeof(i)就相當于拿到變量i的類型
           
({ })
  • ({ })是GNU C編譯器的文法擴充
  • ({ })與逗号表達式類似,結果為最後一個語句的值

舉個例子:

核心第一宏

好,我們已經認識了typeof和“({ })”兩個小夥伴,這對我們認識container _of會有很大幫助。現在,我們可以來正式的分析container _of宏了。讓我們再一次把container _of的定義搬到這裡:

#define container_of(ptr, type, member) ({			\
		const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
		(type *)( (char *)__mptr - offsetof(type,member) );})
           

定義中使用了擴充文法“({ })”,前面已經說過,它的結果就是最後一個語句的值,既然這樣,我們就可以直接來看最後一個語句。

(type *)( (char *)__mptr - offsetof(type,member) );

這裡面有一個指針__mptr,它在第二行中被定義,類型由typeof來獲得。指針 __mptr和指針ptr的值是一樣的,而ptr又是宏container _of的一個參數,它是指向type結構體中成員member的一個指針,是以 __mptr也指向type結構體中成員member。為了清晰的表示這種關系,我們用一個圖來表示,它們的關系如下圖:

核心第一宏

我們來看(char )__mptr - offsetof(type,member)這句話是什麼意思。通過offsetof(type,member)可以得到成員member的偏移量,也就是上圖中的offset,然後用 __mptr減去offset,得到一個位址,如上圖所示P,而這個位址就是結構體的位址,這樣就實作了通過成員找到結構體的起始位址。 __mptr前面的char是為了進行指針運算的,以實作逐位元組相減。最後通過(type *)強制類型轉換為指向結構體的指針。

到這裡,宏container_of就真相大白了。

**這裡有一點值得思考的是:**既然__mptr = (ptr),那為什麼不直接使用傳入的參數ptr去減,而是看似“多此一舉”的在第二行将ptr的值賦給 __mptr,然後用 __mptr去減呢?

答案是為了對傳入的參數進行一次類型安全檢查。宏是在編譯的時候由預處理器來進行處理的。預處理器做的是單純的文本替換,不會進行任何的類型檢查,這就有可能導緻我們在編寫代碼的時候,由于粗心大意而造成錯誤。舉例來說,container _of(ptr, type, member)有三個參數,如果傳入ptr的時候,我們由于粗心大意,将一個錯誤的ptr指針傳入,發現程式可能會正常運作,但是結果是錯誤的。這個時候為了增加代碼的安全性,為了能夠有一點點的類型安全的檢查,是以核心的設計者們在定義container _of的時候,在定義的第二行添加了一行用于類型安全檢查的代碼,它會在你傳入錯誤的指針時,彈出一個警告,這個警告告訴我們,在這個地方存在着類型不相容的情況,這樣我們在運作之前就可以再次去檢查一下參數,進而避免一次BUG。

結語

至此,我們已經清楚的知道了container_of的作用了。現在我們回到最初的出發點———list _entry(),也就明白了為什麼它被稱作核心第一宏了。