ebpf 实践之 查看文件占用的缓存大小

最近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可以实现该需求。

实现细节

image.png

源代码分为两个文件,一个是用户态的filecache_user.c,一个是filecache_kern.c经过xxd转换后的filecache.h文件 将源码放在文章最下面,不影响阅读体验。代码解析如下 :

  1. filecache_kern.c中将bpf_vfs_getattr_nosec已kprobe的方式注册到内核中。
  2. 通过clang将filecache_kern.c编译成bpf格式的.o文件。
  3. 通过xxd -i filecache_kern.o > filecache.h,将.o转换为.h文件。
  4. 在filecache_user.c中包含该filecache.h头文件。
  5. 将filecache.h中的内容还原成filecache_kern.o,因为4.19内核的ebpf只提供了load_ebpf_file这一个接口,这个接口的参数是文件的路径名。
  6. 通过stat函数调用获取文件的inodenum。
  7. 将该inodenum通过para_map传入到内核中。
  8. 再次调用stat函数,触发内核调用1中注册的hook函数。
  9. 在filecache_kern.c中,当1中注册的函数被触发时,获取用户传过来的inodenum,并与inode->i_ino进行对比,如果相同,则通过inode->i_mapping.nrpages将页数写入pagecache_map中。
  10. 在filecache_user.c中,读取pagecache_map的值,如果有值,就是文件缓存的页数,如果没有值,则说明文件没有被缓存在内存中。
  11. 程序结束后,由内核自动清理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,那么本文也就没有什么意义了,以下是个人认为比较有创新性的点。

  1. 仿照bcc CORE(cross once,run everywhere),将bpf字节码导入到头文件中,达到只提供一个二进制交付件的目的。
  2. 演示了如何通过map向内核传参数。
  3. 分析了如何选取合适的内核函数作为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(&para_map, &x);
                                if (ptr) {
                                        bpf_probe_read(&para_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;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容