Linux 內核預備知識:淺析 offsetof 宏以及新手的所思所想


最近一頭扎進了 Linux 內核的學習中,對於我這樣一個沒什么 C 語言基礎的新生代 Java 農民工來說實在太痛苦了。Linux 內核的學習,需要的基礎知識太多太多了:C 語言、匯編語言、數據結構與算法、操作系統原理、計算機組成原理、計算機體系結構。在囫圇吞棗補完一些計算機基礎知識后,還是在一開始就被一個小小的 offsetof 宏搞暈了。

offsetof 宏

先來看看offsetof宏是什么,這是定義在 <linux/stddef.h>中的一個宏,用來計算一個 struct 結構體中某個成員相對於結構體首地址的偏移量。這是一個很有用的宏,因為 Linux 內核的數據結構大量用了嵌入式的結構體(什么是嵌入式結構體,可以參考 <linux/list.h> 的巧妙設計,這個以后再講)。

// offsetof 宏的定義
#define offsetof(TYPE, MEMBER)   ((size_t) &((TYPE *)0)->MEMBER)

當看到這個東西完全傻眼了,size_t 是啥東東,((TYPE *)0) 又是啥東東,這個 0 又是什么鬼?特別看到后面是訪問一個成員,我去,這不是 Java Farmer 眼中的 NPE 嗎?因為這個宏展開后沒見任何一個結構體的實例。-_-! 於是上網搜索一番。

size_t 基本知道了就是代表一個整數類型,只是為了程序的可移植、效率等原因定義成這樣,具體解釋可以看《為什么size_t重要?》這篇文章。

至於 &((TYPE *)0)->MEMBER) 這段代碼,簡單來說就是取 TYPE 類型的結構體里名字為 MEMBER 的成員的地址,是相對 0 的地址(0 就是 TYPE 結構體的首地址)。C 語言里指針就是個無符號整數,所有 0 也可以轉成一個 TYPE 類型的指針,那么不寫 0 行嗎?答案是肯定的,但算偏移量需要后面再減去首地址值,例如((size_t) (&((TYPE *)1000)->MEMBER)-1000),這樣也行,但是,這就有點多此一舉了。

另外,很重要的一點:這樣算偏移地址僅僅是從邏輯計算上來寫計算的表達式,實際上程序運行時是不會發生任何計算,而是編譯器直接就能取到這個地址偏移量,因而也不會有任何的訪存操作。下面從一個例子可以證明:

1、先寫個 C 測試程序

#include <stdio.h>

// 定義一個取偏移量的宏
#define offsetof(TYPE, MEMBER)   ((size_t) &((TYPE *)0)->MEMBER)

// 定義一個結構體
struct my_struct {
    int a,b,c;
    struct my_struct *next; // 后面我要算這個成員的偏移量
};

// main 函數很簡單就是輸出偏移量的值
void main() {
    printf("offsetof next=%d\n", offsetof(struct my_struct, next));
}

編譯,運行,最后輸出的結果是:offsetof next=16,為什么是16?next 前面有三個 int 類型的成員,各占 4 字節,那 next 應該是從 12 開始,其實這要看編譯的是 64 位還是 32 位,因為筆者的機器是 64 位的 Redhat,而 gcc 編譯選項沒加 -m32,所以編譯出來的程序自然是 64 位的了,因此 next 指針是 8 個字節,要 8 字節對齊的話,自然不能從 12 開始,要從 16 開始。整個結構體的長度是 24 字節(即 sizeof(struct my_struct) = 24)。

2、第二部再將上面的 C 代碼編譯成匯編看看,指令是怎么執行的

/**
 *  以 . 開頭的行我們不用管它,都是些編譯器生成的東西,只看匯編指令即可
**/
	.file	"mymain.c"
	.section	.rodata
.LC0:
	.string	"offsetof next=%d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$16, %esi        /* printf 的第二個參數,看,這里沒有任何計算,編譯時就知道偏移量是 16,直接存到 esi 寄存器作為 printf 函數的實參 */
	movl	$.LC0, %edi      /* printf 的第一個參數,就是上面的字符串常量 */
	movl	$0, %eax
	call	printf           /* 調用 printf 函數,要說明的是,在 x86-64 結構體系中,有 6 個寄存器是可以用於傳參的(這里用了 esi 和 edi),多於 6 的其余就壓棧,也就是上面 rsp 所指的棧頂 */
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
	.section	.note.GNU-stack,"",@progbits

