引言
內核研究與開發是計算機底層處于與硬件打交道的部位,ebpf可以理解為是內核開發的一個模塊。在研究ebpf開發之前需要對計算機的一些基礎知識學習了解,懂得計算機的基本組成和操作系統的基本原理和運行機制,了解Linux內核設計的機制和相關源碼的閱讀與理解,再深入內核模塊觀察ebpf的設計思路,進而做到對ebpf的開發與實現。
在此之前,首先需要儲備一些基本的計算機知識。
基礎知識儲備
計算機組成原理
學習計算機組成原理可以對計算機的基礎架構有所理解,了解計算機中常見的術語和概念。
計算機組成原理知識要點見:計算機組成原理學習筆記
操作系統
操作系統作為人和計算機交互的橋梁,理解其工作原理對后續內核開發有很好的幫助,對操作系統的術語了解知道其背后的道理是開發的基礎。
操作系統知識要點見:操作系統學習筆記
C語言
C語言是Linux內核開發主要使用的編程語言和開發工具,需要熟悉其基本語法和結構。
Linux基礎
了解Linux基本組成和常用的shell命令,熟悉Linux的文件架構。
FHS(Filesystem Hierarchy Standard):
FHS依據文件系統使用的頻繁與否與是否允許使用者隨意更動, 而將目錄定義成為四種交互作用的形態,用表格來說有點像底下這樣:
無法復制加載中的內容
可分享的:可以分享給其他系統掛載使用的目錄,所以包括執行文件與用戶的郵件等數據, 是能夠分享給網絡上其他主機掛載用的目錄;
不可分享的:自己機器上面運作的裝置文件或者是與程序有關的socket文件等, 由于僅與自身機器有關,所以當然就不適合分享給其他主機了.
不變的:有些數據是不會經常變動的,跟隨著distribution而不變動. 例如函式庫、文件說明文件、系統管理員所管理的主機服務配置文件等等;
可變動的:經常改變的數據,例如登錄文件、一般用戶可自行收受的新聞組等.
事實上,FHS針對目錄樹架構僅定義出三層目錄底下應該放置什么數據而已,分別是底下這三個目錄的定義:
- / (root, 根目錄):與開機系統有關;
- /usr (unix software resource):與軟件安裝/執行有關;
- /var (variable):與系統運作過程有關.
根目錄 (/) 的意義與內容:
概要:
所有的目錄都是由根目錄衍生出來的(根目錄是整個系統最重要的一個目錄)
與開機/還原/系統修復等動作有關. (由于系統開機時需要特定的開機軟件、核心文件、開機所需程序、 函式庫等等文件數據,若系統出現錯誤時,根目錄也必須要包含有能夠修復文件系統的程序才行)
FHS標準建議:根目錄(/)所在分割槽應該越小越好, 且應用程序所安裝的軟件最好不要與根目錄放在同一個分割槽內,保持根目錄越小越好.(因為越大的分割槽妳會放入越多的數據,如此一來根目錄所在分割槽就可能會有較多發生錯誤的機會,如此不但效能較佳,根目錄所在的文件系統也較不容易發生問題.)
根目錄(/)底下目錄FHS定義的說明:
無法復制加載中的內容
除上 FHS 中定義的目錄說明外, 底下是幾個在Linux當中非常重要的目錄:
無法復制加載中的內容
不可與根目錄分開的目錄(與開機過程有關):
根目錄與開機有關,開機過程中僅有根目錄會被掛載, 其他分割槽則是在開機完成之后才會持續的進行掛載的行為.就是因為如此,因此根目錄下與開機過程有關的目錄, 就不能夠與根目錄放到不同的分割槽去!
/etc:配置文件
/bin:重要執行檔
/dev:所需要的裝置文件
/lib:執行檔所需的函式庫與核心所需的模塊
/sbin:重要的系統執行文件
/usr 的意義與內容:
概要:
依據FHS的基本定義,/usr里面放置的數據屬于可分享的與不可變動的(shareable, static), 如果你知道如何透過網絡進行分割槽的掛載,那么/usr確實可以分享給局域網絡內的其他主機來使用!
usr(Unix Software Resource 即Unix操作系統軟件資源) FHS建議所有軟件開發者,應該將他們的數據合理的分別放置到這個目錄下的次目錄,而不要自行建立該軟件自己獨立的目錄.
所有系統默認的軟件(distribution發布者提供的軟件)都會放置到/usr底下,因此這個目錄有點類似Windows 系統的『C:\Windows\ + C:\Program files\』這兩個目錄的綜合體,系統剛安裝完畢時,這個目錄會占用最多的硬盤容量.
一般來說,/usr的次目錄建議有底下這些:
無法復制加載中的內容
/var 的意義與內容:
概要:
/var目錄主要針對常態性變動的文件,包括緩存(cache)、登錄檔(log file)以及某些軟件運作所產生的文件, 包括程序文件(lock file, run file),或者例如MySQL數據庫的文件等等. 所以/var在系統運作后才會漸漸占用硬盤容量的目錄
常見的次目錄有:
無法復制加載中的內容
針對FHS,各家distributions的異同:
由于FHS僅是定義出最上層(/)及次層(/usr, /var)的目錄內容應該要放置的文件或目錄數據, 因此,在其他次目錄層級內,就可以隨開發者自行來配置了.舉例來說,CentOS的網絡設定數據放在 /etc/sysconfig/network-scripts/ 目錄下,但是SuSE則是將網絡放置在 /etc/sysconfig/network/ 目錄下,目錄名稱可是不同的呢!不過只要記住大致的FHS標準,差異性其實有限啦!
Linux 命令大全查詢表
無法復制加載中的內容
Linux內核
Linux內核學習策略
Linux學習建議配套遠古版本的Linux內核源碼學習,有助于幫助理解內核設計的思路,下載并閱讀Linux內核1.0版本的源碼去學習,該版本基本包含了內核基本部件,后續的版本都是在此基礎上擴充功能,但是基本的內在沒有變化。
內核源碼不同版本間的閱讀與對比可參考Bootlin,其中1.0源碼目錄結構如下,其中對主要文件目錄進行解釋:
無法復制加載中的內容
對照著內核設計的源代碼進行學習,會從根源上思考這樣設計的目的是什么。
Linux內核開發環境配置
內核開發環境和源碼安裝配置:Linux內核開發環境配置
Linux內核簡介
Linux 內核的用途是什么?
Linux 內核有 4 項工作:
- 內存管理:追蹤記錄有多少內存存儲了什么以及存儲在哪里
- 進程管理:確定哪些進程可以使用中央處理器(CPU)、何時使用以及持續多長時間
- 設備驅動程序:充當硬件與進程之間的調解程序/解釋程序
- 系統調用和安全防護:從流程接受服務請求
在正確實施的情況下,內核對于用戶是不可見的,它在自己的小世界(稱為內核空間)中工作,并從中分配內存和跟蹤所有內容的存儲位置。用戶所看到的內容(例如 Web 瀏覽器和文件)則被稱為用戶空間。這些應用通過系統調用接口(SCI)與內核進行交互。
舉例來說,內核就像是一個為高管(硬件)服務的忙碌的個人助理。助理的工作就是將員工和公眾(用戶)的消息和請求(進程)轉交給高管,記住存放的內容和位置(內存),并確定在任何特定的時間誰可以拜訪高管、會面時間有多長。
Linux內核學習路線和框架圖
Linux Security Coaching
Linux內核基礎學習資料
該視頻資料詳細介紹了Linux內核的知識點及其在內核中的實現進行比對,很有參考價值。
MakeFile詳解
Makefile 可以簡單的認為是一個工程文件的編譯規則,描述了整個工程的編譯和鏈接等規則。詳細介紹如下:
Makefile文件負責編寫程序的編譯與運行規則,免去命令行使用Clang去逐步編譯分析。
GDB詳解
GDB是一個強大的調試工具,通過它可以實現C程序代碼bug的調試。
Linux崩潰調試
Linux內核Crash下的問題解決方案:
EBPF基礎
什么是ebpf?
Linux 內核一直是實現監控/可觀測性、網絡和安全功能的理想地方。 不過很多情況下這并非易事,因為這些工作需要修改內核源碼或加載內核模塊, 最終實現形式是在已有的層層抽象之上疊加新的抽象。 eBPF 是一項革命性技術,它能在內核中運行沙箱程序(sandbox programs), 而無需修改內核源碼或者加載內核模塊。
eBPF 催生了一種全新的軟件開發方式。基于這種方式,我們不僅能對內核行為進行 編程,甚至還能編寫跨多個子系統的處理邏輯,而傳統上這些子系統是完全獨立、 無法用一套邏輯來處理的。
安全:
觀測和理解所有的系統調用的能力,以及在 packet 層和 socket 層審視所有的網絡操作的能力, 這兩者相結合,為系統安全提供了革命性的新方法。 以前,系統調用過濾、網絡層過濾和進程上下文跟蹤是在完全獨立的系統中完成的; eBPF 的出現統一了可觀測性和各層面的控制能力,使我們有更加豐富的上下文和更精細的控制能力, 因而能創建更加安全的系統。
網絡:
eBPF 的兩大特色 —— 可編程和高性能 —— 使它能滿足所有的網絡包處理需求。 可編程意味著無需離開內核中的包處理上下文,就能添加額外的協議解析器或任何轉發邏輯, 以滿足不斷變化的需求。高性能的 JIT 編譯器使 eBPF 程序能達到幾乎與原生編譯的內核態代碼一樣的執行性能。
跟蹤 & 性能分析:
eBPF 程序能夠加載到 trace points、內核及用戶空間應用程序中的 probe points, 這種能力使我們對應用程序的運行時行為(runtime behavior)和系統本身 (system itself)提供了史無前例的可觀測性。應用端和系統端的這種觀測能力相結合, 能在排查系統性能問題時提供強大的能力和獨特的信息。BPF 使用了很多高級數據結構, 因此能非常高效地導出有意義的可觀測數據,而不是像很多同類系統一樣導出海量的原始采樣數據。
觀測 & 監控:
相比于操作系統提供的靜態計數器(counters、gauges),eBPF 能在內核中收集和聚合自定義 metric, 并能從不同數據源來生成可觀測數據。這既擴展了可觀測性的深度,也顯著減少了整體系統開銷, 因為現在可以選擇只收集需要的數據,并且后者是直方圖或類似的格式,而非原始采樣數據。
Linux驅動模塊開發
簡介
Linux 內核的整體結構已經非常龐大,而其包含的組件也非常多。這會導致兩個問題,一是生成的內核會很大,二是如果我們要在現有的內核中新增或刪除功能,將不得不重新編譯內核。Linux 提供了這樣的一種機制,這種機制被稱為模塊(Module)。使得編譯出的內核本身并不需要包含所有功能,而在這些功能需要被使用的時候,其對應的代碼被動態地加載到內核中。
舉例
先來看一個最簡單的內核模塊“Hello World”,代碼如下:
#include <linux/init.h>
#include <linux/module.h>
static int hello_init(void) /初始化函數/
{
printk(KERN_INFO " Hello World enter\n");
return 0;
}
static void hello_exit(void) /卸載函數/
{
printk(KERN_INFO " Hello World exit\n ");
}
module_init(hello_init); /模塊初始化/
module_exit(hello_exit); /卸載模塊/
MODULE_LICENSE("Dual BSD/GPL"); /許可聲明/
MODULE_AUTHOR("Linux");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");
這個模塊定義了兩個函數, 一個在模塊加載到內核時被調用( hello_init )以及一個在模塊被去除時被調用( hello_exit ). moudle_init 和 module_exit 這幾行使用了特別的內核宏來指出這兩個函數的角色. 另一個特別的宏 (MODULE_LICENSE) 是用來告知內核, 該模塊帶有一個自由的許可證.
注:內核模塊中用于輸出的函數是內核空間的 printk()而非用戶空間的 printf(),具體用法參考附件 printk函數介紹。
幾個常用命令
加載模塊
通過“insmod ./hello.ko”命令可以加載,加載時輸出“Hello World enter”。
卸載模塊
通過“rmmod hello”命令可以卸載,卸載時輸出“Hello World exit”。
查看系統中已經加載的模塊列表
在Linux中,使用lsmod命令可以獲得系統中加載了的所有模塊以及模塊間的依賴關系,例如:
root@imx6:~$ lsmod
Module Size Used by
hello 1568 0
ohci1394 32716 0
ide_scsi 16708 0
ide_cd 39392 0
cdrom 36960 1 ide_cd
查看某個具體模塊的詳細信息
使用modinfo <模塊名>命令可以獲得模塊的信息,包括模塊作者、模塊的說明、模塊所支持 的參數以及 vermagic:
root@imx6:~$ modinfo hello.ko
filename: hello.ko
license: Dual BSD/GPL
author: Song Baohua
description: A simple Hello World Module
alias: a simplest module
vermagic: 2.6.15.5 686 gcc-3.2
depends:
Linux 內核模塊程序的結構
一個Linux內核模塊主要由如下幾個部分組成:
1、模塊加載函數(一般需要) 當通過insmod或modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。
2、模塊卸載函數(一般需要) 當通過rmmod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。
3、模塊許可證聲明(必須) 許可證(LICENSE)聲明描述內核模塊的許可權限,如果不聲明LICENSE,模塊被加載時,將收到內核被污染 (kernel tainted)的警告。在Linux 2.6內核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多數情況下,內核模塊應遵循GPL兼容許可權。Linux 2.6內核模塊最常見的是以MODULE_LICENSE( “Dual BSD/GPL” )語句聲明模塊采BSD/GPL雙LICENSE。
4、模塊參數(可選) 模塊參數是模塊被加載的時候可以被傳遞給它的值,它本身對應模塊內部的全局變量。
5、模塊導出符號(可選) 內核模塊可以導出符號(symbol,對應于函數或變量),這樣其它模塊可以使用本模塊中的變量或函數。
6、模塊作者等信息聲明(可選) 用于申明模塊作者的相關信息,一般用于備注作者姓名、郵箱等。
模塊加載函數
Linux 內核模塊加載函數一般以_ _init 標識聲明,典型的模塊加載函數如下:
static int _ _init initialization_function(void)
{
/* 初始化代碼 */
}
module_init(initialization_function);
模塊加載函數必須以“module_init(函數名)”的形式被指定。它返回整型值,若初始化成功,應返回 0。而在初始化失敗時,應該返回錯誤編碼。在 Linux 內核里,錯誤編碼是一個負值。
在 Linux 2.6 內核中,可以使用 request_module(const char *fmt, …)函數加載內核模塊,驅動開發人員可以通過調用。
request_module(module_name);
/**** 或者 ****/
request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));
注意:在 Linux 中,所有標識為_ init 的函數在連接的時候都放在.init.text 這個區段內,此外,所有的 init 函數在區段.initcall.init 中還保存了一份函數指針,在初始化時內核會通過這些函數指針調用這些 _init 函數,并在初始化完成后,釋放 init 區段(包括.init.text、.initcall.init 等)。
模塊卸載函數
static void _ _exit cleanup_function(void)
{
/* 釋放代碼 */
}
module_exit(cleanup_function);
模塊卸載函數在模塊卸載的時候執行,不返回任何值,必須以“module_exit(函數名)”的形式來指定。通常來說,模塊卸載函數要完成與模塊加載函數相反的功能,如下所示。
若模塊加載函數注冊了 XXX,則模塊卸載函數應該注銷 XXX。
若模塊加載函數動態申請了內存,則模塊卸載函數應釋放該內存。
若模塊加載函數申請了硬件資源(中斷、DMA 通道、I/O 端口和 I/O 內存等)的占用,則模塊卸載函數應釋放這些硬件資源。
若模塊加載函數開啟了硬件,則卸載函數中一般要關閉之。
模塊參數
用“module_param(參數名,參數類型,參數讀/寫權限)”為模塊定義一個參數,例如下列代碼定義了 1 個整型參數和 1 個字符指針參數:
static char *book_name = " dissecting Linux Device Driver ";
static int num = 4 000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
參數類型可以是 byte、short、ushort、int、uint、long、ulong、charp(字符指針)、bool 或 invbool(布爾的反),在模塊被編譯時會將 module_param 中聲明的類型與變量定義的類型進行比較,判斷是否一致。
在裝載內核模塊時,用戶可以向模塊傳遞參數,形式為“insmode(或 modprobe)模塊名 參數名=參數值”,如果不傳遞,參數將使用模塊內定義的缺省值。
內核模塊的符號導出
模塊可以使用如下宏導出符號到內核符號表:
EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);
導出的符號將可以被其他模塊使用,使用前聲明一下即可。EXPORT_SYMBOL_GPL()只適用于包含 GPL 許可權的模塊。
模塊聲明與描述
在Linux內核模塊中,我們可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分別聲明模塊的作者、描述、版本、設備表和別名,例如:
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
對于USB、PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE。
MakeFile
Kernel modules
obj-m += hello.o
Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules #modules表示編譯成模塊的意思
#CURDIR是make的內嵌變量,自動設置為當前目錄
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
該 Makefile 文件應該與源代碼 hello.c 位于同一目錄,開啟其中的 EXTRA_CFLAGS=-g -O0可以得到包含調試信息的 hello.ko 模塊。運行 make 命令得到的模塊可直接在 PC 上運行。
注:uname 的更多用法詳見附件
如果一個模塊包括多個.c 文件(如 file1.c、file2.c),則應該以如下方式編寫 Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o
obj-m是個makefile變量,它的值可以是一串.o文件的表列
EBPF詳解
ebpf詳細學習筆記和記錄:
EBPF(Berkeley Packet Filter)學習記錄
在這里不贅述ebpf的歷史等沒有太多學習意義的信息,主要從實際開發角度需要去展開必要介紹。
Seccross項目理解
開發記錄及相關源碼分析記錄: