您好,登錄后才能下訂單哦!
ANSI C 定義了 sizeof 關鍵字,用來獲取一個變量或數據類型在內存中所占的存儲字節數。GNU C 擴展了一個關鍵字 typeof,用來獲取一個變量或表達式的類型。這里使用關鍵字可能不太合適,因為畢竟 typeof 還沒有被寫入 C 標準,是 GCC 擴展的一個關鍵字。為了方便,我們就姑且稱之為關鍵字吧。
通過使用 typeof,我們可以獲取一個變量或表達式的類型。所以 typeof 的參數有兩種形式:表達式或類型。
int i ;
typeof(i) j = 20;
typeof(int *) a;
int f();
typeof(f()) k;
在上面的代碼中,因為變量 i 的類型為 int,所以 typeof(i) 就等于 int,typeof(i) j =20 就相當于 int j = 20,typeof(int ) a; 相當于 int a;,函數也是有類型的,函數的類型即其返回值類型,所以 typeof(f()) k; 就相當于 int k;。
根據上面 typeof 的用法,我們編寫一個程序,來學習一下 typeof 的使用。
int main(void)
{
int i = 2;
typeof(i) k = 6;
int *p = &k;
typeof(p) q = &i;
printf("k = %d\n",k);
printf("*p= %d\n",*p);
printf("i = %d\n",i);
printf("*q= %d\n",*q);
return 0;
}
運行結果為:
k = 6
*p = 6
i = 2
*q = 2
通過運行結果可知,通過 typeof 獲取一個變量的類型 int 后,可以使用該類型再定義一個變量。這跟我們直接使用 int 定義一個變量,效果是一樣的。
除了使用 typeof 獲取基本數據類型,還有其它一些高級的用法:
typeof (int *) y; // 把 y 定義為指向 int 類型的指針,相當于int *y;
typeof (int) *y; //定義一個執行 int 類型的指針變量 y
typeof (*x) y; //定義一個指針 x 所指向類型 的指針變量y
typeof (int) y[4]; //相當于定義一個:int y[4]
typeof (*x) y[4]; //把 y 定義為指針 x 指向的數據類型的數組
typeof (typeof (char *)[4]) y;//相當于定義字符指針數組:char *y[4];
typeof(int x[4]) y; //相當于定義:int y[4]
在上一節中,我們定義了一個宏 MAX(x,y),用來求出兩個數中較大的那個,而且可以支持不同類型數據:
#define MAX(type,x,y)({ \
type _x = x; \
type _y = y; \
_x > _y ? _x : _y; \
})
這個宏雖然可以支持任意數據類型,但是仍有瑕疵:我們必須把數據的類型作為一個單獨的參數傳遞給宏。接下來,我們繼續優化這個宏:不需要再單獨傳遞這個參數,而是使用 typeof 關鍵字來直接獲取參數的數據類型。
#define MAX(x,y)({ \
typeof(x) _x = x; \
typeof(x) _y = y; \
_x > _y ? _x : _y; \
})
int main(void)
{
int i = 2;
int j = 6;
printf("max: %d\n", MAX(i, j));
printf("max: %f\n", MAX(3.14, 3.15));
return 0;
}
通過 typeof 直接獲取宏的參數類型,這樣我們就不必再單獨將參數的類型傳給宏了。改進后的宏同樣也支持任意類型的數據比較大小。在 main 函數中,我們分別使用這個宏去比較 int 型數據和 float 型數據,發現都可以正常工作!是不是很酷?等你面試時把這個宏寫給面試官看,你覺得面試官還會舍得讓你回去等消息么?
有了這個思路,我們同樣也可以將以前定義的一些宏通過這種方式改寫,這樣 SWAP 宏也可以支持多種類型的數據了。
#define swap(a, b) \
do { \
typeof(a) __tmp = (a); \
(a) = (b); \
(b) = __tmp; \
} while (0)
關鍵字 typeof 在 Linux 內核中被廣泛使用,主要用在宏定義中,用來獲取宏參數類型。比如內核中,min/max 宏的定義:
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
在 min\max 宏定義中,使用 typeof 直接獲取參數類型,就不必再給宏單獨傳遞參數 type 了。內核中定義的宏跟我們上面舉的例子有點不一樣,多了一行代碼:
(void) (&_max1 == &_max2);
這一句很有意思:看起來是一句廢話,其實用得很巧妙!它主要是用來檢測宏的兩個參數 x 和 y 的數據類型是否相同。如果不相同,編譯器會給一個警告信息,提醒程序開發人員。
warning:comparison of distinct pointer types lacks a cast
讓我們分析一下,它是怎么實現的:語句 &_max1 == &_max2 用來判斷兩個變量 _max1 和 _max2的地址是否相等,即比較兩個指針是否相等。&_max1 和 &_max2分別表示兩個不同變量的地址,怎么可能相等呢!既然大家都知道,內存中兩個不同的變量地址肯定不相等,那為什么還要在此多此一舉呢?妙就妙在,當兩個變量類型不相同時,對應的地址,即指針類型也不相同。比如一個 int 型變量,一個 char 變量,對應的指針類型,分別為 char 和 int ,而兩個指針比較,它們必須是同種類型的指針,否則編譯器會有警告信息。所以,通過這種“曲線救國”的方式,這行程序語句就實現了這樣一個功能:當宏的兩個參數類型不相同時,編譯器會及時給我們一個警告信息,提醒開發者。
看完這個宏的實現,不得不感嘆內核的博大精深!每一個細節,每一個不經意的語句,細細品來,都能學到很多知識,讓你的 C 語言功底更加深厚。不要走,我們接著分析 Linux 內核中另一個更有意思的宏。
有了上面語句表達式和 typeof 的基礎知識,接下來我們就可以分析 Linux 內核第一宏:container_of。這個宏在 Linux 內核中應用甚廣。會不會用這個宏,看不看得懂這個宏,也逐漸成為考察一個內核驅動開發者 C 語言功底的不成文標準。廢話少說,我們還是先一睹芳容吧。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
作為內核第一宏,絕對不是蓋的:看看這身段,這曲線,高端大氣上檔次,低調奢華有內涵,不出去再做個頭發,簡直就是暴殄天物。GNU C 高端擴展特性的綜合運用,宏中有宏,不得不佩服內核開發者這天才般地設計。那這個宏到底是干什么的呢?它的主要作用就是:根據結構體某一成員的地址,獲取這個結構體的首地址。根據宏定義,我們可以看到,這個宏有三個參數,它們分別是:
也就是說,我們知道了一個結構體的類型,結構體內某一成員的地址,就可以直接獲得到這個結構體的首地址。container_of 宏返回的就是這個結構體的首地址。
container_of 宏使用示例
比如現在,我們定義一個結構體類型 student:
struct student
{
int age;
int num;
int math;
};
int main(void)
{
struct student stu;
struct student *p;
p = container_of( &stu.num, struct student, num);
return 0;
}
在這個程序中,我們定義一個結構體類型 student,然后定義一個結構體變量 stu,我們現在已經知道了結構體成員變量 stu.num 的地址,那我們就可以通過 container_of 宏來獲取結構體變量 stu 的首地址。
這個宏在內核中非常重要。我們知道,Linux 內核驅動中,為了抽象,對數據結構體進行了多次封裝,往往一個結構體里面嵌套多層結構體。也就是說,內核驅動中不同層次的子系統或模塊,使用的是不同封裝程度的結構體,這也是 C 語言的面向對象思想。分層、抽象、封裝,可以讓我們的程序兼容性更好,適配更多的設備,但同時也增加了代碼的復雜度。
我們在內核中,經常會遇到這種情況:我們傳給某個函數的參數是某個結構體的成員變量,然后在這個函數中,可能還會用到此結構體的其它成員變量,那這個時候怎么辦呢?container_of 就是干這個的,通過它,我們可以首先找到結構體的首地址,然后再通過結構體的成員訪問就可以訪問其它成員變量了。
struct student
{
int age;
int num;
int math;
};
int main(void)
{
struct student stu = { 20, 1001, 99};
int *p = &stu.math;
struct student *stup = NULL;
stup = container_of( p, struct student, math);
printf("%p\n",stup);
printf("age: %d\n",stup->age);
printf("num: %d\n",stup->num);
return 0;
}
在這個程序中,我們定義一個結構體變量 stu,知道了它的成員變量 math 的地址 &stu.math,我們就可以通過 container_of 宏直接獲得 stu 結構體變量的首地址,然后就可以直接訪問 stu 結構體的其它成員 stup->age 和 stup->num。
知道了 container_of 宏的用法之后,我們接著去分析這個宏的實現。作為一名 Linux 內核驅動開發者,除了要面對各種手冊、底層寄存器,有時候還要應付底層造輪子的事情,為了系統的穩定和性能,有時候我們不得不深入底層,死磕某個模塊,進行分析和優化。底層的工作雖然很有挑戰性,但有時候也是很枯燥的,不像應用開發那樣有意思。所以,為了提高對工作的興趣,大家表面上雖然不說自己牛 X,但內心深處,一定要建立起自己的職位優越感。人不可有傲氣,但一定要有傲骨:我們可不像應用開發,知道 API 接口、讀讀文檔、完成功能就 OK 了。作為一名底層開發者,要時刻記住,要和寄存器、內存、硬件電路等各族底層群眾打成一片。從群眾中來,到群眾中去,急群眾所急,想群眾所想,這樣才能構建一個穩定和諧的嵌入式系統:穩定高效、上下通暢、運行365個日出也不崩潰。
container_of 宏的實現主要用到了我們上兩節所學的知識:語句表達式和 typeof,再加上結構體存儲的基礎知識。為了幫助大家更好地理解這個宏,我們先復習下結構體存儲的基礎知識。
我們知道,結構體作為一個復合類型數據,它里面可以有多個成員。當我們定義一個結構體變量時,編譯器要給這個變量在內存中分配存儲空間。除了考慮數據類型、字節對齊因素之外,編譯器會按照結構體中各個成員的順序,在內存中分配一片連續的空間來存儲它們。
struct student{
int age;
int num;
int math;
};
int main(void)
{
struct student stu = { 20, 1001, 99};
printf("&stu = %p\n", &stu);
printf("&stu.age =%p\n", &stu.age);
printf("&stu.num =%p\n", &stu.num);
printf("&stu.math =%p\n", &stu.math);
return 0;
}
在這個程序中,我們定義一個結構體,里面有三個 int 型數據成員,我們定義一個變量,然后分別打印結構體的地址、各個成員變量的地址,運行結果如下:
&stu = 0028FF30
&stu.age = 0028FF30
&stu.num = 0028FF34
&stu.math = 0028FF38
從運行結果我們可以看到,結構體中的每個成員變量,從結構體首地址開始,依次存放。每個成員變量相對于結構體首地址,都有一個固定偏移。比如 num 相對于結構體首地址偏移了4個字節。math 的存儲地址,相對于結構體首地址偏移了8個字節。
一個結構體數據類型,在同一個編譯環境下,各個成員相對于結構體首地址的偏移是固定的。我們可以修改一下上面的程序,當結構體的首地址為0時,結構體中的各成員地址在數值上等于結構體各成員相對于結構體首地址的偏移。
struct student{
int age;
int num;
int math;
};
int main(void)
{
printf("&age = %p\n",&((struct student*)0)->age);
printf("&num = %p\n",&((struct student*)0)->num);
printf("&math= %p\n",&((struct student*)0)->math);
return 0;
}
在上面的程序中,我們沒有直接定義結構體變量,而是將數字0,通過強制類型轉換,轉換為一個指向結構體類型為 student 的常量指針,然后分別打印這個常量指針指向的結構體的各成員地址。運行結果如下:
&age = 00000000
&num = 00000004
&math= 00000008
因為常量指針為0,即可以看做結構體首地址為0,所以結構體中每個成員變量的地址即為該成員相對于結構體首地址的偏移。container_of 宏的實現就是使用這個技巧來實現的。
有了上面的基礎,我們再去分析 container_of 宏的實現就比較簡單了。知道了結構體成員的地址,如何去獲取結構體的首地址?很簡單,直接拿結構體成員的地址,減去該成員在結構體內的偏移,就可以得到該結構體的首地址了。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
從語法角度,我們可以看到,container_of 宏的實現由一個語句表達式構成。語句表達式的值即為最后一個表達式的值:
(type *)( (char *)__mptr - offsetof(type,member) );
最后一句的意義就是,拿結構體某個成員 member 的地址,減去這個成員在結構體 type 中的偏移,結果就是結構體 type 的首地址。因為語句表達式的值等于最后一個表達式的值,所以這個結果也是整個語句表達式的值,container_of 最后就會返回這個地址值給宏的調用者。
那如何計算結構體某個成員在結構體內的偏移呢?內核中定義了 offset 宏來實現這個功能,我們且看它的定義:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
這個宏有兩個參數,一個是結構體類型 TYPE,一個是結構體的成員 MEMBER,它使用的技巧跟我們上面計算0地址常量指針的偏移是一樣的:將0強制轉換為一個指向 TYPE 的結構體常量指針,然后通過這個常量指針訪問成員,獲取成員 MEMBER 的地址,其大小在數值上就等于 MEMBER 在結構體 TYPE 中的偏移。
因為結構體的成員數據類型可以是任意數據類型,所以為了讓這個宏兼容各種數據類型。我們定義了一個臨時指針變量 __mptr,該變量用來存儲結構體成員 MEMBER 的地址,即存儲 ptr 的值。那如何獲取 ptr 指針類型呢,通過下面的方式:
typeof( ((type *)0)->member ) *__mptr = (ptr);
我們知道,宏的參數 ptr 代表的是一個結構體成員變量 MEMBER 的地址,所以 ptr 的類型是一個指向 MEMBER 數據類型的指針,當我們使用臨時指針變量 mptr 來存儲 ptr 的值時,必須確保 mptr 的指針類型是一個指向 MEMBER 類型的指針變量。typeof( ((type )0)->member )表達式使用 typeof 關鍵字,用來獲取結構體成員 member 的數據類型,然后使用該類型,使用 typeof( ((type )0)->member ) *__mptr 這行程序語句,就可以定義一個指向該類型的指針變量了。
還有一個需要注意的細節就是:在語句表達式的最后,因為返回的是結構體的首地址,所以數據類型還必須強制轉換一下,轉換為 TYPE ,即返回一個指向 TYPE 結構體類型的指針,所以你會在最后一個表達之中看到一個強制類型轉換(TYPE )。
好了,到這里,我們對 container_of 宏的分析也就接近尾聲了。任何一個復雜的東西,我們都可以把它分解,運用所學的基礎知識一點一點剖析:先去降維分析,然后再進行綜合。比如 container_of 宏的定義,就運用了結構體的存儲、語句表達式、typeof 等知識點。掌握了這些基礎知識,有了分析方法,以后在內核中再遇到這樣類似的宏,就不用再百度、Google了,萬一搜不到怎么辦?在這樣一個考察工程師技術能力的關鍵時刻,我們可以自信從容地去自己分析了。這就是你的核心競爭力,也是你超越其他工程師、脫穎而出的機會。
本教程根據 C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關注:
微信公眾號:宅學部落(armlinuxfun)
51CTO學院-王利濤老師:http://edu.51cto.com/sd/d344f
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。