eBPF+內(nèi)核調(diào)優(yōu):動(dòng)態(tài)追蹤內(nèi)存碎片的終極武器
在計(jì)算機(jī)系統(tǒng)中,內(nèi)存管理至關(guān)重要,而內(nèi)存碎片是影響其效率的關(guān)鍵問題。內(nèi)存碎片指內(nèi)存分配和釋放過程中,產(chǎn)生的無法有效利用的小塊內(nèi)存。它分為內(nèi)部和外部碎片。內(nèi)部碎片是進(jìn)程已分配卻未用完的內(nèi)存,比如租大倉庫只使用部分空間。外部碎片則是內(nèi)存中有零散空閑塊,但因不連續(xù)無法滿足大內(nèi)存請求,像零散拼圖拼不成畫。
內(nèi)存碎片危害大,會(huì)降低內(nèi)存利用率,增加程序響應(yīng)時(shí)間,嚴(yán)重時(shí)導(dǎo)致內(nèi)存分配失敗,引發(fā)系統(tǒng)崩潰。傳統(tǒng)內(nèi)存管理技術(shù)難以徹底解決該問題,隨著應(yīng)用程序復(fù)雜,內(nèi)存碎片問題愈發(fā)嚴(yán)重。這時(shí),eBPF 技術(shù)出現(xiàn)了。它能深入系統(tǒng)底層,精準(zhǔn)追蹤內(nèi)存分配和釋放過程,幫助剖析內(nèi)存碎片成因,為解決這一難題提供新方向。
一、eBPF技術(shù)簡介最后
了解內(nèi)存碎片問題后,再來認(rèn)識 eBPF 技術(shù)。它最初用于網(wǎng)絡(luò)數(shù)據(jù)包過濾,如今已演變成通用執(zhí)行引擎。能在內(nèi)核構(gòu)建安全虛擬機(jī)環(huán)境,在不改動(dòng)內(nèi)核源碼、不重啟系統(tǒng)的情況下,動(dòng)態(tài)加載自定義程序,精準(zhǔn)追蹤內(nèi)存分配與釋放,進(jìn)而解決內(nèi)存碎片問題 。
1.1 eBPF的起源與發(fā)展
eBPF,全稱是擴(kuò)展伯克利數(shù)據(jù)包過濾器(Extended Berkeley Packet Filter) ,它的故事始于 1992 年。當(dāng)時(shí),Steven McCanne 和 Van Jacobson 為了解決網(wǎng)絡(luò)數(shù)據(jù)包過濾的效率問題,開發(fā)了 BPF。最初的 BPF 就像是一個(gè)專注于網(wǎng)絡(luò)過濾的工匠,它允許用戶在內(nèi)核空間運(yùn)行過濾代碼,大大提高了網(wǎng)絡(luò)數(shù)據(jù)包處理的效率,相比當(dāng)時(shí)的其他技術(shù)快了 20 倍之多。tcpdump 就是基于 BPF 技術(shù)實(shí)現(xiàn)的,它讓網(wǎng)絡(luò)管理員能夠方便地捕獲和分析網(wǎng)絡(luò)數(shù)據(jù)包,就像用一把精準(zhǔn)的手術(shù)刀,對網(wǎng)絡(luò)流量進(jìn)行精細(xì)的剖析。
隨著時(shí)間的推移,計(jì)算機(jī)系統(tǒng)變得越來越復(fù)雜,應(yīng)用場景也越來越多樣化。傳統(tǒng)的 BPF 逐漸顯露出它的局限性,它就像一個(gè)只能在特定領(lǐng)域發(fā)揮作用的專家,難以滿足日益增長的內(nèi)核編程需求。于是,在 2014 年,eBPF 應(yīng)運(yùn)而生。它就像是 BPF 的一次華麗升級,對傳統(tǒng)的 BPF 進(jìn)行了全面擴(kuò)展,使其從單純的網(wǎng)絡(luò)過濾工具,轉(zhuǎn)變?yōu)橐环N通用的內(nèi)核編程技術(shù),擁有了更強(qiáng)大的功能和更廣泛的應(yīng)用領(lǐng)域。
1.2 eBPF的工作原理
eBPF 的工作原理可以用一個(gè)形象的比喻來理解。假設(shè)內(nèi)核是一個(gè)龐大而復(fù)雜的工廠,里面有許多不同的生產(chǎn)線(內(nèi)核子系統(tǒng))在運(yùn)作。eBPF 就像是一個(gè)靈活的機(jī)器人,它可以根據(jù)你的需求,被派往不同的生產(chǎn)線(掛載到內(nèi)核鉤子上),執(zhí)行特定的任務(wù)(運(yùn)行用戶定義的代碼)。
具體來說,開發(fā)人員首先使用高級語言(如 C 語言)編寫 eBPF 程序,這個(gè)程序就像是給機(jī)器人下達(dá)的任務(wù)指令。然后,通過 LLVM 等編譯器,將 eBPF 程序編譯成字節(jié)碼,這就好比將任務(wù)指令翻譯成機(jī)器人能夠理解的機(jī)器語言。接著,這些字節(jié)碼被加載到內(nèi)核中,在 eBPF 虛擬機(jī)中運(yùn)行。在加載之前,字節(jié)碼會(huì)經(jīng)過嚴(yán)格的驗(yàn)證,確保它不會(huì)對內(nèi)核的安全性和穩(wěn)定性造成威脅,就像在機(jī)器人進(jìn)入工廠之前,要對它進(jìn)行全面的安全檢查,防止它攜帶危險(xiǎn)物品進(jìn)入工廠。
當(dāng)內(nèi)核中發(fā)生特定的事件(如網(wǎng)絡(luò)數(shù)據(jù)包的接收、系統(tǒng)調(diào)用的發(fā)生等)時(shí),掛載在相應(yīng)鉤子上的 eBPF 程序就會(huì)被觸發(fā)執(zhí)行,它可以訪問內(nèi)核中的數(shù)據(jù)結(jié)構(gòu),根據(jù)預(yù)設(shè)的邏輯進(jìn)行處理,然后將結(jié)果返回或者存儲(chǔ)在特定的映射(Map)中,以便用戶空間的程序可以獲取這些信息。
1.3 eBPF的獨(dú)特優(yōu)勢
與傳統(tǒng)的追蹤技術(shù)相比,eBPF 在性能、安全性、靈活性等方面都展現(xiàn)出了巨大的優(yōu)勢。
在性能方面,eBPF 程序直接在內(nèi)核空間運(yùn)行,避免了頻繁的用戶態(tài)和內(nèi)核態(tài)之間的上下文切換,大大提高了執(zhí)行效率。這就好比在一個(gè)工廠中,傳統(tǒng)的方式是工人要頻繁地在不同的車間(用戶態(tài)和內(nèi)核態(tài))之間來回奔波傳遞信息,效率低下;而 eBPF 則像是在車間內(nèi)部直接設(shè)置了一個(gè)快速通道,信息可以直接在內(nèi)部傳遞,大大提高了工作效率。
安全性是 eBPF 的另一大亮點(diǎn)。在加載到內(nèi)核之前,eBPF 程序會(huì)經(jīng)過嚴(yán)格的驗(yàn)證器檢查,確保程序不會(huì)訪問非法內(nèi)存、陷入無限循環(huán)或者執(zhí)行其他危險(xiǎn)操作,從而避免了內(nèi)核崩潰或安全漏洞的出現(xiàn)。這就像一個(gè)嚴(yán)格的安檢系統(tǒng),對進(jìn)入工廠的每一個(gè)物品(eBPF 程序)都進(jìn)行仔細(xì)檢查,只有通過安檢的物品才能進(jìn)入工廠,確保了工廠的安全運(yùn)行。
eBPF 還具有極高的靈活性。它支持多種類型的映射,用于在用戶空間和內(nèi)核空間之間高效地共享數(shù)據(jù),并且可以通過事件驅(qū)動(dòng)的方式,在不同的內(nèi)核事件發(fā)生時(shí)觸發(fā)執(zhí)行。這使得開發(fā)人員可以根據(jù)具體的需求,靈活地編寫 eBPF 程序,實(shí)現(xiàn)各種復(fù)雜的功能。例如,在網(wǎng)絡(luò)監(jiān)控中,可以根據(jù)不同的網(wǎng)絡(luò)事件,實(shí)時(shí)地調(diào)整監(jiān)控策略;在性能分析中,可以針對不同的性能指標(biāo),進(jìn)行定制化的分析。
正是這些獨(dú)特的優(yōu)勢,使得 eBPF 在眾多的內(nèi)核編程技術(shù)中脫穎而出,成為了解決內(nèi)存碎片問題的理想選擇。
1.4 ebpf環(huán)境搭建
①編譯運(yùn)行源碼samples/bpf中的代碼
- 下載內(nèi)核源碼并解壓
- /bin/sh: scripts/mod/modpost: No such file or directory 遇到這種錯(cuò)誤,需要make scripts
- make M=samples/bpf 需要.config文件,需要保證這些項(xiàng)存在
- 遇到錯(cuò)誤libcrypt1.so.1 not found,執(zhí)行如下代碼:
$ cd /tmp$ apt -y download libcrypt1$ dpkg-deb -x libcrypt1_1%3a4.4.25-2_amd64.deb .$ cp -av lib/x86_64-linux-gnu/* /lib/x86_64-linux-gnu/$ apt -y --fix-broken install
5.編譯成功,可以執(zhí)行samples/bpf中的可執(zhí)行文件。
②編譯運(yùn)行自己開發(fā)的代碼
1. 下載linux source code,編譯內(nèi)核并升級
git clone https://github.com/torvalds/linux.gitcd linux/git checkout -b v5.0 v5.0
配置文件
cp -a /boot/config-4.14.81.bm.15-amd64 ./.configecho 'CONFIG_BPF=yCONFIG_BPF_SYSCALL=yCONFIG_BPF_JIT=yCONFIG_HAVE_EBPF_JIT=yCONFIG_BPF_EVENTS=yCONFIG_FTRACE_SYSCALLS=yCONFIG_FUNCTION_TRACER=yCONFIG_HAVE_DYNAMIC_FTRACE=yCONFIG_DYNAMIC_FTRACE=yCONFIG_HAVE_KPROBES=yCONFIG_KPROBES=yCONFIG_KPROBE_EVENTS=yCONFIG_ARCH_SUPPORTS_UPROBES=yCONFIG_UPROBES=yCONFIG_UPROBE_EVENTS=yCONFIG_DEBUG_FS=yCONFIG_DEBUG_INFO_BTF=y' >> ./.config
需要添加sid源安裝dwarves
apt install dwarvesmake oldconfigapt install libssl-devmakemake modules_installmake installreboot
此時(shí):
uname -aLinux n231-238-061 5.0.0 #1 SMP Mon Dec 13 05:38:52 UTC 2021 x86_64 GNU/Linux
③編譯bpf helloworld
切換到helloworld目錄
sed -i 's;/kernel-src;/root/linux;' Makefilemake
cp /root/linux/include/uapi/linux/bpf.h /usr/include/linux/bpf.h
執(zhí)行./monitor-exec,有報(bào)錯(cuò)
./monitor-exec: error while loading shared libraries: libbpf.so: cannot open shared object file: No such file or directory
④解決方法
cd /root/linux/tools/lib/bpf/makemake install
在 /etc/ld.so.conf 中添加 /usr/local/lib64這一行,運(yùn)行 sudo ldconfig 重新生成動(dòng)態(tài)庫配置信息。
~/linux/tools/lib/bpf# ldconfig -v 2>/dev/null | grep libbpf libbpf.so.0 -> libbpf.so.0.5.0 libbpf.so -> libbpf.so
⑤最終執(zhí)行情況:
可能需要安裝apt-get install gcc-multilib g++-multilib。
二、eBPF框架
在開始說明之前先解釋下eBPF上的名詞,來幫忙更好地理解:
- eBPF bytecode:將C語言寫的鉤子代碼,通過clang編譯成二進(jìn)制字節(jié)碼,通過程序加載到內(nèi)核中,鉤子觸發(fā)后在kernel "虛擬機(jī)"中運(yùn)行。
- JIT: Just-in-time compilation,將字節(jié)碼編譯成本地機(jī)器碼來提升運(yùn)行速度,和Java中的概念類似。
- Maps:鉤子代碼可以將一些統(tǒng)計(jì)類信息保存在鍵值對的map中,來與用戶空間程序進(jìn)行通信,傳遞數(shù)據(jù)。
- 關(guān)于eBPF機(jī)制詳細(xì)的講解網(wǎng)上有很多,這里就不展開了,這里先上一張圖,這里包括了使用或者編寫ebpf涉及到的所有東西,下面會(huì)對這個(gè)圖進(jìn)行詳細(xì)的講解。
oo_kern.c 鉤子實(shí)現(xiàn)代碼,主要負(fù)責(zé):
- 聲明使用的Map節(jié)點(diǎn)
- 聲明鉤子掛載點(diǎn)及處理函數(shù)
通過LLVM/clang編譯成字節(jié)碼
- 編譯命令:clang --target=bpf
- android平臺(tái)有集成eBPF的編譯,后文會(huì)提到
foo_user.c 用戶空間處理函數(shù),主要負(fù)責(zé):
- 將foo_kern.c 編譯成的字節(jié)碼加載到kenel中
- 讀取Map中的信息并處理輸出給用戶
kernel當(dāng)收到eBPF的加載請求時(shí),會(huì)先對字節(jié)碼進(jìn)行驗(yàn)證,并通過JIT編譯為機(jī)器碼,當(dāng)鉤子事件來臨后,調(diào)用鉤子函數(shù),kernel會(huì)對加載的字節(jié)碼進(jìn)行驗(yàn)證,來保證系統(tǒng)的安全性,主要驗(yàn)證規(guī)則如下:
a. 檢查是否聲明了GNU GPL,檢查kernel的版本是否支持
b. 函數(shù)調(diào)用規(guī)則:
允許bpf函數(shù)之間的相互調(diào)用,只允許調(diào)用kernel允許的BPF helper函數(shù),具體可以參考linux/bpf.h文件,上述以外的函數(shù)及動(dòng)態(tài)鏈接都是不允許的。
c. 流程處理規(guī)則:
不允許使用loop循環(huán)以防止進(jìn)入死循環(huán)卡死kernel
不允許有不可到達(dá)的分支代碼
d. 堆棧大小被限制在MAX_BPF_STACK范圍內(nèi)。
e. 編譯的字節(jié)碼大小被限制在BPF_COMPLEXITY_LIMIT_INSNS范圍內(nèi)。
鉤子掛載點(diǎn),主要包括:
另外在kernel的源代碼中samples/bpf目錄下有大量的示例,感興趣的可以閱讀下。
三、eBPF動(dòng)態(tài)追蹤內(nèi)存碎片實(shí)戰(zhàn)
理論鋪墊之后,現(xiàn)在就來進(jìn)入實(shí)戰(zhàn)環(huán)節(jié),看看如何使用 eBPF 來動(dòng)態(tài)追蹤內(nèi)存碎片。這就像是一位醫(yī)生拿著先進(jìn)的醫(yī)療設(shè)備,深入到系統(tǒng)的 “身體” 內(nèi)部,精確地找出內(nèi)存碎片這個(gè) “病癥” 的根源。
3.1 準(zhǔn)備工作
在開始使用 eBPF 追蹤內(nèi)存碎片之前,首先要確保你的系統(tǒng)滿足一些基本的條件。
需要有一個(gè)支持 eBPF 的 Linux 內(nèi)核版本,一般來說,4.1 及以上版本的內(nèi)核都已經(jīng)支持 eBPF,但為了獲得更好的性能和更多的功能,建議使用 4.9 及以上版本的內(nèi)核 。你可以通過以下命令查看當(dāng)前系統(tǒng)的內(nèi)核版本:
uname -r
還需要安裝 BPF 編譯器集合(BCC,BPF Compiler Collection)工具集。BCC 是一個(gè)基于 Python 和 C 的開發(fā)工具,它提供了一系列的庫和工具,使得編寫和運(yùn)行 eBPF 程序變得更加簡單和高效。在大多數(shù) Linux 發(fā)行版中,可以通過包管理器來安裝 BCC。例如,在 Ubuntu 系統(tǒng)上,可以使用以下命令進(jìn)行安裝:
sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)
在 CentOS 系統(tǒng)上,可以使用以下命令:
sudo yum install bcc-tools bpftool python3-bcc
3.2 編寫eBPF程序
準(zhǔn)備工作完成后,接下來就可以開始編寫 eBPF 程序了。我們的目標(biāo)是編寫一個(gè)能夠追蹤內(nèi)存分配和釋放函數(shù)的 eBPF 程序,通過這個(gè)程序,我們可以獲取內(nèi)存分配和釋放的相關(guān)信息,進(jìn)而分析內(nèi)存碎片的情況。
以下是一個(gè)簡單的用 C 語言編寫的 eBPF 程序示例,它可以追蹤kmalloc(內(nèi)核空間內(nèi)存分配函數(shù))和kfree(內(nèi)核空間內(nèi)存釋放函數(shù)):
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
// 定義一個(gè)用于存儲(chǔ)內(nèi)存分配信息的結(jié)構(gòu)體
struct alloc_info {
u32 pid; // 進(jìn)程ID
u64 size; // 分配的內(nèi)存大小
u64 timestamp; // 分配時(shí)間
char comm[TASK_COMM_LEN]; // 進(jìn)程名稱
};
// 定義一個(gè)用于存儲(chǔ)內(nèi)存釋放信息的結(jié)構(gòu)體
struct free_info {
u32 pid;
u64 timestamp;
char comm[TASK_COMM_LEN];
};
// 創(chuàng)建一個(gè)BPF哈希表,用于存儲(chǔ)內(nèi)存分配信息
BPF_HASH(alloc_map, u64, struct alloc_info);
// 創(chuàng)建一個(gè)BPF環(huán)形緩沖區(qū),用于輸出內(nèi)存釋放信息
BPF_PERF_OUTPUT(free_events);
// 定義一個(gè)kprobe,在kmalloc函數(shù)被調(diào)用時(shí)觸發(fā)
int kprobe__kmalloc(struct pt_regs *ctx, size_t size) {
u64 ip = bpf_get_current_retval(ctx); // 獲取函數(shù)返回值,即分配的內(nèi)存地址
if (!ip) return 0;
struct alloc_info info = {};
info.pid = bpf_get_current_pid_tgid() >> 32;
info.size = size;
info.timestamp = bpf_ktime_get_ns();
bpf_get_current_comm(&info.comm, sizeof(info.comm));
alloc_map.update(&ip, &info); // 將分配信息存儲(chǔ)到哈希表中
return 0;
}
// 定義一個(gè)kprobe,在kfree函數(shù)被調(diào)用時(shí)觸發(fā)
int kprobe__kfree(struct pt_regs *ctx) {
u64 ip = PT_REGS_PARM1(ctx); // 獲取kfree函數(shù)的第一個(gè)參數(shù),即要釋放的內(nèi)存地址
if (!ip) return 0;
struct alloc_info *info = alloc_map.lookup(&ip);
if (!info) return 0;
struct free_info free_info = {};
free_info.pid = info->pid;
free_info.timestamp = bpf_ktime_get_ns();
__builtin_memcpy(&free_info.comm, &info->comm, sizeof(free_info.comm));
free_events.perf_submit(ctx, &free_info, sizeof(free_info)); // 將釋放信息輸出到環(huán)形緩沖區(qū)
alloc_map.delete(&ip); // 從哈希表中刪除已釋放的內(nèi)存分配信息
return 0;
}
在這段代碼中,我們定義了兩個(gè)結(jié)構(gòu)體alloc_info和free_info,分別用于存儲(chǔ)內(nèi)存分配和釋放的相關(guān)信息。然后,我們創(chuàng)建了一個(gè) BPF 哈希表alloc_map,用于存儲(chǔ)內(nèi)存分配信息,以及一個(gè) BPF 環(huán)形緩沖區(qū)free_events,用于輸出內(nèi)存釋放信息。
通過kprobe__kmalloc和kprobe__kfree這兩個(gè)函數(shù),我們分別在kmalloc和kfree函數(shù)被調(diào)用時(shí),捕獲相關(guān)信息,并進(jìn)行相應(yīng)的處理。在kmalloc函數(shù)中,我們獲取分配的內(nèi)存地址、大小、進(jìn)程 ID、進(jìn)程名稱和分配時(shí)間,并將這些信息存儲(chǔ)到哈希表中;在kfree函數(shù)中,我們獲取要釋放的內(nèi)存地址,從哈希表中查找對應(yīng)的分配信息,然后將釋放信息輸出到環(huán)形緩沖區(qū),并從哈希表中刪除該分配信息。
3.3 加載與運(yùn)行
編寫好 eBPF 程序后,接下來就需要將其加載到內(nèi)核中,并運(yùn)行起來。這一步可以通過 BCC 工具集中的 Python 庫來實(shí)現(xiàn)。以下是一個(gè)簡單的 Python 腳本,用于加載和運(yùn)行上述 eBPF 程序:
from bcc import BPF
import time
# 加載eBPF程序
b = BPF(src_file="memory_trace.c")
# 定義一個(gè)回調(diào)函數(shù),用于處理從環(huán)形緩沖區(qū)中讀取的內(nèi)存釋放信息
def print_free_event(cpu, data, size):
event = b["free_events"].event(data)
print(f"PID: {event.pid}, COMM: {event.comm.decode('utf-8')}, FREE_TIME: {event.timestamp / 1e9:.6f} s")
# 打開環(huán)形緩沖區(qū),并注冊回調(diào)函數(shù)
b["free_events"].open_perf_buffer(print_free_event)
print("Tracing memory allocations and frees... Ctrl+C to end.")
try:
while True:
b.perf_buffer_poll()
time.sleep(1)
except KeyboardInterrupt:
pass
在這個(gè)腳本中,我們首先使用BPF類加載了 eBPF 程序文件memory_trace.c。然后,我們定義了一個(gè)回調(diào)函數(shù)print_free_event,用于處理從環(huán)形緩沖區(qū)中讀取的內(nèi)存釋放信息。這個(gè)函數(shù)會(huì)打印出進(jìn)程 ID、進(jìn)程名稱和釋放時(shí)間。
接著,我們使用open_perf_buffer方法打開環(huán)形緩沖區(qū),并將回調(diào)函數(shù)注冊到緩沖區(qū)上。這樣,當(dāng)有新的內(nèi)存釋放信息寫入環(huán)形緩沖區(qū)時(shí),回調(diào)函數(shù)就會(huì)被自動(dòng)調(diào)用。
最后,我們通過一個(gè)無限循環(huán),不斷調(diào)用perf_buffer_poll方法來輪詢環(huán)形緩沖區(qū),檢查是否有新的事件到來。當(dāng)用戶按下Ctrl+C時(shí),程序會(huì)捕獲到KeyboardInterrupt異常,從而退出循環(huán),結(jié)束程序的運(yùn)行。
運(yùn)行這個(gè) Python 腳本后,eBPF 程序就會(huì)開始追蹤內(nèi)存分配和釋放的操作。每當(dāng)有內(nèi)存被釋放時(shí),回調(diào)函數(shù)就會(huì)被觸發(fā),打印出相應(yīng)的信息。通過分析這些信息,我們就可以了解內(nèi)存的使用情況,進(jìn)而找出內(nèi)存碎片產(chǎn)生的原因和規(guī)律。
四、eBPF在Android平臺(tái)的使用
經(jīng)過上面枯燥的講解,大家應(yīng)該對eBPF有了基礎(chǔ)的認(rèn)識,下面我們就來通過android平臺(tái)上的一個(gè)監(jiān)控性能的小例子來實(shí)操下。這個(gè)小例子的需求是統(tǒng)計(jì)系統(tǒng)中每個(gè)應(yīng)用在一段時(shí)間內(nèi)系統(tǒng)調(diào)用的次數(shù)。
4.1 android系統(tǒng)對eBPF的編譯支持
目前android編譯系統(tǒng)已經(jīng)對eBPF進(jìn)行了集成,通過android.bp就能很方便的在android源代碼中編譯eBPF的字節(jié)碼。
android.bp示例:
圖片
相關(guān)的編譯代碼在soong的bpf.go,雖然google關(guān)于soong的文檔很少,但是至少代碼是比較清晰的。
圖片
這里的$ccCmd一般是clang, 所以它的編譯命令主要是clang --target=bpf。和普通的bpf編譯沒有區(qū)別。
4.2 eBPF鉤子代碼實(shí)現(xiàn)
解決了編譯問題,下一步我們開始實(shí)現(xiàn)鉤子代碼,我們準(zhǔn)備使用tracepoint鉤子,首先要找到我們需要的tracepoint函數(shù)sys_enter和sys_exit。
函數(shù)定義在include/trace/events/syscalls.h文件sys_enter的trace參數(shù)是id 和長度為6的數(shù)組,sys_exit的trace參數(shù)是兩個(gè)長整形數(shù) id 和ret。
找到了鉤子后,下一步就可以編寫鉤子處理代碼了:
定義map保存系統(tǒng)調(diào)用統(tǒng)計(jì)信息,在DEFINE_BPF_MAP聲明map的同時(shí),也會(huì)生成刪,改,查的宏函數(shù),例如本例中會(huì)生成如下函數(shù):
bpf_pid_syscall_map_lookup_elem
bpf_pid_syscall_map_update_elem
bpf_pid_syscall_map_delete_elem
- 定義回調(diào)函數(shù)參數(shù)類型,需要參考前面的tracepoint的定義。
- 指定監(jiān)聽的tracepoint事件。
- 使用bpf_trace_printk函數(shù)打印debug信息,會(huì)直接打印信息到ftrace中。
- 在map中查找指定key。
- 更新指定的key的值。
4.3 加載鉤子代碼
我們只需要把我們編譯出來的*.o文件push到手機(jī)的system/etc/bpf目錄下,重啟手機(jī),系統(tǒng)會(huì)自動(dòng)加載我們的鉤子文件,加載成功后會(huì)在 /sys/fs/bpf目錄下顯示我們定義的map及prog文件。
系統(tǒng)加載代碼在system/bpf/bpfloader中,代碼很簡單。
主要有如下操作:
1)在early-init階段向下面兩個(gè)節(jié)點(diǎn)寫1
– /proc/sys/net/core/bpf_jit_enable
使能eBPF JIT,當(dāng)內(nèi)核設(shè)定BPF_JIT_ALWAYS_ON的時(shí)候,默認(rèn)為1
– /proc/sys/net/core/bpf_jit_kallsyms
使特權(quán)用戶可以通過kallsyms節(jié)點(diǎn)讀取kernel的symbols
2)啟動(dòng)bpfloader service
- 讀取system/etc/bpf目錄下的*.o文件,調(diào)用libbpf_android.so中的loadProg函數(shù)加載進(jìn)內(nèi)核。
- 生成相應(yīng)的/sys/fs/bpf/節(jié)點(diǎn)。
- 設(shè)置屬性bpf.progs_loaded為1
sys節(jié)點(diǎn)分為map節(jié)點(diǎn)和prog節(jié)點(diǎn)兩種, 分別為map_<filename>_<mapname>, prog_<filename>_<mapname>
下面是Android Q版本上的節(jié)點(diǎn)信息。
可以使用下面的命令調(diào)試動(dòng)態(tài)加載
4.4 用戶空間程序?qū)崿F(xiàn)
下面我們需要編寫用戶空間的顯示程序,本質(zhì)上就是在用戶態(tài)通過系統(tǒng)調(diào)用把BPF map給讀出來
1)eBPF統(tǒng)計(jì)只有在調(diào)用bpf_attach_tracepoint只有才會(huì)起作用。bpf_attach_tracepoint是bcc里面的函數(shù),android將bcc的一部分內(nèi)容打包成了libbpf,放到了系統(tǒng)庫里面。
2)取得map的fd, bpf_obj_get會(huì)直接調(diào)用bpf的系統(tǒng)調(diào)用。
3)將fd包裝成BpfMap,android在BpfMap.h中定義了很多方便的函數(shù)。
4)遍歷map回調(diào)函數(shù)。返回值必須是android::netdutils::status::ok(在android的新版本中已經(jīng)進(jìn)行修改)。
4.5 運(yùn)行結(jié)果查看
直接在目錄下執(zhí)行mm,將編譯出來的bpf.o push到/system/etc/bpf目錄下,將統(tǒng)計(jì)程序push到/system/bin目錄下,重啟,看下結(jié)果。
前面的是pid, 后面的是系統(tǒng)調(diào)用次數(shù)。至此,如何在android平臺(tái)使用eBPF實(shí)現(xiàn)統(tǒng)計(jì)系統(tǒng)中每個(gè)pid在一段時(shí)間內(nèi)系統(tǒng)調(diào)用的次數(shù)的功能就介紹完了。
此外還有很多技術(shù)細(xì)節(jié)沒有深入研究,不過畢竟只是初探,就先講到這里了,后續(xù)有時(shí)間再進(jìn)一步深入研究。研究的時(shí)間還是比較短,如果有任何錯(cuò)誤的地方歡迎指正。
五、內(nèi)核調(diào)優(yōu):與eBPF協(xié)同作戰(zhàn)
5.1 內(nèi)存相關(guān)內(nèi)核參數(shù)調(diào)整
在解決內(nèi)存碎片問題的過程中,eBPF 就像是我們手中的精密探測器,能夠深入系統(tǒng)底層獲取關(guān)鍵數(shù)據(jù),而內(nèi)核參數(shù)調(diào)整則是對系統(tǒng)內(nèi)存管理機(jī)制的宏觀調(diào)控,兩者相互配合,才能達(dá)到最佳的優(yōu)化效果。下面來介紹一些與內(nèi)存管理密切相關(guān)的內(nèi)核參數(shù),以及它們?nèi)绾闻c eBPF 協(xié)同工作。
swappiness是一個(gè)重要的內(nèi)核參數(shù),它的值介于 0 - 100 之間,用于控制系統(tǒng)將內(nèi)存數(shù)據(jù)交換到磁盤交換空間(swap)的傾向程度 。默認(rèn)情況下,許多 Linux 發(fā)行版的swappiness值設(shè)置為 60。當(dāng)swappiness的值較高時(shí),系統(tǒng)會(huì)更積極地將內(nèi)存中不常使用的數(shù)據(jù)移動(dòng)到交換空間,以釋放物理內(nèi)存供其他進(jìn)程使用;相反,當(dāng)swappiness的值較低時(shí),系統(tǒng)會(huì)盡量保留數(shù)據(jù)在物理內(nèi)存中,減少磁盤 I/O 操作。
例如,對于一些對磁盤 I/O 性能敏感的應(yīng)用場景,如數(shù)據(jù)庫服務(wù)器,將swappiness值降低到 10 或 20,可以有效減少因頻繁交換數(shù)據(jù)而導(dǎo)致的性能下降。通過 eBPF 獲取的內(nèi)存使用和交換空間使用情況數(shù)據(jù),可以幫助我們更準(zhǔn)確地判斷當(dāng)前swappiness值是否合適,從而進(jìn)行相應(yīng)的調(diào)整。
overcommit_memory則是另一個(gè)關(guān)鍵的內(nèi)核參數(shù),它決定了內(nèi)核對于內(nèi)存分配請求的處理策略,有三個(gè)可選值:0、1 和 2 。當(dāng)overcommit_memory設(shè)置為 0 時(shí),內(nèi)核會(huì)采用一種啟發(fā)式的策略來判斷內(nèi)存分配請求是否合理。它會(huì)檢查系統(tǒng)當(dāng)前的可用內(nèi)存、交換空間以及內(nèi)存使用趨勢等因素,如果內(nèi)核認(rèn)為分配請求可能導(dǎo)致系統(tǒng)內(nèi)存不足,就會(huì)拒絕該請求。這種策略相對保守,適用于對內(nèi)存穩(wěn)定性要求較高的系統(tǒng)。當(dāng)設(shè)置為 1 時(shí),內(nèi)核會(huì)允許分配所有的物理內(nèi)存,而不管當(dāng)前的內(nèi)存狀態(tài)如何,即對內(nèi)存申請來者不拒。
這種策略在某些情況下可以避免因內(nèi)存分配失敗而導(dǎo)致的程序異常,但也增加了系統(tǒng)發(fā)生內(nèi)存耗盡(OOM,Out - Of - Memory)的風(fēng)險(xiǎn)。若設(shè)置為 2,內(nèi)核會(huì)嚴(yán)格限制內(nèi)存分配,不允許分配超過所有物理內(nèi)存和交換空間總和的內(nèi)存,即禁止 overcommit。通過 eBPF 對內(nèi)存分配和使用情況的實(shí)時(shí)追蹤,我們可以了解系統(tǒng)中各個(gè)進(jìn)程的內(nèi)存需求模式,從而根據(jù)實(shí)際情況選擇合適的overcommit_memory值,在保證系統(tǒng)穩(wěn)定性的同時(shí),充分利用內(nèi)存資源。
5.2 eBPF 與內(nèi)核調(diào)優(yōu)的配合策略
eBPF 與內(nèi)核調(diào)優(yōu)之間的配合是一個(gè)動(dòng)態(tài)的、相互反饋的過程。通過 eBPF 程序?qū)?nèi)存分配、釋放以及內(nèi)存碎片情況的實(shí)時(shí)追蹤,我們可以獲取大量的詳細(xì)數(shù)據(jù),這些數(shù)據(jù)就像是系統(tǒng)內(nèi)存狀態(tài)的 “實(shí)時(shí)監(jiān)控畫面”,為內(nèi)核調(diào)優(yōu)提供了堅(jiān)實(shí)的數(shù)據(jù)基礎(chǔ)。
當(dāng) eBPF 監(jiān)測到系統(tǒng)中內(nèi)存碎片的比例逐漸增加,導(dǎo)致內(nèi)存分配效率下降時(shí),我們可以根據(jù)這些數(shù)據(jù),針對性地調(diào)整相關(guān)內(nèi)核參數(shù)。如果發(fā)現(xiàn)由于頻繁的內(nèi)存交換導(dǎo)致內(nèi)存碎片問題加劇,就可以適當(dāng)降低swappiness的值,減少內(nèi)存與交換空間之間的數(shù)據(jù)交換,從而減少碎片的產(chǎn)生。相反,如果 eBPF 數(shù)據(jù)顯示系統(tǒng)內(nèi)存利用率較低,而有一些進(jìn)程因?yàn)閮?nèi)存分配限制而無法正常運(yùn)行,這時(shí)可以考慮適當(dāng)調(diào)整overcommit_memory的值,放寬內(nèi)存分配策略,提高內(nèi)存的利用率。
還可以結(jié)合 eBPF 獲取的進(jìn)程內(nèi)存使用信息,對不同的進(jìn)程進(jìn)行差異化的內(nèi)存管理。對于一些內(nèi)存使用量大且穩(wěn)定的進(jìn)程,可以通過內(nèi)核參數(shù)為其分配更多的固定內(nèi)存塊,避免頻繁的內(nèi)存分配和釋放操作,從而減少碎片的產(chǎn)生;對于內(nèi)存使用量較小且波動(dòng)頻繁的進(jìn)程,可以采用更靈活的內(nèi)存分配算法,提高內(nèi)存的使用效率。
在實(shí)際應(yīng)用中,eBPF 與內(nèi)核調(diào)優(yōu)的配合需要不斷地進(jìn)行測試和優(yōu)化。通過在不同的負(fù)載條件下運(yùn)行 eBPF 程序,收集系統(tǒng)性能指標(biāo)數(shù)據(jù),然后根據(jù)這些數(shù)據(jù)調(diào)整內(nèi)核參數(shù),再進(jìn)行新一輪的測試,直到系統(tǒng)性能達(dá)到最佳狀態(tài)。這種循環(huán)優(yōu)化的過程就像是一場精密的調(diào)試,需要耐心和細(xì)心,才能找到系統(tǒng)內(nèi)存管理的最優(yōu)配置。