最近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;