最近ebpf技術(shù)的文章越來(lái)越多的出現(xiàn)在好幾個(gè)微信公眾號(hào)中,之前只是了解ebpf技術(shù)的原理,并不清楚細(xì)節(jié),所以需要實(shí)踐一下。以什么課題內(nèi)容來(lái)實(shí)踐呢,想起來(lái)之前遺留的一個(gè)問(wèn)題,如何查看文件在內(nèi)存中的緩存(當(dāng)時(shí)沒(méi)有搜索到vmtouch這個(gè)工具),所以就以這個(gè)問(wèn)題做為導(dǎo)向?qū)嵺`一下ebpf技術(shù)。
獲取文件緩存原理解析
要獲取文件在內(nèi)存中的緩存,只需要在內(nèi)核中找到該文件對(duì)應(yīng)的inode結(jié)構(gòu),然后讀取inode->i_mapping.nrpages的值,該值就是文件緩存的頁(yè)數(shù)。要實(shí)現(xiàn)這個(gè)功能,kprobe當(dāng)然是沒(méi)有問(wèn)題的,但是不夠靈活。
另外內(nèi)核中的被hook函數(shù)要選擇哪一個(gè),既可以獲取到nrpages,又不會(huì)影響內(nèi)核的性能呢?經(jīng)過(guò)分析,可以將hook函數(shù)設(shè)置為vfs_getattr_nosec,該內(nèi)核函數(shù)是用戶態(tài)執(zhí)行stat,fstat,lstat等獲取文件的屬性信息時(shí)必須調(diào)用的函數(shù),因此選擇該函數(shù)作為被hook的函數(shù)非常合適,即可以實(shí)現(xiàn)功能,又不影響性能。
細(xì)節(jié)原理可以參考之前的文章 量化分析pagecache
交付形式
最終理想的交付方式是只提供一個(gè)二進(jìn)制文件,通過(guò)執(zhí)行filecache filename即可無(wú)需等待獲取該文件占用的內(nèi)存頁(yè)數(shù)。
[root@localhost bpf]# filecache
Usage : filecache filepath
但是標(biāo)準(zhǔn)的ebpf程序的交付件是有兩個(gè),一個(gè)是用戶態(tài)執(zhí)行的二進(jìn)制程序,一個(gè)是bpf格式的kern.o文件,如何將這兩個(gè)文件進(jìn)行融合達(dá)到只有一個(gè)交付件的目的呢?這里借鑒了bcc CORE的方式,將bpf格式的kern.o轉(zhuǎn)為字符串?dāng)?shù)組寫(xiě)入到c格式的頭文件中,然后在用戶態(tài)二進(jìn)制執(zhí)行的時(shí)候?qū)⒆址當(dāng)?shù)組再轉(zhuǎn)換成kern.o文件,之后調(diào)用load_bpf_file將kern.o文件加載到內(nèi)核中。經(jīng)過(guò)搜索,xxd -i可以實(shí)現(xiàn)該需求。
實(shí)現(xiàn)細(xì)節(jié)

源代碼分為兩個(gè)文件,一個(gè)是用戶態(tài)的filecache_user.c,一個(gè)是filecache_kern.c經(jīng)過(guò)xxd轉(zhuǎn)換后的filecache.h文件 將源碼放在文章最下面,不影響閱讀體驗(yàn)。代碼解析如下 :
- filecache_kern.c中將bpf_vfs_getattr_nosec已kprobe的方式注冊(cè)到內(nèi)核中。
- 通過(guò)clang將filecache_kern.c編譯成bpf格式的.o文件。
- 通過(guò)xxd -i filecache_kern.o > filecache.h,將.o轉(zhuǎn)換為.h文件。
- 在filecache_user.c中包含該filecache.h頭文件。
- 將filecache.h中的內(nèi)容還原成filecache_kern.o,因?yàn)?.19內(nèi)核的ebpf只提供了load_ebpf_file這一個(gè)接口,這個(gè)接口的參數(shù)是文件的路徑名。
- 通過(guò)stat函數(shù)調(diào)用獲取文件的inodenum。
- 將該inodenum通過(guò)para_map傳入到內(nèi)核中。
- 再次調(diào)用stat函數(shù),觸發(fā)內(nèi)核調(diào)用1中注冊(cè)的hook函數(shù)。
- 在filecache_kern.c中,當(dāng)1中注冊(cè)的函數(shù)被觸發(fā)時(shí),獲取用戶傳過(guò)來(lái)的inodenum,并與inode->i_ino進(jìn)行對(duì)比,如果相同,則通過(guò)inode->i_mapping.nrpages將頁(yè)數(shù)寫(xiě)入pagecache_map中。
- 在filecache_user.c中,讀取pagecache_map的值,如果有值,就是文件緩存的頁(yè)數(shù),如果沒(méi)有值,則說(shuō)明文件沒(méi)有被緩存在內(nèi)存中。
- 程序結(jié)束后,由內(nèi)核自動(dòng)清理map數(shù)據(jù)(perf_event_open)。
代碼編譯
由于內(nèi)核中代碼編譯依賴的庫(kù)和頭文件系統(tǒng)比較復(fù)雜,這里仿照其他示例將文件放到samples/bpf中。
在編譯之前,獲取到內(nèi)核源碼,先進(jìn)行編譯,生成必要的頭文件,參考centos獲取指定版本內(nèi)核代碼,或者安裝kernel-devel包。還需要先進(jìn)行源碼安裝高版本clang,這里也踩過(guò)坑了clang源碼編譯。
如果不想在centos7上折騰源碼編譯,還有一個(gè)思路可以參考,在ubuntu上編譯bpf程序,在centos上編譯用戶態(tài)程序也是可以的。
做好準(zhǔn)備工作后,進(jìn)入內(nèi)核代碼目錄的samples/bpf,修改對(duì)應(yīng)的Makefile,執(zhí)行make即可生成filecache二進(jìn)制文件,且該二進(jìn)制文件可以拿到其他centos內(nèi)核版本為4.19.x的環(huán)境中直接運(yùn)行。編譯過(guò)程中可能會(huì)遇到問(wèn)題,根據(jù)錯(cuò)誤提示信息搜索一下即可找到答案,一般是缺少某些rpm,如elfutils-libelf-devel。
[root@localhost bpf]# pwd
/root/lugl/ebpf-kill-example/linux-4.19.113/samples/bpf
[root@localhost bpf]# ls -l filecache*.c
-rw-r--r-- 1 root root 1703 Jan 16 22:02 filecache_kern.c
-rw-r--r-- 1 root root 1879 Jan 16 21:56 filecache_user.c
[root@localhost bpf]# ls -l filecache*.h
-rw-r--r-- 1 root root 12217 Jan 16 21:48 filecache.h
[root@localhost bpf]# cat Makefile | grep filecache
hostprogs-y += filecache
filecache-objs := bpf_load.o filecache_user.o
always += filecache_kern.o
效果展示
在/run目錄創(chuàng)建一個(gè)測(cè)試文件,該目錄下的文件會(huì)占用內(nèi)存,且echo 3 > /proc/sys/vm/drop_caches也不會(huì)清除該文件的緩存(因?yàn)?run是基于內(nèi)存的文件系統(tǒng)),將結(jié)果與vmtouch進(jìn)行對(duì)比。
// 生成測(cè)試對(duì)比文件
[root@localhost bpf]# dd if=/dev/zero of=/run/test bs=1M count=256
256+0 records in
256+0 records out
268435456 bytes (268 MB) copied, 3.72593 s, 72.0 MB/s
// 第一組數(shù)據(jù)對(duì)比
[root@localhost bpf]# vmtouch /run/test
Files: 1
Directories: 0
Resident Pages: 65536/65536 256M/256M 100%
Elapsed: 0.003635 seconds
[root@localhost bpf]# filecache /run/test
filename : /run/test has 65536 pages in memory
# 第二組數(shù)據(jù)對(duì)比
[root@localhost bpf]# vmtouch /var/log/messages
Files: 1
Directories: 0
Resident Pages: 987/987 3M/3M 100%
Elapsed: 0.000992 seconds
[root@localhost bpf]# filecache /var/log/messages
filename : /var/log/messages has 988 pages in memory
# 使用drop_caches后進(jìn)行數(shù)據(jù)對(duì)比
[root@localhost bpf]# echo 3 > /proc/sys/vm/drop_caches
[root@localhost bpf]# vmtouch /var/log/messages
Files: 1
Directories: 0
Resident Pages: 2/989 8K/3M 0.202%
Elapsed: 0.002407 seconds
[root@localhost bpf]# filecache /var/log/messages
filename : /var/log/messages has 3 pages in memory
# 讀取/var/log/mesage后,進(jìn)行數(shù)據(jù)對(duì)比
[root@localhost bpf]# head -n 1000 /var/log/messages > /dev/null
[root@localhost bpf]# vmtouch /var/log/messages
Files: 1
Directories: 0
Resident Pages: 174/993 696K/3M 17.5%
Elapsed: 8.4e-05 seconds
[root@localhost bpf]# filecache /var/log/messages
filename : /var/log/messages has 174 pages in memory
# /run目錄下經(jīng)過(guò)drop_caches后數(shù)據(jù)對(duì)比
[root@localhost bpf]# vmtouch /run/test
Files: 1
Directories: 0
Resident Pages: 65536/65536 256M/256M 100%
Elapsed: 0.003071 seconds
[root@localhost bpf]# filecache /run/test
filename : /run/test has 65536 pages in memory
vmtouch工具介紹
vmtouch工具同樣可以查看文件占用的緩存,提供了更好的結(jié)果展示。另外vmtouch還可以管理文件緩存,如-t選項(xiàng),提前將文件緩存到內(nèi)存中,-e選項(xiàng)釋放指定文件占用的文件緩存,更多功能,參考幫助提示信息。vmtouch可以說(shuō)沒(méi)有依賴(只有g(shù)libc),因?yàn)樗闹饕ぷ魇峭ㄟ^(guò)mincore系統(tǒng)調(diào)用完成的。使用也相當(dāng)簡(jiǎn)單,編譯一下,即可拿到其他節(jié)點(diǎn)去運(yùn)行,因?yàn)槭峭ㄟ^(guò)系統(tǒng)調(diào)用,所以該工具可以跨多個(gè)內(nèi)核版本正常運(yùn)行。
[root@localhost bpf]# vmtouch --help
vmtouch: invalid option -- '-'
vmtouch v1.3.1 - the Virtual Memory Toucher by Doug Hoyte
Portable file system cache diagnostics and control
Usage: vmtouch [OPTIONS] ... FILES OR DIRECTORIES ...
Options:
-t touch pages into memory
-e evict pages from memory
-l lock pages in physical memory with mlock(2)
-L lock pages in physical memory with mlockall(2)
-d daemon mode
-m <size> max file size to touch
-p <range> use the specified portion instead of the entire file
-f follow symbolic links
-F don't crawl different filesystems
-h also count hardlinked copies
-i <pattern> ignores files and directories that match this pattern
-I <pattern> only process files that match this pattern
-b <list file> get files or directories from the list file
-0 in batch mode (-b) separate paths with NUL byte instead of newline
-w wait until all pages are locked (only useful together with -d)
-P <pidfile> write a pidfile (only useful together with -l or -L)
-o <type> output in machine friendly format. 'kv' for key=value pairs.
-v verbose
-q quiet
[root@localhost bpf]# ldd /usr/bin/vmtouch
linux-vdso.so.1 => (0x00007ffeb6dfa000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb8f9223000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb8f95f1000)
// filecache多了一個(gè)elf和z的動(dòng)態(tài)庫(kù),是因?yàn)橐馕鰁lf格式的文件頭
[root@localhost bpf]# ldd /usr/bin/filecache
linux-vdso.so.1 => (0x00007ffe177e0000)
libelf.so.1 => /lib64/libelf.so.1 (0x00007f97de08c000)
libc.so.6 => /lib64/libc.so.6 (0x00007f97ddcbe000)
libz.so.1 => /lib64/libz.so.1 (0x00007f97ddaa8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f97de2a4000)
[root@localhost bpf]#
實(shí)踐總結(jié)
如果只是簡(jiǎn)單的實(shí)踐下ebpf,那么本文也就沒(méi)有什么意義了,以下是個(gè)人認(rèn)為比較有創(chuàng)新性的點(diǎn)。
- 仿照bcc CORE(cross once,run everywhere),將bpf字節(jié)碼導(dǎo)入到頭文件中,達(dá)到只提供一個(gè)二進(jìn)制交付件的目的。
- 演示了如何通過(guò)map向內(nèi)核傳參數(shù)。
- 分析了如何選取合適的內(nèi)核函數(shù)作為hook點(diǎn)。
源碼展示,僅供參考
本實(shí)踐中的代碼就像拼積木,我知道我要干什么,然后從源碼中找各種各樣的零件拼接起來(lái)完成我要的積木。
- filecache_user.c代碼
#include "bpf_load.h"
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "filecache.h"
char * bpf_filepath = "/tmp/filecache_kern.o";
long get_inode_by_filename(char *filename) {
struct stat statbuf;
int ret = stat(filename, &statbuf);
if (ret != 0) {
printf("get inode number failed.\n");
exit(-1);
}
if ((statbuf.st_mode & S_IFREG) != S_IFREG) {
printf("This program is only support normal file currently.\n");
exit(-1);
}
return statbuf.st_ino;
}
void write_bpf_to_file() {
struct FILE * fp;
fp = fopen(bpf_filepath, "w");
if (fp == NULL) {
printf("create bpf file error, filepath : %s\n", bpf_filepath);
exit(-1);
}
size_t writen = fwrite(filecache_kern_o, filecache_kern_o_len, 1, fp);
if (writen != 1) {
printf("write bpf file error,filepath : %s\n", bpf_filepath);
exit(-1);
}
fclose(fp);
}
int main(int argc, char **argv) {
struct stat statbuf;
long inode_number;
int fd1 = map_fd[1];
long key1 = -1, prev_key1;
long x = 0;
if(argc != 2) {
printf("Usage : filecache filepath\n");
exit(-1);
}
const char * filename = argv[1];
write_bpf_to_file();
// Load our newly compiled eBPF program
if (load_bpf_file(bpf_filepath) != 0) {
printf("load the BPF program faild, filepath : %s\n", bpf_filepath);
return -1;
}
inode_number = get_inode_by_filename(filename);
bpf_map_update_elem(fd1, &x, &inode_number, BPF_NOEXIST);
stat(filename, &statbuf);
// map_fd is a global variable containing all eBPF map file descriptors
int fd = map_fd[0], val;
long key = -1, prev_key;
// Iterate over all keys in the map
if (bpf_map_get_next_key(fd, &prev_key, &key) == 0) {
printf("filename : %s has %ld pages in memory \n", filename, key);
} else {
printf("filename : %s has no pages in memory \n", filename);
}
}
- filecache_kern.c,代碼僅供參考。
#include <uapi/linux/bpf.h>
#include <linux/version.h>
#include "bpf_helpers.h"
#include <linux/fs.h>
// Data in this map is accessible in user-space
struct bpf_map_def SEC("maps") pagecache_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(long),
.value_size = sizeof(char),
.max_entries = 2,
};
// user parameter
struct bpf_map_def SEC("maps") para_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(long),
.value_size = sizeof(long),
.max_entries = 1,
};
#define _(P) ({typeof(P) val = 0; bpf_probe_read(&val, sizeof(val), &P); val;})
SEC("kprobe/vfs_getattr_nosec")
int bpf_vfs_getattr_nosec(struct pt_regs *ctx)
{
long page_num = 0;
long val=0, x=0;
struct path* path;
long para_inode_num,inode_num;
struct inode* inode;
struct address_space *add;
if (ctx == NULL)
return -1;
path = (struct path *)PT_REGS_PARM1(ctx);
if (path) {
struct dentry * dentry = _(path->dentry);
if (dentry) {
inode = _(dentry->d_inode);
if (inode) {
// get inode number.
inode_num = _(inode->i_ino);
// get inode number from user.
void *ptr = bpf_map_lookup_elem(¶_map, &x);
if (ptr) {
bpf_probe_read(¶_inode_num, sizeof(para_inode_num), ptr);
}
if (inode_num==para_inode_num) {
add = _(inode->i_mapping);
if (add) {
// get cached pages number and update map
page_num = _(add->nrpages);
bpf_map_update_elem(&pagecache_map, &page_num, &val, BPF_NOEXIST);
}
}
}
}
}
return 0;
}
// All eBPF programs must be GPL licensed
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;