好,到這里,從上面匯編指令可以看到,offsetof 宏展開后就是一個 16 這個值,編譯器直接就優化算好了,所有匯編指令僅僅是為了調用 printf 函數所做的壓棧保護現場,傳參,彈棧恢復現場這些指令。當然,上面說的是在 x86 結構體系下的指令結構。

小結

Linux 是一個非常龐大的系統,幾乎涵蓋了所有計算機基礎知識。學習 Linux 內核是非常艱巨的,不但需要非常牢固的計算機基礎,還需要想象力,大局觀。學習了一個月,總結幾點經驗:

1、基礎知識要時時溫習,“溫故知新”。每次看都會有不同的感悟和理解。像這篇學習筆記就是對基礎知識的溫故,從匯編指令角度看編譯器對宏展開做的工作。永遠不要相信網上一些什么視頻教程說的不需要什么基礎,學習 Linux 內核需要的基礎知識太多了,而且這些視頻也不必要浪費時間看,浪費錢買,都是一些二手知識;也永遠不要相信什么“一文讀懂xxx”這類的文章,同樣是一些二手知識,是不是發現看這些文章很容易就忘了?掌握知識從來沒有捷徑。

基礎、基礎,基礎才是最重要的,計算機技術發展了這么多年,以及近些年來火起來的什么大數據,AI,其實都不是什么新東西,本質還是那些計算機基礎知識原理和數學

  基礎知識脫節,沒可能入門 Linux 內核,不要說入門,入窗戶都不可能。所以想學 Linux 內核,從基礎知識開始,無論基礎有多差,只要肯下功夫,不成問題,這些基礎知識包括:

  • 計算機組成原理:站在抽象的層次理解計算機的工作原理,CPU 如何取指執行(這個可以說是現代計算機工作的本質),內存如何工作,高速緩存如何工作,中斷的原理,外設如何協同並行工作等等;

  • C 語言:這個不用說了,肯定最重要的,C 語言玩得溜,可以省大量時間;

  • 數據結構及算法:Linux 里可以說是各種數據結構和算法的大雜燴,你能想到的里面都有,同樣這個玩得溜,可以省大量時間;

  • 匯編語言(計算機體系結構):匯編其實很簡單,沒什么好學的,這是要與某一個結構體系緊密結合(基本都 x86 最熟吧),不用強記(記也記不住),只要混個臉熟就好,需要用的時候查手冊即可,主要是結構體系的原理,高速緩存、緩存一致,流水線原理;

  • 操作系統原理:理論指導實踐,有了理論,才容易形成藍圖。而學習 Linux 內核只是實踐。

2、大局觀,抓主線,雖然 Linux 內核代碼將近 800MB,其實大部分不怎么需要看。網上很多教程,其實都不怎么好,要么泛泛而談,要么講些過時的(很多將0.11版的內核,個人覺得沒啥價值,純屬浪費時間),要么一下子就從某一結構體系講起,初學者很容易被繞暈,還有些直接就從怎么自己寫一個操作系統開始,我們要學的是 Linux 內核,一開始講這些個人覺得沒學會走路就學飛;不可否認,講這些教程的人也許很牛,但個人認為不是一個好老師。所以:

  • 我們學 Linux 的目的是什么,不同的人有不同的需求,像 Java 過來的新生代農民工,應該着重學習 Linux 內核的設計哲學,例如 kernel 是如何能像我們 Java 面向對象一樣,與各種結構體系(arch)完美適配的,設計的哲學,這些都是網上那些視頻沒講的。再進一步就是細致到進程管理、內存管理、磁盤這些怎么管理,學會這些,那些老喜歡被問的什么 kafka 原理啊、零拷貝啊這些簡直就是小菜。作為 Javaer,工作的環境就是 Linux 內核,因此,Linux 太重要了,能學多深就學多深。

  • 要多想象,根據上面的基礎知識,想象,愛因斯坦也說過,想象力比知識跟重要。所以,我們在學習 Linux 內核時要多想象,猜測,帶着問題去學,驗證;

  • Linux 是一個巨復雜的系統,Javaer 更應該學習的是如何應對復雜系統的方法;

  • 上面三點個人才覺得是一個工程師最有價值的地方,這些工程師才是工匠。

3、多動手,搭建環境學習源碼,多編寫代碼驗證,特別是從 Java 轉過來的。“紙上得來終覺淺,絕知此事要躬行”。

4、由於筆者也是剛剛才開始學 Linux 內核不久,水平有限,有不正確的地方多多交流,不勝感激。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM