最近ebpf技術的文章越來越多的出現在好幾個微信公眾號中,之前只是了解ebpf技術的原理,并不清楚細節,所以需要實踐一下。以什么課題內容來實踐呢,想起來之前遺留的一個問題,如何查看文件在內存中的緩存(當時沒有搜索到vmtouch這個工具),所以就以這個問題做為導向實踐一下ebpf技術。
獲取文件緩存原理解析
要獲取文件在內存中的緩存,只需要在內核中找到該文件對應的inode結構,然后讀取inode->i_mapping.nrpages的值,該值就是文件緩存的頁數。要實現這個功能,kprobe當然是沒有問題的,但是不夠靈活。
另外內核中的被hook函數要選擇哪一個,既可以獲取到nrpages,又不會影響內核的性能呢?經過分析,可以將hook函數設置為vfs_getattr_nosec,該內核函數是用戶態執行stat,fstat,lstat等獲取文件的屬性信息時必須調用的函數,因此選擇該函數作為被hook的函數非常合適,即可以實現功能,又不影響性能。
細節原理可以參考之前的文章 量化分析pagecache
交付形式
最終理想的交付方式是只提供一個二進制文件,通過執行filecache filename即可無需等待獲取該文件占用的內存頁數。
[root@localhost bpf]# filecache
Usage : filecache filepath
但是標準的ebpf程序的交付件是有兩個,一個是用戶態執行的二進制程序,一個是bpf格式的kern.o文件,如何將這兩個文件進行融合達到只有一個交付件的目的呢?這里借鑒了bcc CORE的方式,將bpf格式的kern.o轉為字符串數組寫入到c格式的頭文件中,然后在用戶態二進制執行的時候將字符串數組再轉換成kern.o文件,之后調用load_bpf_file將kern.o文件加載到內核中。經過搜索,xxd -i可以實現該需求。
實現細節
源代碼分為兩個文件,一個是用戶態的filecache_user.c,一個是filecache_kern.c經過xxd轉換后的filecache.h文件 將源碼放在文章最下面,不影響閱讀體驗。代碼解析如下 :
- filecache_kern.c中將bpf_vfs_getattr_nosec已kprobe的方式注冊到內核中。
- 通過clang將filecache_kern.c編譯成bpf格式的.o文件。
- 通過xxd -i filecache_kern.o > filecache.h,將.o轉換為.h文件。
- 在filecache_user.c中包含該filecache.h頭文件。
- 將filecache.h中的內容還原成filecache_kern.o,因為4.19內核的ebpf只提供了load_ebpf_file這一個接口,這個接口的參數是文件的路徑名。
- 通過stat函數調用獲取文件的inodenum。
- 將該inodenum通過para_map傳入到內核中。
- 再次調用stat函數,觸發內核調用1中注冊的hook函數。
- 在filecache_kern.c中,當1中注冊的函數被觸發時,獲取用戶傳過來的inodenum,并與inode->i_ino進行對比,如果相同,則通過inode->i_mapping.nrpages將頁數寫入pagecache_map中。
- 在filecache_user.c中,讀取pagecache_map的值,如果有值,就是文件緩存的頁數,如果沒有值,則說明文件沒有被緩存在內存中。
- 程序結束后,由內核自動清理map數據(perf_event_open)。
代碼編譯
由于內核中代碼編譯依賴的庫和頭文件系統比較復雜,這里仿照其他示例將文件放到samples/bpf中。
在編譯之前,獲取到內核源碼,先進行編譯,生成必要的頭文件,參考centos獲取指定版本內核代碼,或者安裝kernel-devel包。還需要先進行源碼安裝高版本clang,這里也踩過坑了clang源碼編譯。
如果不想在centos7上折騰源碼編譯,還有一個思路可以參考,在ubuntu上編譯bpf程序,在centos上編譯用戶態程序也是可以的。
做好準備工作后,進入內核代碼目錄的samples/bpf,修改對應的Makefile,執行make即可生成filecache二進制文件,且該二進制文件可以拿到其他centos內核版本為4.19.x的環境中直接運行。編譯過程中可能會遇到問題,根據錯誤提示信息搜索一下即可找到答案,一般是缺少某些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目錄創建一個測試文件,該目錄下的文件會占用內存,且echo 3 > /proc/sys/vm/drop_caches也不會清除該文件的緩存(因為/run是基于內存的文件系統),將結果與vmtouch進行對比。
// 生成測試對比文件
[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
// 第一組數據對比
[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
# 第二組數據對比
[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后進行數據對比
[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后,進行數據對比
[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目錄下經過drop_caches后數據對比
[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工具同樣可以查看文件占用的緩存,提供了更好的結果展示。另外vmtouch還可以管理文件緩存,如-t選項,提前將文件緩存到內存中,-e選項釋放指定文件占用的文件緩存,更多功能,參考幫助提示信息。vmtouch可以說沒有依賴(只有glibc),因為它的主要工作是通過mincore系統調用完成的。使用也相當簡單,編譯一下,即可拿到其他節點去運行,因為是通過系統調用,所以該工具可以跨多個內核版本正常運行。
[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多了一個elf和z的動態庫,是因為要解析elf格式的文件頭
[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]#
實踐總結
如果只是簡單的實踐下ebpf,那么本文也就沒有什么意義了,以下是個人認為比較有創新性的點。
- 仿照bcc CORE(cross once,run everywhere),將bpf字節碼導入到頭文件中,達到只提供一個二進制交付件的目的。
- 演示了如何通過map向內核傳參數。
- 分析了如何選取合適的內核函數作為hook點。
源碼展示,僅供參考
本實踐中的代碼就像拼積木,我知道我要干什么,然后從源碼中找各種各樣的零件拼接起來完成我要的積木。
- 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;