2023-01-11
關鍵字:container_of、內存對齊
1、示例
container_of是定義在linux內核kernel.h中的一個宏,它的作用是根據結構體中某個成員的地址反推出該結構體的地址。
container_of之所以能做到這點,得歸功於linux的內存管理方式在邏輯上是連續的這一特性。
先來看看container_of的用法:
1 struct S1{ 2 int a; 3 unsigned char b; 4 long c; 5 int d; 6 char e; 7 char f; 8 char* g; 9 }; 10 11 struct S1 s1; 12 13 s1.c = 1000; 14 15 long* pc = &s1.c; 16 17 struct S1* s1tmp = container_of(pc, struct S1, c); 18 printf("s1tmp->c:%d\n", s1tmp->c);
這段代碼運行後將打印“s1tmp->:1000”,表示僅通過struct S1中的成員long c的地址就反推出了包含此成員的結構體的地址。
2、剖析
接下來看看container_of是如何實現反推功能的。
其定義原型如下:
1 ./include/linux/kernel.h 2 3 /** 4 * container_of - cast a member of a structure out to the containing structure 5 * @ptr: the pointer to the member. 6 * @type: the type of the container struct this is embedded in. 7 * @member: the name of the member within the struct. 8 * 9 */ 10 #define container_of(ptr, type, member) ({ \ 11 void *__mptr = (void *)(ptr); \ 12 BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \ 13 !__same_type(*(ptr), void), \ 14 "pointer type mismatch in container_of()"); \ 15 ((type *)(__mptr - offsetof(type, member))); })
第一個參數即我們已知的結構體內成員變量的地址。一定得注意,這個必須是成員變量本身的地址,某些成員可能是指針類型,必須使用這個指針變量本身的地址,而不能是它所指向的地址。舉例如下:
以下寫法是可行的:
struct S1{ int a; unsigned char b; long c; int d; char e; char f; char* g; }; struct S1 s1; char* ctmp1 = "hello world"; s1.g = ctmp1; char* pg = &s1.g; struct S1* s1tmp = container_of(pg, struct S1, g);
以下寫法是不可行的:
struct S1{ int a; unsigned char b; long c; int d; char e; char f; char* g; }; struct S1 s1; char* ctmp1 = "hello world"; s1.g = ctmp1; char* pg = s1.g; struct S1* s1tmp = container_of(pg, struct S1, g);
container_of的第二個參數直接填要反推的結構體類型。
第三個參數填的是第一個參數在所求結構體中成員的名稱。
查看container_of宏實現原型,就三行代碼。第一行無須理會。第二行其實就是去判斷所傳的參數是否合法,其實也無須理會。實際上第二行代碼在某些平臺可能壓根就不做任何事。真正起作用的是最後一行代碼,它是用第一個參數中的地址減去該成員在結構體中的偏移量從而得到結構體第一個成員地址,而我們知道在結構體中第一個成員的地址其實就是這個結構體的根地址。所以我前面纔會說container_of之所以能存在,完全得益於其邏輯連續的內存管理機制。
這裏額外提一句,最後一行代碼中計算成員在結構體中的偏移量offsetof最終是靠編譯器來算出的。這種最底層的實現就沒有必要去跟蹤了。同時這也表明了我們無法通過純代碼來複刻一個container_of功能,對一個結構體來講,我們可能很容易就能算出某成員的偏移量,但如果要我們用代碼來實現,真的很難。
3、結構體的內存管理
上一節提到我們可以目測計算成員在結構體內的偏移量,這一節就通過一個小例子簡單探究下。
先來看第一節示例代碼中的結構體:
struct S1{ int a; unsigned char b; long c; int d; char e; char f; char* g; };
如果單純地以“組合”心態來看待結構體與普通變量之間的關係,那麼對struct S1求sizeof將會得到27個字節。但實際上,sizeof(struct S1)將會得到32個字節。
那我們稍微改一下struct S1,把f成員刪掉:
struct S1{ int a; unsigned char b; long c; int d; char e; char* g; };
sizeof(struct S1)的值是多少?仍然是32個字節!
再改一下,在已經刪掉f成員的基礎上再刪去c成員和g成員並增加一個char e2成員:
struct S1{ int a; unsigned char b; int d; char e; char e2; };
sizeof(struct S1)的值會是多少?答案是16個字節。
爲什麼會這樣呢?爲什麼linux中結構體的大小不是簡單地普通變量的組合呢?
linux的內存管理中有一個被稱爲“內存對齊”的機制。我對它的瞭解十分有限,查到的資料看的也是一知半解,只是大概知道內存對齊能提高內存訪問效率,且內存對齊就是在結構體中將各類型變量會以相同的寬度來存儲。
以上述第一個結構體來看,我們先給這些變量賦上值:
struct S1{ int a; unsigned char b; long c; int d; char e; char f; char* g; }; struct S1 s1; s1.a = 0x01; s1.b = 0x02; s1.c = 0x1000; s1.d = 0x10; s1.e = 0x04; s1.f = 0x08; s1.g = NULL;
s1在內存中的管理方式如下:
結構體成員a的地址爲0xaee0 ~ 0xaee3,共佔4個字節。
成員b的地址爲0xaee4,共佔1個字節。但緊隨其後的3個字節是不可被使用的。
成員c的地址爲0xaee8 ~ 0xaeef,共佔8個字節。
成員d的地址爲0xaf0 ~ 0xaf3,共佔4個字節。
成員e的地址爲0xaf4,佔1個字節。
成員f的地址爲0xaf5,佔1個字節。但緊隨其後的2個字節是不可使用的。
成員g的地址爲0xaf8 ~ 0xaff,共佔8個字節。
默認情況下的對齊方式是按結構體成員的類型中最長的那個作爲基準。如在上述struct S1結構體中,最長的類型就是long和char*,佔8個字節。因此其它所有成員都得按8個字節對齊。但它又不是強行將每一個成員都擴充至8個字節長度,而是擴充與壓縮多措並舉。首先將成員a擴充至8字節,但因int類型僅需4個字節即可,剩餘的4個字節不應被浪費,而它後面的成員b所需要的字節數又少於多出的字節數,因此成員b就可以緊隨a之後存儲數據。如上圖所示。在處理好成員a和成員b後仍有3個字節被空閒,但之後的成員c所需的字節數大於剩餘的空間,無法合併,故而重新開僻一個8字節空間用於存儲成員c。剩下的成員也依此類推。
所以,上述struct S1所佔的空間會是32個字節。由此我們也可以推導出,在設計結構體時可以有意識地排版各成員類型以降低內存空間的閒置率從而減少程序對資源的佔用。
在瞭解了上述struct S1的內存排布方式後,本節開頭處的三個示例結構體剩餘的兩個的大小也就很容易理解了,就不再此贅述了。
最後,其實我們是可以動態修改其內存對齊字節數的。linux默認就是按最長類型作爲對齊基準字節數,但我們可以通過關鍵字"#pragma param(*)"來修改。若我們將對齊字節數修改爲1,則結構體在內存中就是普通類型變量的組合,該是多少字節就是多少字節。
具體的修改方式如下,可以在代碼文件的任何地方聲明:
#pragma pack() //動態調整,以成員類型最長的字節作爲基準數 #pragma pack(1) //以1字節作爲對齊基準數。即不對齊。 #pragma pack(4) //以4字節作爲對齊基準數。