添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

一道思考题所引起动态跟踪 ‘学案’

本文地址:https://www.ebpf.top/post/ftrace_kernel_dynamic李程远老师在极客时间 《容器实战高手课》中的 “ 加餐 04 | 理解 ftrace(2):怎么理解 ftrace 背后的技术 tracepoint 和 kprobe?” 留了一道思考题:想想看,当我们用 kprobe 为一个内核函数注册了 probe 之后,怎样能看到对应内核函数的第一条指令被替换了呢?kprobe 是内核函数动态跟踪的一种实现机制,使用该机制几乎可跟踪所有的内核函数(排除带有 __kprobes/nokprobe_inline 注解的和标有 NOKPROBE_SYMBOL 的函数)。 kprobe 跟踪机制的实现目前主要有 2 种机制:一般情况下,当 kprobe 函数注册的时候,把目标地址上内核代码的指令码,替换成了 “cc”,也就是 int3 指令。这样一来,当内核代码执行到这条指令的时候,就会触发一个异常而进入到 Linux int3 异常处理函数 do_int3() 里。在 do_int3() 这个函数里,进行检查,如果发现有对应的 kprobe 注册了 probe,就会依次执行注册的 pre_handler()、替换前的指令、post_handler()。如内核基于 ftrace 对函数进行 trace,则会函数头上预留了 callq <__fentry__> 的 5 个字节(在启动的时候被替换成了 nop)。kprobe 跟踪机制会复用 ftrace 跟踪预留的 5 个字节,将其替换成 ftrace_caller ,而不再使用 int3 软中断指令替换。不论上述那种方式,kprobe 实现原理基本一致:进行目标指令替换,替换的指令可以使程序跳转到一个特定的 handler 里,然后再去执行注册的 probe 的函数。本文,我将基于 ftrace 机制对整个动态替换的机制进行验证。如对 ftrace 不熟悉,建议提前阅读 Linux 原生跟踪工具 Ftrace 必知必会 。1. 基础知识1.1 默认编译我们用 C 语言实现一个非常简单程序进行简单验证:#include <stdio.h> #include <stdlib.h> int a() { return 0; int main(int argc, char ** argv){ return 0; }在默认参数编译后的代码如下,可见函数头部没有特殊定义。$ gcc -o hello hello.c $ objdump -S hello 0000000000001129 <a>: 1129: f3 0f 1e fa endbr64 112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 1131: b8 00 00 00 00 mov $0x0,%eax 1136: 5d pop %rbp 1137: c3 ret ...1.2 使用 -pg 选项使用 -pg 参数编译后,我们可以看到在函数头部增加了对 mcount 函数的调用,这种机制常用用于运行程序性能分析:$ gcc -pg -o hello.pg hello.c $ objdump -S hello.pg 00000000000011e9 <a>: 11e9: f3 0f 1e fa endbr64 11ed: 55 push %rbp 11ee: 48 89 e5 mov %rsp,%rbp 11f1: ff 15 f1 2d 00 00 call *0x2df1(%rip) # 3fe8 <mcount@GLIBC_2.2.5> 11f7: b8 00 00 00 00 mov $0x0,%eax 11fc: 5d pop %rbp 11fd: c3 ret ...gcc 添加 -pg 选项后,编译器都会在函数头部增加 mcount/fentry 函数调用( 设置了 notrace 属性函数除外);#define notrace __attribute__((no_instrument_function))1.3 使用 -pg 和 -mfentry 选项在 gcc 4.6 版本后,新增编译选项 -mfentry, 将通过调用实现更加简洁高效的 __fentry__ 函数替换 mcount , 在 Linux Kernel 4.19 x86 体系结构默认使用该方式 。# echo 'void foo(){}' | gcc -x c -S -o - - -pg -mfentry $ gcc -pg -mfentry -o hello.pg.entry hello.c $ objdump -S hello.pg.entry 00000000000011e9 <a>: 11e9: f3 0f 1e fa endbr64 11ed: ff 15 05 2e 00 00 call *0x2e05(%rip) # 3ff8 <__fentry__@GLIBC_2.13> 11f3: 55 push %rbp 11f4: 48 89 e5 mov %rsp,%rbp 11f7: b8 00 00 00 00 mov $0x0,%eax 11fc: 5d pop %rbp 11fd: c3 ret这里我们以 fentry 为例,该函数调用会占用 5 个字节。 Linux 内核中 fentry 函数被定位为 retq 直接返回。SYM_FUNC_START(__fentry__) SYM_FUNC_END(__fentry__)即使通过 reqt 直接返回,每个函数都调用的时候仍然会带来大概 13% 的性能损耗,在实际运行过程中,ftrace 机制会在内核启动时候将 5 个字节(ff 15 05 2e 00 00 call __fentry__)直接替换成 nop 指令,在 x86_64 体系中为 nop 指令为: 0F 1F 44 00 00H 。在启用 ftrace 动态跟踪机制时(CONFIG_DYNAMIC_FTRACE),设置跟踪函数后,内核会对当前 nop 指令进行动态替换(hot hook),替换成跳转到 ftrace_caller 函数,从而实现了动态跟踪。在替换过程中为了避免引发多核异常,首先将第一个直接替换成 0xcc 的中断指令,然后再替换后续的指令,具体实现参见 void ftrace_replace_code(int enable);1.4 对内核进行验证我们以内核函数 schedule 为例,使用 gdb 调试带有符号信息的 vmlinux 文件时,我们可直接查看到函数编译后的汇编代码:__fentry__ 函数则直接被定义为了 retq 指令:call 汇编指令解析: 0xffffffff81c33580 <+0>: e8 1b 41 44 ff call 0xffffffff810776a0 <__fentry__>e8 代表 call, 1b 41 44 ff 相对于下一条指令的偏移量 (0xffffffff81c33580 + 5), FF 44 41 1B 为负数,补码为 BB BE E5, 0xffffffff810776a0 - 0xffffffff81c33585 = -bbbee52. ftrace 中 kprobe 跟踪机制验证这里,我们打算验证 3 件事情:函数在内核启动后,函数首部的 call 指令会被替换成 nop 指令;ftrace 方式下设置 kprobe 函数跟踪后,nop 指令会被替换成相对应的 call 调用;kprobe 跟踪停止后,函数头部的 5 个字节会被替换成 nop 指令;(1,2 验证后,则很容易验证)为了验证内核函数动态替换过程,我首先考虑的是通过内核模块打印函数地址对应的首部 5 个字节。3. 使用内核模块进行验证3.1 使用 kallsyms_lookup_name 方式获取最常见或流行的做法是在内核模块中使用内核函数 kallsyms_lookup_name() 获取到跟踪函数的地址,然后进行打印。首先,我也想尝试通过这种方式进行,其他获取内核符号地址的方式参见 获取内核符号地址的方式 。内核模块的样例代码参考 hello_kernel_module,代码也非常简单:static int __init hello_init(void) char *func_addr = (char *)kallsyms_lookup_name("schedule"); // 判断地址是否合法,然后进行打印 }但在编译阶段报错(本地环境 5.11.22-generic):ERROR: modpost: "kallsyms_lookup_name" [hello_kernel_module/hello.ko] undefined!在新版内核 ( >= 5.7 ) 中,出于安全考虑 kallsyms_lookup_name 函数不再被导出,在内核模块中不能再直接应用,相关说明可参见文章 Unexporting kallsyms_lookup_name 和提交的 补丁 。 这里 讨论了几种可行的替代方案,另外关于多内核版本下的统一方案可参考 The Linux Kernel Module Programming Guide 中的样例代码 syscall.c。这里为了简化,我使用 kprobe 注册机制(仅支持 Linux 5.11 内核),完整代码如下:#include <linux/init.h> #include <linux/module.h> #include <linux/kprobes.h> static struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" static int __init hello_init(void) typedef unsigned long (*kallsyms_lookup_name_t)(const char *name); int i = 0; kallsyms_lookup_name_t kallsyms_lookup_name; register_kprobe(&kp); kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr; unregister_kprobe(&kp); char *func_addr = (char *)kallsyms_lookup_name("schedule"); pr_info("fun addr 0x%lx\n", func_addr); for (i = 0; i < 5; i++) pr_info("0x%02x ", (u8)func_addr[i]); return 0; }完整代码参见 get_inst.c。编译并安装后,可通过 dmesg 进行查看:$ sudo insmod ./hello.ko $ dmesg -T [Sat Apr 9 12:11:25 2022] fun addr 0xffffffff9eea3eb0 [Sat Apr 9 12:11:25 2022] 0x0f 0x1f 0x44 0x00 0x00这里我们可以看到函数首部的 5 个字节已被替换成 nop 指令(0f 1f 44 00 00),这个过程是在内核启动时由 ftrace_init() 函数统一处理替换的。同样,新安装的内核模块中导出的函数,首部也会自动被替换成成 nop 指令。对应到 ftrace pdf 中 schedule 函数的样例如下:图 未启用 kprobe 跟踪前,函数首部 5 个字节为 nop 指令 <图来自于 ftrace pdf P36>接着,启用内核函数 schedule 的跟踪,再进行验证:$ cd /sys/kernel/debug/tracing $ sudo echo 'p:schedule schedule' >> kprobe_events $ sudo cat kprobe_events p:kprobes/schedule schedule $ sudo echo 1 > events/kprobes/schedule/enable $ insmod ./hello.ko $ demsg -T [Sun Apr 10 20:07:12 2022] 0xe8 0x7b 0x5a 0xd9 0x20 [Sun Apr 10 20:07:12 2022] fun addr 0xffffffff9fa33580 $ sudo echo 0 > events/kprobes/schedule/enable在启用内核函数 schedule 函数跟踪后,我们可以看到首部 5 个字节 (nop)已经被替换成了其他函数调用。大体效果如下所示: 图:在注册 kprobe 函数 nop 指令被替换效果 <来自于 ftrace pdf P37>3.2 直接使用内核函数地址(踩坑笔记,可跳过)如果不通过 kallsyms_lookup_name 函数,直接使用 /boot/System.map 中的地址是否可以?答案是可以的,但是需要小心 KASLR(Kernel Address Space Layout Randomization)机制。KASLR 可能会在每次启动时随机化内核代码和数据的地址,目的是保护内核空间不被攻击者破坏,这样以来 /boot/System.map 中列出的静态地址会被随机值调整。如果没有 KASLR,攻击者可能会在固定地址中轻易找到目标地址。如果 /proc/kallsyms 中的符号地址与 /boot/System.map 中的地址不同,说明 KASLR 系统运行的内核中被启用。两个查看需要 root 用户权限才能查看。$ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" $ sudo grep schedule$ /boot/System.map-$(uname -r) ffffffff81c33580 T schedule $ grep schedule$ /proc/kallsyms ffffffff9fa33580 T schedule # 如果系统未启用 KASLR(内核地址空间随机地址)功能,两者地址会相等,否则会不一致。如果启用了 KASLR,我们必须在每次重启机器时注意 /proc/kallsyms 的地址( 每次重启机器都会发生变化 )。为了使用 /boot/System.map 中的地址,要确保 KASLR 被禁用。我们可以在启动命令行中添加 nokaslr 来禁用 KASLR,重启生效:$ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" $ sudo perl -i -pe 'm/quiet/ and s//quiet nokaslr/' /etc/default/grub $ grep quiet /etc/default/grub GRUB_CMDLINE_LINUX_DEFAULT="quiet nokaslr splash" $ sudo update-grub我们可在内核模块中添加一个 sym 变量获取传入的函数地址,样例代码如下:#include <linux/init.h> #include <linux/module.h> #include <linux/kallsyms.h> static unsigned long sym = 0; module_param(sym, ulong, 0644); static int __init hello_init(void) char *func_addr = 0; int i = 0; if (sym != 0) func_addr = (char *)sym; for ( i = 0; i < 5; i++) pr_info("0x%02x ", (u8)func_addr[i]); pr_info("fun addr 0x%p\n", func_addr); return 0; module_init(hello_init);在确保 KASLR 被禁用后,我们编译上述模块并运行,可得到与上述方式一致的结果:$ addr=`grep -w "schedule" /proc/kallsyms|cut -d " " -f 1` $ insmod ./hello.ko sym=0x$addr $ dmesg -T [Sun Apr 10 20:50:51 2022] 0xe8 0x7b 0x5a 0xd9 0x20 [Sun Apr 10 20:50:51 2022] fun addr 0x000000005aad203e $ rmmod hello如果不禁用 KASLR 使用固定地址进行编译,加载驱动则会报错:$ sudo dmesg -T [Fri Apr 8 17:39:47 2022] BUG: unable to handle page fault for address: ffffffff810a3eb2 [Fri Apr 8 17:39:47 2022] #PF: supervisor read access in kernel mode [Fri Apr 8 17:39:47 2022] #PF: error_code(0x0000) - not-present page4. 使用 gdb + qemu 进行验证我将编译内核带上 DEBUG 选项的内核及相关文件保存到了 百度网盘 ,提取码 av28。关于内核编译及调试的详细过程可参考 使用 GDB + Qemu 调试 Linux 内核 。这里介绍一下如何在 Mac 环境下使用 qemu 软件进行内核调试:$ brew install qemu $ brew link qemu需要提前下载网盘的文件至本地目录,运行 qemu 进行测试:$ cat run.sh #!/bin/bash qemu-system-x86_64 -machine type=q35,accel=hvf -kernel ./bzImage -initrd ./rootfs_root.img -append "nokaslr console=ttyS0" -s c $ ./run.sh注意这里添加了 -machine type=q35,accel=hvf 标记,在 mac 环境下使用 hvf 加速,如果不启用加速,默认使用 xen 虚拟化指令集。如果在 qemu-system-x86_64 命令行没有启用 hvf 加速,看到函数前 5 个字节会有所差异,默认为 66 66 66 66 90 data16 data16 data16 xchg %ax,%ax,这是因为 nop 指令在不同的体系结构会有所不同。# cd /sys/kernel/debug/tracing # echo 'p:schedule schedule' >> kprobe_events # echo 1 > events/kprobes/schedule/enable这里我们对传入头部的函数继续进行跟踪:(gdb) x/100i 0xffffffffc0002000在后续翻页中可以看到调用了 kprobe_ftrace_handler 注册函数。需要注意地址 0xffffffffc0002000 的函数并不是 ftrace 注册函数 ftrace_caller 或 ftrace_regs_caller,而是依据这两个函数在内存中动态构建的 trampoline(蹦床),将 ftrace_caller 或 ftrace_regs_caller 修改注册函数后的汇编拷贝到这段 trampoline 中,(本次调试 ftrace 函数为 ftrace_regs_caller,事件注册函数为 kprobe_ftrace_handler)。参考ftrace 作者的 pdf:Ftrace Kernel Hooks: More than just tracing 探秘 ftrace 内核文档 Function Tracer Design 二十分钟 Linux ftrace 原理抛砖引玉 - Cache OneKASLR 当 ftrace 用于用户空间 Linux kernel debug on macOS 搭建可视化内核 debug 环境

Google Kubernetes引擎上使用Istio简化微服务 — 第 III 部分(译)

Google Kubernetes引擎上使用Istio简化微服务 — 第III部分作者:Nithin Mallya翻译:狄卫华原文:Simplifying Microservices with Istio in Google Kubernetes Engine — Part III原文链接:https://medium.com/google-cloud/simplifying-microservices-with-istio-in-google-kubernetes-engine-part-iii-6b62876d0a7d本系列翻译链接:在 GKE 上使用 Istio 简化微服务-Part I在 GKE 上使用 Istio 简化微服务-Part II在 GKE 上使用 Istio 简化微服务-Part III我所写的关于 Istio 的文章是 Istio 非常棒的官方文档 中的一部分。如果想了解更多,请阅读官方文档。在本系列的 Part I 中,我们看到如何使用 Istio 来简化我们的微服务之间的通信。在本系列的 Part II 中,我们学会了使用 Istil egress 规则来控制访问服务网格外面的服务。在这个部分,我们将会看到如何实现金丝雀(Canary)发布和使用 Istio 进行流量迁移。背景知识: 在以前的文章中,我详细解释了我们如何使用 Kubernets 实现蓝绿(Blue/Green)发布。通过蓝绿发布可以让我们在相同的生产环境中部署应用的当前版本和一个新的版本,通过零宕机部署( Zero Downtime Deployments)来保证用户不会在我们切换新版本的时候受到影响。系统中同时存在两个版本(当前版本和新版本)也可以让我在新版本遇到问题的时候,能够回滚到当前的版本。与此同时,我们也需要一种机制能够将流量引入(或者停止)到我们新版本的应用,同时监控是否有不利的影响。金丝雀部署或发布(Canary)则可以实现这一目的。不太有趣的事实:当矿工进入矿场时带着金丝雀。 任何有毒气体首先会杀死金丝雀,从而警告他们离开矿区。同样在程序部署方面,通过金丝雀部署,我们可以将新版本的程序部署到生产环境中,并仅向该新部署的版本发送一小部分流量。 这个新版本将与当前版本并行运行,我们则能够在将所有流量切换到新版本之前的任何问题提前发现。例如:我们的应用 v1 版本可以占据 90% 的流量,而v2版本可以占据其他 10%。 如果一切运行正常,我们可以将v2 版本流量增加到 25%,50%,最终达到 100%。 Istio 金丝雀部署的另一个优点是我们可以根据请求中的自定义头部信息增加流量。 例如将具有特定 cookie 标头值的流量的 10% 至我们应用的v2版本。注意:尽管金丝雀部署 “可以” 与A / B测试结合使用,用来了解用户如何从业务度量标准角度对新版本做出反应,但真正的动机是确保应用程序从功能角度满足需求。 此外,企业所有者可能希望运行A / B测试活动的时间更长(例如:许多天甚至几周),而不是金丝雀部署可能需要的时间。 因此将它们分开是明智的做法。实际操作我们从 Part I 中了解到,我们的 PetService 与 PetDetailsService(v1)和 PetMedicalHistoryService(v1)进行通信。 调用PetService的输出如下所示:$ curl http://108.59.82.93/pet/123 "petDetails": { "petName": "Maximus", "petAge": 5, "petOwner": "Nithin Mallya", "petBreed": "Dog" "petMedicalHistory": { "vaccinationList": [ "Bordetella, Leptospirosis, Rabies, Lyme Disease" }在上面的响应消息中,你会注意到宠物品种(petBreed)对应的值是 “Dog”。 然而 Maximus 恰好是 “German Shepherd Dog” (德国牧羊犬),我们需要修改 PetDetailsService,以便正确返回品种。所以我们现在创建 PetDetailsService的 v2 版本,它将返回 “German Shepherd Dog”。 同时我们希望确保将所有流量推送到v2之前,让一小部分用户测试此 v2 版本的服务。在下面的图1中,我们将流量配置为 50% 的请求发送到 v1 和 50% 至v2,即我们的金丝雀部署署(它可以是任何数字比例,具体取决于我们修改范围大小,并尽量减少任何负面影响)。步骤创建 PetDetailsService v2 版本并像以前一样进行部署(参见 petdetailservice/kube 目录下的 petinfo.yaml)$ kubectl get pods NAME READY STATUS RESTARTS AGE petdetailsservice-v1-2831216563-qnl10 2/2 Running 0 19h petdetailsservice-v2-2943472296-nhdxt 2/2 Running 0 2h petmedicalhistoryservice-v1-28468096-hd7ld 2/2 Running 0 19h petservice-v1-1652684438-3l112 2/2 Running 0 19h 2.创建RouteRule分流petdetailsservice50%的请求至 v1 版本,50%的请求至 v2,如下所示:$ cat <<EOF | istioctl create -f - apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata: name: petdetailsservice-default spec: destination: name: petdetailsservice route: - labels: version: v1 weight: 50 - labels: version: v2 weight: 50 $ istioctl get routerule NAME KIND NAMESPACE petdetailsservice-default RouteRule.v1alpha2.config.istio.io default 3.现在,如果我们访问PetService,就应该看到替代请求分别返回 “Dog” 和 “German Shepherd Dog”,如下所示:$ curl http://108.59.82.93/pet/123 "petDetails": { "petName": "Maximus", "petAge": 5, "petOwner": "Nithin Mallya", "petBreed": "Dog" "petMedicalHistory": { "vaccinationList": [ "Bordetella, Leptospirosis, Rabies, Lyme Disease" $ curl http://108.59.82.93/pet/123 "petDetails": { "petName": "Maximus", "petAge": 5, "petOwner": "Nithin Mallya", "petBreed": "German Shepherd Dog" "petMedicalHistory": { "vaccinationList": [ "Bordetella, Leptospirosis, Rabies, Lyme Disease" }已经可以正常工作。这引出了一个问题:我们不能用 Kubernetes 金丝雀部署 来做到这一点吗? 简短的答案是肯定的。但是,步骤涉及更多并且存在限制:仍然可以创建 2 个 PetDetailsService 部署(v1和v2),但需要在部署期间手动限制 v2 副本的数量,以维持v1:v2 比例,例如可以使用 10 个副本部署 v1,并使用2个副本部署 v2 以实现 10:2 负载平衡。由于所有的 pod 无论版本是否相同会被同样对待,Kubernetes集群中的流量负载平衡仍然受到随机性的影响。基于流量的自动扩容也会遇到问题,因为我们需要单独自动缩放2个部署,这些部署可以根据每个服务的流量负载分布来表现不一致。如果我们想根据某些标准(例如请求头部信息)为某些用户允许/限制流量,则基于Kubernetes金丝雀部署 可能无法实现此目的。结论:您刚刚看到创建Canary部署以及使用 Istio 控制流量是多么容易。 而且 Maximus 也很开心!资源本系列 Part I : https://medium.com/google-cloud/simplifying-microservices-with-istio-in-google-kubernetes-engine-part-i-849555f922b8本系列 Part I : https://medium.com/google-cloud/simplifying-microservices-with-istio-in-google-kubernetes-engine-part-ii-7461b1833089Istio网站 https://istio.io/DevOxx Istio 展示 Ray Tsang: https://www.youtube.com/watch?v=AGztKw580yQ&t=231s样例的 Github 地址: https://github.com/nmallya/istiodemoKubernetes: https://kubernetes.io/

Kubernetes NodePort vs LoadBalancer vs Ingress? 我们应该什么时候使用?(译)

作者:Sandeep Dinesh翻译:狄卫华原文:Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?原文链接:https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0最近有人向我了解 NodePorts ,LoadBalancers 和 Ingress 之间的区别是怎么样的。 它们都是将外部流量引入群集的方式,适用的场景却各不相同。 本文接下来我们将介绍它们的工作原理以及适用的相关场景。注意:文中所述内容适用于 GKE [Google Kubernetes Engine]。 如果你在其他云平台上运行使用步骤略有不同,比如 minikube 或其他相关软件。 我本人也不打算过多深入技术细节, 如果您有兴趣了解更多,Kubernetes 官方文档 会提供更多的有用资源!ClusterIPClusterIP 服务是 Kubernetes 默认的服务类型。 如果你在集群内部创建一个服务,则在集群内部的其他应用程序可以进行访问,但是不具备集群外部访问的能力。ClusterIP 服务的 YAML 文件看起来像这样:apiVersion: v1 kind: Service metadata: name: my-internal-service selector: app: my-app spec: type: ClusterIP ports: - name: http port: 80 targetPort: 80 protocol: TCP如果不能从互联网访问 ClusterIP 服务,我为什么要谈论它呢?事实上,你可以通过 Kubernetes 代理进行访问。感谢 Ahmet Alp Balkan 提供的图表启动 Kubernetes 代理:$ kubectl proxy --port=8080现在,可以通过 Kubernetes API 通过以下的模式来访问这个服务:http://localhost:8080/api/v1/proxy/namespaces//services/:/,通过这种方式可以使用以下的地址来访问我们上述定义的服务:http://localhost:8080/api/v1/proxy/namespaces/default/services/my-internal-service:http/何时使用这种访问方式?有以下几种场景,你可以使用 Kubernetes 代理的方式来访问服务:调试服务或者某些情况下通过笔记本电脑直接连接服务允许内部的通信,显示内部的仪表盘(dashboards)等因为此种方式需要作为一个授权用户运行 kubectl,因此不应该用来暴露服务至互联网访问或者用于生产环境。NodePortNodePort 服务通过外部访问服务的最基本的方式。顾名思义,NodePort 就是在所有的节点或者虚拟机上开放特定的端口,该端口的流量将被转发到对应的服务。从技术上层面,这不能算是最准确的图表,但我认为它能够直观展示了 NodePort 的工作方式。NodePort 服务的 YAML 文件看起来像这样:apiVersion: v1 kind: Service metadata: name: my-nodeport-service selector: app: my-app spec: type: NodePort ports: - name: http port: 80 targetPort: 80 nodePort: 30036 protocol: TCP从根本上讲,NodePort 方式的服务与 ClusterIP 方式的服务有两个区别。第一,类型是 NodePort,这需要指定一个称作 nodePort 的附加端口,并在所有节点上打开对应的端口。如果我们不具体指定端口,集群会选择一个随机的端口。大多数的情况下,我们都可以让 Kubernetes 帮我们选择合适的端口。正如 thockin 所描述的那样,有许多相关的注意事项(caveats)关于那些端口可供我们使用。何时使用这种访问方式?NodePort 方式有许多缺点:每个服务占用一个端口可以使用的 30000-32767 范围端口 (译者注:可以通过api-server启动参数service-node-port-range指定限制范围,默认为30000-32767)如果节点/虚拟机IP地址发生更改,需要进行相关处理由于上述原因,我不建议在生产环境中使用直接暴露服务。 如果运行的服务不要求高可用或者非常关注成本,这种方法则比较适合。 很好的例子就是用于演示或临时使用的程序。LoadBalancerLoadBalancer 服务是暴露服务至互联网最标准的方式。在 GKE 上,这将启动一个网络负载均衡器(Network Load Balancer),它会提供一个 IP 地址,以将所有流量转发到服务。感谢 Ahmet Alp Balkan 提供图表何时使用这种访问方式?这是公开服务的默认的方法。指定的端口上流量都将被转发到对应的服务,不经过过滤和其他路由等操作。这种方式意味着转发几乎任何类型的流量,如HTTP,TCP,UDP,Websockets,gRPC或其他。这种方式最大的缺点是,负载均衡器公开的每个服务都将获取独立 IP 地址,而我们则必须为每个暴露的服务对应的负载均衡器支付相关费用,这可能会变得非常昂贵!Ingress和上述讨论的服务方式不同,Ingress 实际上并不是服务类型中的一种。相反,它位于多个服务的前端充当一个智能路由或者集群的入口点。你可以使用 Ingress 做很多不同的事情,并且不同类型的 Ingress 控制也具备不同的能力。GKE 默认的 Ingress 控制器将启动一个 HTTP(S) 的负载均衡器。则将使我们可以基于访问路径和子域名将流量路由到后端服务。例如,你可以将 foo.yourdomain.com 下的流量转发到 foo 服务,将 yourdomain.com/bar/ 路径下的流量转发到 bar 服务。感谢 Ahmet Alp Balkan 提供图表在 GKE 上定义 L7层 HTTP 负载均衡器的 Ingress 对象定义的 YAML 看起来像这样:apiVersion: extensions/v1beta1 kind: Ingress metadata: name: my-ingress spec: backend: serviceName: other servicePort: 8080 rules: - host: foo.mydomain.com http: paths: - backend: serviceName: foo servicePort: 8080 - host: mydomain.com http: paths: - path: /bar/* backend: serviceName: bar servicePort: 8080 何时使用这种方式?Ingress 方式可能是暴露服务的最强大的方式,但也最复杂。现在有不同类型的 Ingress 控制器,包括 Google 云 负载均衡器, Nginx, Contour, Istio 等。此外,还有 Ingress 控制器的许多插件,比如 cert-manager 可以用来自动为服务提供 SSL 证书。如果希望在同一个IP地址下暴露多个服务,并且它们都使用相同的 L7 协议(通常是HTTP),则 Ingress 方式最有用。 如果使用本地 GCP 集成,则只需支付一台负载平衡器费用,并且是“智能”性 Ingress ,可以获得许多开箱即用的功能(如SSL,Auth,路由等)。KubernetesMicroservicesServicesLoad BalancingIngress

WP-Editor.MD升级后 500问题解决

WP-Editor.MD升级后 500问题解决使用 markdown 格式来发布 blog,遇到语法渲染的问题,升级 WP-Editor.MD。1. 现象升级 WP-Editor.MD 后发布文章入口出现 500 错误,停用 WP-Editor.MD 后可以正常访问。2. 调测修改根目录下 wp-config.php 中/** * 开发者专用:WordPress调试模式。 * 将这个值改为true,WordPress将显示所有用于开发的提示。 * 强烈建议插件开发者在开发环境中启用WP_DEBUG。 * 要获取其他能用于调试的信息,请访问Codex。 * @link https://codex.wordpress.org/Debugging_in_WordPress define('WP_DEBUG', true); // false -> ture重新访问文章发布页面:Fatal error: Cannot redeclare Markdown() (previously declared in wordpress/wp-content/plugins/jetpack-markdown/markdown/lib/extra.php:51) in wordpress/wp-content/plugins/wp-editormd/Jetpack/lib/markdown/extra.phpon line 673. 解决WP-Editor.MD 插件底层使用WordPress Jetpack 的Markdown模块来解析和保存内容,因此可能是相关的插件存在冲突通过报错可以看到是函数 Markdown() 重复定义造成的,在 插件 jetpack 中定义了一个函数 Markdown(), 由于 wp-editormd 底层也是用了 Jetpack 底层的 Markdown 模块,造成包含库的时间有冲突。wordpress/wp-content/plugins/jetpack/_inc/lib/markdown/extra.php### Standard Function Interface ### @define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); function Markdown($text) { # Initialize the parser and return the result of its transform method. # Setup static parser variable. static $parser; if (!isset($parser)) { $parser_class = MARKDOWN_PARSER_CLASS; $parser = new $parser_class; # Transform text using parser. return $parser->transform($text); }wordpress/wp-content/plugins/wp-editormd/Jetpack/lib/markdown/extra.php### Standard Function Interface ### @define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser_Editormd' ); function Markdown($text) { # Initialize the parser and return the result of its transform method. # Setup static parser variable. static $parser; if (!isset($parser)) { $parser_class = MARKDOWN_PARSER_CLASS; $parser = new $parser_class; # Transform text using parser. return $parser->transform($text); }安装wp-editor插件后写文章页面打不开,报500错误JetPack 插件也打开md语法功能,造成冲突,关闭 JetPack md 语法功能,或者将两个文件中的文件名修改一个;作者在最新版本已经修复该问题,参见 commit关闭 wordpress 调试模式define('WP_DEBUG', false);

Google Kubernetes引擎上使用Istio简化微服务 — 第I部分(译)

使用Istio简化微服务系列一:如何用Isito解决Spring Cloud Netflix部署微服务的挑战?Original 2018-03-12 姚炳雄 译 ServiceMesh中文网作者:Nithin Mallya翻译:姚炳雄原文:Simplifying Microservices with Istio in Google Kubernetes Engine — Part I原文链接:https://medium.com/google-cloud/simplifying-microservices-with-istio-in-google-kubernetes-engine-part-i-849555f922b8本系列翻译链接:在 GKE 上使用 Istio 简化微服务-Part I在 GKE 上使用 Istio 简化微服务-Part II在 GKE 上使用 Istio 简化微服务-Part III我所写的关于 Istio 的文章是 Istio 非常棒的官方文档 中的一部分。如果想了解更多,请阅读官方文档。概述Istio 简化了服务间的通信,流量涨落,容错,性能监控,跟踪等太多太多。如何利用它来帮我们从各微服务中抽象萃取出基础架构和功能切面?我写的这些关于 Istio 的文章是 Istio官网文档的子集。读官网文档可了解更多。注意::如果你很熟悉微服务,请跳过背景介绍这段。在本系列的第一部分,将涵盖如下内容:背景: 单体应用及微服务介绍Spring Cloud Netflix Stack及其优势Istio 介绍Istio的服务-服务通信举例背景过去,我们运维着“能做一切”的大型单体应用程序。 这是一种将产品推向市场的很好的方式,因为刚开始我们也只需要让我们的第一个应用上线。而且我们总是可以回头再来改进它的。部署一个大应用总是比构建和部署多个小块要容易。然而,这样的应用开发将导致“爆炸式的”工作量(我们经过数月的工作后将再次部署整个应用),并且增量变更将因为构建/测试/部署/发布周期等的复杂特性而来来回回折腾很长时间。但是,如果你是产品负责人,尤其是在部署一个新版本后发现一个严重的 Bug,那么这就不仅仅是多少钱的问题。 这甚至可能导致整个应用回滚。相对于比较小的组件来说,将这样的一个大型应用部署到云上并弹性扩展它们也并不容易。进入微服务微服务是运行在自己的进程中的可独立部署的服务套件。 他们通常使用 HTTP 资源进行通信,每个服务通常负责整个应用中的某一个单一的领域。 在流行的电子商务目录例子中,你可以有一个商品条目服务,一个审核服务和一个评价服务,每个都只专注一个领域。用这种方法来帮助分布式团队各自贡献各种服务,而不需要在每个服务变更时去构建/测试/部署整个应用,而且调试也无需进入彼此的代码。 将服务部署到云上也更容易,因为独立的服务就能按需进行自动弹性扩展。用这种方法让多语言服务(使用不同语言编写的服务)也成为可能,这样我们就可以让 Java/C++ 服务执行更多的计算密集型工作,让 Rails / Node.js 服务更多来支持前端应用等等。Spring Cloud Netflix:随着微服务的流行,简化服务的创建和管理的框架如雨后春笋。 我个人在2015年最喜欢的是 Netflix OSS 栈(Spring Cloud Netflix),它让我用一个非常简单的方式,通过 Spring Tool Suite IDE 来创建 Java 微服务。我可以通过 Netflix 套件获得以下功能(图1):通过 Eureka 进行服务注册- 用于注册和发现服务用 Ribbon 做客户端的负载均衡- 客户端可以选择将其请求发送到哪个服务器。声明 REST 客户端 Feign 与其他服务交谈。在内部,使用 Ribbon。API 网关用 Zuul —单一入口点来管理所有 API 调用,并按路由规则路由到微服务。Hystrix 做熔断器 — 处理容错能力以及在短时间内关闭通信信道(断开回路)并在目标服务宕机时返回用户友好的响应。用 Hystrix 和 Turbine 做仪表板 —— 可视化流量和熔断图1: Spring Cloud Netflix 实现微服务当然,这种构建和部署应用的方法也带来了它的挑战。挑战部署:怎样才能通过一种统一一致的方式将我们的服务部署到云中,并确保它们始终可用,并让它们按需进行自动弹性扩展?横切关注点:如何对每个微服务代码改动很少甚至不改代码的情况下能获得更多我们所看到的 Spring Cloud Netflix 中所实现的功能? 另外,如何处理用不同语言编写的服务?解决方案部署:Kubernetes 已经为在 Google Kubernetes Engine(GKE)中高效部署和编排 Docker 容器铺平了道路。 Kubernetes 抽象出基础架构,并让我们通过 API 与之进行交互。 请参阅本文末尾的链接以获取更多详细信息。横切关注点:我们可以用 Istio。 Istio 官网上的解释称:“ Istio 提供了一种简单的方法,来创建一个提供负载均衡、服务间认证、监控等的服务网络,且不需要对服务代码进行任何更改。 通过在整个环境中部署专门的 sidecar 代理服务,来拦截微服务间的所有网络通信,整个配置和管理通过 Istio的控制面板来做。”Istio介绍:换句话说,通过Istio,我们可以创建我们的微服务,并将它们与“轻量级 Sidecar 代理”一起部署(下图2),以处理我们的许多横切需求,例如:服务到服务的通信追踪熔断(类 Hystrix 功能)和重试性能监控和仪表板(类似于 Hystrix 和 Turbine 仪表板)流量路由(例如:发送 x% 流量到 V2 版本的应用实例),金丝雀部署一个额外的红利(特别是如果您正在处理医疗保健中的 PHI 等*敏感数据时)出站(Istio 服务网格之外的外部可调用服务)需要明确配置,并且可以阻止在服务网格之外的做特殊调用的服务。图2: 用 Envoy 代理来做多语言服务间的通信在上图2中,我们已经去掉了图1中的许多组件,并添加了一个新组件(Envoy Proxy)。 任何服务(A)如需与另一个服务(B)交谈,则提前对它的代理做路由规则预配置,以路由到对方的代理进行通信。 代理与代理交谈。 由于所有通信都是通过代理进行的,所以很容易监控流量,收集指标,根据需要使用熔断规则等。对横切面的声明式的配置规则和策略,无需更改任何服务代码,让我们可以更专注于最重要的事情:构建高业务价值的高质量的服务。从高的层面看,Istio 有如下组件:Envoy:一个高性能,低空间占用的代理,支持服务之间的通信,并有助于负载平衡,服务发现等;Mixer:负责整个生态(服务网格)中所有服务的访问控制策略,并收集通过 Envoy 或其他服务发送的遥测信息;Pilot:帮助发现服务,流量缓慢调整和容错(熔断,重试等);Istio-Auth :用于服务间认证以及都使用 TLS 的终端用户认证。本文中的示例不使用 Istio-Auth。用Istio进行服务—服务通信让我们在练习中了解它!我们将举一个简单的例子,展示3个通过 Envoy 代理进行通信的微服务。它们已经用 Node.js 写好,但如前所述,你可以用任何语言。图3: 用于提取宠物细节的3个微服务的逻辑视图宠物服务:通过调用 PetDetailsService 和 PetMedicalHistoryService 来返回宠物的信息和病史。 它将在9080端口上运行。宠物详细信息服务:返回宠物信息,如姓名,年龄,品种,拥有者等,它将在端口9081上运行。宠物医疗历史信息服务:返回宠物的病史(疫苗接种)。 它将在9082端口运行。步骤:在 GKE中创建一个 Kubernetes 集群(我叫 nithinistiocluster)。 确保缺省服务帐户具有以下权限:roles / container.admin(Kubernetes Engine Admin)。按照 https://istio.io/docs/setup/kubernetes/quick-start.html 中的说明安装 istio。1. 现在,我们准备将我们的应用程序(上述3个服务)部署到 GKE,并将边车代理注入到部署中。2. 在 github 仓库中,您将看到4个目录(安装各种组件时创建的istio目录和我的微服务的3个目录)。3. 对于每个微服务,我在 petinfo.yaml 文件的 kube 目录中创建了相应的 Kubernete s部署和服务。 服务名为宠物服务,宠物详细信息服务和宠物医疗历史信息服务。 由于PetServic e可以公开访问,因此它有一个指向 petservice 的 Kubernetes Ingress。4. 你可以转到每个服务目录,在 deploy.sh 文件中更新项目和群集名称并运行它。 它构建服务,创建 Docker 镜像,将其上传到Google Container Registry,然后运行 istioct l 以注入 Envoy 代理。 例如,对于 PetService,它看起来像:#!/usr/bin/env bash export PROJECT=nithinistioproject export CONTAINER_VERSION=feb4v2 export IMAGE=gcr.io/$PROJECT/petservice:$CONTAINER_VERSION export BUILD_HOME=. gcloud config set project $PROJECT gcloud container clusters get-credentials nithinistiocluster --zone us-central1-a --project $PROJECT echo $IMAGE docker build -t petservice -f "${PWD}/Dockerfile" $BUILD_HOME echo 'Successfully built ' $IMAGE docker tag petservice $IMAGE echo 'Successfully tagged ' $IMAGE #push to google container registry gcloud docker -- push $IMAGE echo 'Successfully pushed to Google Container Registry ' $IMAGE # inject envoy proxy kubectl apply -f <(istioctl kube-inject -f "${PWD}/kube/petinfo.yaml")在上面的代码中,高亮显示的行显示了我们如何使用 Istio 命令行工具(istioctl)来将代理注入到我们的各种 Kubernetes 部署中。Petservice 目录下的 petinfo.yaml 文件包含服务、部署和 Ingress的配置。 看起来像:apiVersion: v1 kind: Service metadata: name: petservice labels: app: petservice spec: ports: - port: 9080 name: http selector: app: petservice apiVersion: extensions/v1beta1 kind: Deployment metadata: name: petservice-v1 spec: replicas: 1 template: metadata: labels: app: petservice version: v1 spec: containers: - name: petservice image: gcr.io/nithinistioproject/petservice:feb4v2 imagePullPolicy: IfNotPresent ports: - containerPort: 9080 ########################################################################### # Ingress resource (gateway) ########################################################################## apiVersion: extensions/v1beta1 kind: Ingress metadata: name: gateway annotations: kubernetes.io/ingress.class: "istio" spec: rules: - http: paths: - path: /pet/.* backend: serviceName: petservice servicePort: 9080 ---一旦运行了 deploy.sh,就可以通过执行以下命令来检查确认部署、服务和 Ingress 是否已经创建:mallyn01$ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE petdetailsservice-v1 1 1 1 1 1h petmedicalhistoryservice-v1 1 1 1 1 58m petservice-v1 1 1 1 1 54m mallyn01$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.51.240.1 <none> 443/TCP 2d petdetailsservice ClusterIP 10.51.255.10 <none> 9081/TCP 1h petmedicalhistoryservice ClusterIP 10.51.244.19 <none> 9082/TCP 59m petservice ClusterIP 10.51.242.18 <none> 9080/TCP 1h petservice mallyn01$ kubectl get ing NAME HOSTS ADDRESS PORTS AGE gateway * 108.59.82.93 80 1h mallyn01$ kubectl get pods NAME READY STATUS RESTARTS AGE petdetailsservice-v1-5bb8c65577-jmn6r 2/2 Running 0 12h petmedicalhistoryservice-v1-5757f98898-tq5j8 2/2 Running 0 12h petservice-v1-587696b469-qttqk 2/2 Running 0 12h 当查看控制台中 pod 的信息,即使你只为每个容器部署了一项服务,但仍会注意到有2/2个容器正在运行。 另一个容器是 istioctl 命令注入的边车代理。5. 一旦上述所有内容都运行完毕,您可以使用 Ingress 的 IP 地址去调用示例端点来获取 Pet 的详细信息。mallyn01$ curl http://108.59.82.93/pet/123 "petDetails": { "petName": "Maximus", "petAge": 5, "petOwner": "Nithin Mallya", "petBreed": "Dog" "petMedicalHistory": { "vaccinationList": [ "Bordetella, Leptospirosis, Rabies, Lyme Disease" 注意: 由于 PetService 调用 PetDetailsService 和 PetMedicalHistoryService,实际的调用将如下所示:fetch('http://petdetailsservice:9081/pet/123/details') .then(res => res.text()) .then(body => console.log(body)); fetch('http://petmedicalhistoryservice:9082/pet/123/medicalhistory') .then(res => res.text()) .then(body => console.log(body)); ;结论: 我们覆盖了大量内容 (但这只是第一部分!!)在随后的部分中,将详细介绍如何使用其他 Istio 特性,例如将流量逐步迁移到一个新升级的版本上,使用性能监控仪表板等等。特别感谢 Ray Tsang 的关于 Istio 的 演讲材料 。资源The Istio home page https://istio.io/DevOxx 的Ray Tsang的 Istio 演讲材料: https://www.youtube.com/watch?v=AGztKw580yQ&t=231s案例的Github link: https://github.com/nmallya/istiodemoKubernetes: https://kubernetes.io/微服务: https://martinfowler.com/articles/microservices.htmlSpring Cloud Netflix: https://github.com/spring-cloud/spring-cloud-netflix

eBPF+Ftrace 合璧剑指:no space left on device?

本文地址:https://www.ebpf.top/post/no_space_left_on_devices最近在生产环境中遇到了几次创建容器报错 ”no space left on device“ 失败的案例,但是排查过程中发现磁盘使用空间和 inode 都比较正常。在常规的排查方式都失效的情况下,有没有快速通用思路可以定位问题根源呢?本文是在单独环境中使用 eBPF + Ftrace 分析和排查问题流程的记录,考虑到该方式具有一定的通用性,特整理记录,希望能够起到抛砖引玉的作用。作者水平有限,思路仅供参考,难免存在某些判断或假设存在不足,欢迎各位专家批评指正。1. ”no space left on device“ ???本地复现的方式与生产环境中的问题产生的根源并不完全一致,仅供学习使用。在机器上运行 docker run ,系统提示 “no space left on device” ,容器创建失败:$ sudo docker run --rm -ti busybox ls -hl|wc -l docker: Error response from daemon: error creating overlay mount to /var/lib/docker/overlay2/40de1c588e43dea75cf80a971d1be474886d873dddee0f00369fc7c8f12b7149-init/merged: no space left on device. See 'docker run --help'.错误提示信息表明在 overlay mount 过程中磁盘空间不足,通过 df -Th 命令进行确定磁盘空间情况,如下所示:$ sudo df -Th Filesystem Type Size Used Avail Use% Mounted on tmpfs tmpfs 392M 1.2M 391M 1% /run /dev/sda1 ext4 40G 19G 22G 46% / tmpfs tmpfs 2.0G 0 2.0G 0% /dev/shm tmpfs tmpfs 5.0M 0 5.0M 0% /run/lock /dev/sda15 vfat 98M 5.1M 93M 6% /boot/efi tmpfs tmpfs 392M 4.0K 392M 1% /run/user/1000 overlay overlay 40G 19G 22G 46% /root/overlay2/merged但磁盘空间使用情况表明,系统中挂载的 overlay 设备使用率仅为 46%。根据一些排查经验,我记得文件系统中 inode 如耗尽也可能导致这种情况发生,使用 df -i 对于 inode 容量进行查看:$ sudo df -i Filesystem Inodes IUsed IFree IUse% Mounted on tmpfs 500871 718 500153 1% /run /dev/sda1 5186560 338508 4848052 7% / tmpfs 500871 1 500870 1% /dev/shm tmpfs 500871 3 500868 1% /run/lock /dev/sda15 0 0 0 - /boot/efi tmpfs 100174 29 100145 1% /run/user/1000 overlay 5186560 338508 4848052 7% /root/overlay2/merged从 inode 的情况看,overlay 文件系统中的 inode 使用量仅为 7%,那么是否存在文件被删除了,但仍被使用使用延迟释放导致句柄泄露?抱着最后一根稻草,使用 lsof |grep deleted 命令进行查看,结果也一无所获:$ sudo lsof | grep deleted empty常见的错误场景都进行了尝试后,仍然没有线索,问题一下子陷入了僵局。在常规排查思路都失效的情况下,作为问题排查者有没有一种不过于依赖内核专业人员进行定位问题的方式呢?答案是肯定的,今天舞台的主角属于 ftrace 和 eBPF (BCC 基于 eBPF 技术开发工具集)。2. 问题分析及定位2.1 初步锁定问题函数常规的方式,是通过客户端源码逐步分析,层层递进,但是在容器架构中会涉及到 Docker -> Dockerd -> Containerd -> Runc 一系列链路,分析的过程会略微繁琐,而且也需要一定的容器架构专业知识。因此,这里我们通过系统调用的错误码来快速确定问题,该方式有一定的经验和运气成分。在时间充裕的情况下,还是推荐源码逐步定位分析的方式,既能排查问题也能深入学习。”no space left on device“ 的错误,在内核 include/uapi/asm-generic/errno-base.h 文件定义:一般可以拿报错信息在内核中直接搜索。#define ENOSPC 28 /* No space left on device */BCC 提供了系统调用跟踪工具 syscount-bpfcc,可基于错误码来进行过滤,同时该工具也提供了 -x 参数过滤失败的系统调用,在诸多场景中都非常有用。请注意,syscount-bpfcc 后缀 bpfcc 为 Unbuntu 系统中专有后缀,BCC 工具中的源码为 syscount。首先我们先简单了解一下 syscount-bpfcc 工具的使用帮助:$ sudo syscount-bpfcc -h usage: syscount-bpfcc [-h] [-p PID] [-i INTERVAL] [-d DURATION] [-T TOP] [-x] [-e ERRNO] [-L] [-m] [-P] [-l] Summarize syscall counts and latencies. optional arguments: -h, --help show this help message and exit -p PID, --pid PID trace only this pid -i INTERVAL, --interval INTERVAL print summary at this interval (seconds) -d DURATION, --duration DURATION total duration of trace, in seconds -T TOP, --top TOP print only the top syscalls by count or latency -x, --failures trace only failed syscalls (return < 0) -e ERRNO, --errno ERRNO trace only syscalls that return this error (numeric or EPERM, etc.) -L, --latency collect syscall latency -m, --milliseconds display latency in milliseconds (default: microseconds) -P, --process count by process and not by syscall -l, --list print list of recognized syscalls and exit在 syscount-bpfcc 参数中,通过 -e 参数指定我们要过滤返回 ENOSPEC 错误的系统调用:$ sudo syscount-bpfcc -e ENOSPC Tracing syscalls, printing top 10... Ctrl+C to quit. ^C[08:34:38] SYSCALL COUNT mount 1跟踪结果表明系统调用 mount 返回了 ENOSPEC 错误。如果需要确定出错系统调用 mount 的调用程序,我们可通过 "-P" 参数来按照进程聚合显示:$ sudo syscount-bpfcc -e ENOSPC -P Tracing syscalls, printing top 10... Ctrl+C to quit. ^C[08:35:32] PID COMM COUNT 3010 dockerd 1跟踪结果表明,dockerd 后台进程调用的系统调用 mount 返回了 ENOSPEC 错误 。syscount-bpfcc 可通过参数 -p 指定进程 pid 进行跟踪,适用于我们确定了特定进程后进行排查的场景。如果有兴趣查看相关实现的代码,可在命令行后添加 --ebpf 参数打印相关的源码 syscount-bpfcc -e ENOSPC -p 3010 --ebpf。借助于 syscount-bpfcc 工具跟踪的结果,我们初步确定了 dokcerd 系统调用 mount 的返回 ENOSPC 报错。mount 系统调用为 sys_mount,但是 sys_mount 函数在新版本内核中,并不是我们直接可以跟踪的入口,这点需要注意。这是因为 4.17 内核对于系统调用做了一些调整,在不同的平台上会添加对应体系架构,详情可以参见 new BPF APIs to get kernel syscall entry func name/prefix。排查的系统为 Ubuntu 21.10,5.13.0 内核,体系架构为 ARM64,因此 sys_mount 在内核真正入口函数为 __arm64_sys_mount:如果体系结构为 x86_64,那么 sys_mount 对应的函数则为 __x64_sys_mount,其他体系结构可在 /proc/kallsyms 中搜索确认。到目前为止,我们已经确认了内核入口函数 __arm64_sys_mount ,但是如何定位错误具体出现在哪个子调用流程呢?毕竟,内核中的函数调用路径还是偏长,而且还可能涉及到各种跳转或者特定的实现。为了确定出错的子流程,首先我们需要获取到 __arm64_sys_mount 调用的子流程,ftrace 中的 function_graph 跟踪器则可大显身手。本文中,我直接使用项目 perf-tools 工具集中的前端工具 funcgraph,这可以完全避免手动设置各种跟踪选项。如果你对 ftrace 还不熟悉,建议后续学习 Ftrace 必知必会。2.2 定位问题根源perf-tools 工具集中的 funcgraph 函数可用于直接跟踪内核函数的调用子流程。funcgraph 工具使用帮助如下所示:$ sudo ./funcgraph -h USAGE: funcgraph [-aCDhHPtT] [-m maxdepth] [-p PID] [-L TID] [-d secs] funcstring -a # all info (same as -HPt) -C # measure on-CPU time only -d seconds # trace duration, and use buffers -D # do not show function duration -h # this usage message -H # include column headers -m maxdepth # max stack depth to show -p PID # trace when this pid is on-CPU -L TID # trace when this thread is on-CPU -P # show process names & PIDs -t # show timestamps -T # comment function tails funcgraph do_nanosleep # trace do_nanosleep() and children funcgraph -m 3 do_sys_open # trace do_sys_open() to 3 levels only funcgraph -a do_sys_open # include timestamps and process name funcgraph -p 198 do_sys_open # trace vfs_read() for PID 198 only funcgraph -d 1 do_sys_open >out # trace 1 sec, then write to file首次使用,我先通过参数 -m 2 将子函数跟踪的层级设定为 2 ,避免一次查看到过深的函数调用。$ sudo ./funcgraph -m 2 __arm64_sys_mount如何对于跟踪结果在 vim 中折叠显示,可参考 ftrace 必知必会 中的对应章节。gic_handle_irq() 看名字是处理中断相关的函数,我们可以忽略相关调用。通过使用 funcgraph 跟踪的结果,我们可以获取到 __arm64_sys_mount 函数中调用的主要子流程函数。在内核函数调用过程中,如果遇到出错,一般会直接跳转到错误相关的清理函数逻辑中(不再继续调用后续的子函数),这里我们可将注意力从 __arm64_sys_mount 函数转移到尾部的内核函数 path_mount 中重点分析。对于 path_mount 函数查看更深层级调用分析:$ sudo ./funcgraph -m 5 path_mount > path_mount.log基于内核函数中的最后一个可能出错函数调用逐步分析,我们可获得调用关系的逻辑:__arm64_sys_mount() -> path_mount() -> do_new_mount() -> do_add_mount() -> graft_tree() -> attach_recursive_mnt() -> count_mounts()基于上述的函数调用关系,我们很自然推测是 count_mounts 函数返回了错误,最终通过 __arm64_sys_mount 函数返回到了用户空间。既然是推测,就需要通过手段进行验证,我们需要获取到整个函数调用链的返回值。通过 BCC 工具集中 trace-bpfcc 进行相关函数返回值进行跟踪。trace-bpfcc 的帮助文档较长,可在 trace_example.txt 文件中查看,这里从略。在使用 trace-bpfcc 工具跟踪前,我们需要在内核中查看一下相关函数的原型声明。为了验证猜测,我们需要跟踪整个调用链上核心函数的返回值。trace-bpfcc 工具一次性可以跟踪多个函数返回值,通过 'xxx' 'yyy' 进行分割。$ sudo trace-bpfcc 'r::__arm64_sys_mount() "%llx", retval' \ 'r::path_mount "%llx", retval' \ 'r::do_new_mount "%llx", retval' \ 'r::do_add_mount "%llx", retval'\ 'r::graft_tree "%llx", retval' \ 'r::attach_recursive_mnt "%llx" retval'\ 'r::count_mounts "%llx", retval' PID TID COMM FUNC - 3010 3017 dockerd graft_tree ffffffe4 3010 3017 dockerd attach_recursive_mnt ffffffe4 3010 3017 dockerd count_mounts ffffffe4 3010 3017 dockerd __arm64_sys_mount ffffffffffffffe4 3010 3017 dockerd path_mount ffffffe4 3010 3017 dockerd do_new_mount ffffffe4 3010 3017 dockerd do_add_mount ffffffe4其中 r:: __arm64_sys_mount "%llx", retval 命令解释如下:r:: __arm64_sys_mount r 表示跟踪函数返回值;"%llx", retval 中 retval 为函数返回值变量, ”%llx“ 为返回值打印的格式;跟踪到的返回值 0xffffffe4 转成 10 进制,则正好为 -28(0x1B),= -ENOSPC(28)。Trace-bfpcc 底层使用的 perf_event 事件触发,由于多核并发,顺序不能完全保障,在高内核版本中,事件触发切换成 Ring Buffer 机制则可保证顺序。2.3 定位问题的根因通过剥茧抽丝,我们将问题缩小至 count_mounts 函数中。这时,我们需要通过源码分析函数的主流程逻辑,这里直接上代码,幸运的是该函数代码胆小精悍,比较容易理解:int count_mounts(struct mnt_namespace *ns, struct mount *mnt) // 可以加载的最大值是通过 sysctl_mount_max 的变量读取的 unsigned int max = READ_ONCE(sysctl_mount_max); unsigned int mounts = 0, old, pending, sum; struct mount *p; for (p = mnt; p; p = next_mnt(p, mnt)) mounts++; old = ns->mounts; // 当前 namespace 挂载的数量 pending = ns->pending_mounts; // 按照字面意思理解是 pending 的加载数量 sum = old + pending; // 加载的总和为 已经加载的数量 + 在路上加载的数量 if ((old > sum) || (pending > sum) || (max < sum) || (mounts > (max - sum))) // 那么这些条件的判断也就比较容易理解了 return -ENOSPC; ns->pending_mounts = pending + mounts; return 0; }通过代码逻辑的简单理解,我们可确定是当前 namespace 中加载的文件数量超过了系统所允许的 sysctl_mount_max 最大值, 其中 sysctl_mount_max 可通过 /proc/sys/fs/mount-max 设定)。为复现问题,本地环境中我将 /proc/sys/fs/mount-max 的值被设置为了 10(默认值为 100000),达到了与生产环境中一样的报错。$ sudo cat /proc/sys/fs/mount-max 10在根源定位以后,我们将该值调大为默认值 100000,重新 docker run 命令即可成功。当然在生产环境中的情况会比该场景更加复杂,即可能为mount 的异常或也可能为泄露,但排查的思路却可以参考本文提供的思路。至此,我们完成问题定位,貌似已经可完美收工,但是等等,这里还有部分跟踪过程中的疑惑需要澄清,这也是本次排查问题时候积累的经验(踩过的坑),并且这些经验对于运用工具排查问题所必须要了解的内容。基于我们看到的代码进行分析,在实际的跟踪过程中,是否能够所见即所得与代码完全一致呢? 答案是未必,sys_mount 跟踪就属于这种不一致的场景。那么,让我们先来通过 sys_mount 的源码流程与实际跟踪的进行对比分析,来一探究竟。3. 代码流程与跟踪流程差异分析函数 sys_mount 定义在 fs/namespace.c 文件中:SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data) int ret; char *kernel_type; char *kernel_dev; void *options; kernel_type = copy_mount_string(type); // 子函数 1 ret = PTR_ERR(kernel_type); if (IS_ERR(kernel_type)) goto out_type; kernel_dev = copy_mount_string(dev_name); // 子函数 2 ret = PTR_ERR(kernel_dev); if (IS_ERR(kernel_dev)) goto out_dev; options = copy_mount_options(data); // 子函数 3 ret = PTR_ERR(options); if (IS_ERR(options)) goto out_data; ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options); // 子函数 4 kfree(options); out_data: kfree(kernel_dev); out_dev: kfree(kernel_type); out_type: return ret; }通过代码分析,我们可以印证上述从最后子函数调用分析的依据:在函数调用出错时,则不再直接调用后续函数,直接跳转到函数出错部分处理,例如copy_mount_string 函数调用出错,则会直接跳转到函数的清理部分 out_type: ,其后的子函数 copy_mount_string/copy_mount_options/do_mount 将不再被调用,这也是我们定位出错函数为什么直接从最后子函数进行分析的原因。通过简单代码调用关系分析,我们可以得到如下调用关系:__arm64_sys_mount()-> copy_mount_string()-> copy_mount_string()-> copy_mount_options()-> do_mount()但是,细心的读者肯定已经发现了一些端倪,代码分析调用流程和我们实际跟踪的调用流程,并不能直接对应起来。基于我们前面使用 funcgraph 工具跟踪到调用关系则如下所示:__arm64_sys_mount()-> strndup_user()-> strndup_user()-> copy_mount_options()-> path_mount()-> path_put()的确,这并不是所见即所得的效果。但是通过简单分析,我们还是比较容易发现两者的对应关系:其中 copy_mount_string() 与 strndup_user() 函数关系如下,定义在 fs/namespace.c 文件中:static char *copy_mount_string(const void __user *data) return data ? strndup_user(data, PATH_MAX) : NULL; }之所以代码与实际跟踪的结果不一致,这是因为,编译内核的时一般都会设置了 -O2 或 -Os 进行代码级别的优化 ,例如调用展开。这里的 do_mount() 函数的情况也是类似。关于代码编译优化的详情,可以参考 Linux 编译优化这篇文档。除了使用 funcgraph 跟踪分析外,我们也可通过安装调试符号,使用 gdb 调试通过反汇编进行确认。通过此处分析,我们可以得到这样的常识:即使通过源码了解到了函数调用关系,也需要在跟踪前通过 funcgraph 工具进行确认。在本次问题排查开始,我一直尝试使用 BCC trace-bpfcc 工具对 do_mount 进行结果跟踪,但总是不能够得到结果,也让自己对于排查思路产生过怀疑。4. 总结至此,我们通过 funcgraph 工具配合基于 eBPF 技术 BCC 项目中的 syscount-bpfcc 和 trace-bpfcc 等工具,快速定位到了 mount 报错的异常函数。尽管排查是基于 mount 报错,但思路也适用于其他场景下的问题分析和排查。同时,该思路,也可以作为源码阅读和分析内核代码时的有益的工具补充。最后,也希望本文能给大家带来思路的参考,如果你发现文中的错误或者有更好的案例,也期待留言交流。5. 附录部分错误$ sudo syscount-bpfcc Traceback (most recent call last): File "/usr/lib/python3/dist-packages/bcc/syscall.py", line 379, in <module> out = subprocess.check_output(['ausyscall', '--dump'], stderr=subprocess.STDOUT) File "/usr/lib/python3.9/subprocess.py", line 424, in check_output return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, File "/usr/lib/python3.9/subprocess.py", line 505, in run with Popen(*popenargs, **kwargs) as process: File "/usr/lib/python3.9/subprocess.py", line 951, in __init__ self._execute_child(args, executable, preexec_fn, close_fds, File "/usr/lib/python3.9/subprocess.py", line 1821, in _execute_child raise child_exception_type(errno_num, err_msg, err_filename) FileNotFoundError: [Errno 2] No such file or directory: 'ausyscall' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/usr/sbin/syscount-bpfcc", line 20, in <module> from bcc.syscall import syscall_name, syscalls File "/usr/lib/python3/dist-packages/bcc/syscall.py", line 387, in <module> raise Exception("ausyscall: command not found") Exception: ausyscall: command not found系统缺少 ausyscall 命令,各系统安装方式参见 ausyscall:$ sudo apt-get install auditd参考资料Docker cp 提示“no space left on device”Docker cp 提示“no space left on device”[WIP] Fix mount loop on "docker cp"

BTFGen: 让 eBPF 程序可移植发布更近一步

本文地址:https://ebpf.top/post/btfgen-one-step-closer-to-truly-portable-ebpf-programsMauricio 2022 2022/03/16eBPF 是一项广为人知的技术,已经在可观测、网络和安全领域领域得到广泛应用。Linux 操作系统提供了虚拟机,可用于安全和高效的方式运行 eBPF 程序【译者注:如果是 JIT 模式则会直接翻译成本地 CPU 指令,则不需要虚拟机】。eBPF 程序挂载在操作系统提供的钩子上,使其能够在内核中发生特定事件时过滤和提取感兴趣的信息。在本文中,我们将介绍助力 eBPF 程序移植的工具 BTFGen,以及其如何被集成到其他项目中,主要的内容如下:在不同的目标机器上运行 eBPF 程序的挑战;传统上如何通过在加载程序之前通过编译解决的;解决挑战对应的的机制/工具(如 CO-RE 一次编译 - 到处运行);BTFHub 工具如何尝试解决这些挑战。1. 问题所在eBPF 程序需要访问内核结构来获取需要的数据,因此依赖于内核结构的布局。为特定内核版本编译的 eBPF 程序通常不能在另一个内核版本上工作,这是因为相关的内核数据结构布局可能会发生了变化:比如字段添加、删除,或类型被改变,甚至内核编译配置的改变也会改变整个结构布局。例如,禁用 CONFIG_THREAD_INFO_IN_TASK 会改变 task_struct 的所有成员变量的偏移:struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK * For reasons of header soup (see current_thread_info()), this * must be the first element of task_struct. struct thread_info thread_info; #endif unsigned int __state; #ifdef CONFIG_PREEMPT_RT /* saved state for "spinlock sleepers" */ unsigned int saved_state; #endif ...该问题的解决通常是在目标机器使用内核头文件编译 eBPF 程序并进行加载,BCC 项目所使用的正是这种方式。但该方法存在以下问题:必须在目标机器上安装占用大量空间的编译器;编译程序时需要资源,在某些情况下可能会影响工作负载的调度;编译需要相当长的时间,因此事件采集会存在一些延迟;依赖于目标机器上安装内核头文件包。2. CO-RE (一次编译 - 到处运行)CO-RE 机制正是为解决上述问题提出的方案。在该方案中,eBPF 程序一次编译,然后在运行时进行更新(patched):基于运行的机器的内核结构布局更新运行指令。BPF CO-RE (Compile Once - Run Everywhere) 介绍了该技术背后的所有细节。对于本文,需要理解的是 CO-RE 需要有目标内核的 BTF 信息(BPF Type Format 类型格式)。BTF 信息由内核本身提供的,这需要在内核编译时设置 CONFIG_DEBUG_INFO_BTF=y 选项 。该选项在Linux 内核 5.2 中引入的,许多流行的 Linux 发行版在其后的部分内核版本才默认启用。这意味着有很多用户运行的内核并没有导出 BTF 信息,因此不能使用基于 CO-RE 的工具。3. BTFHubBTFHub 是 Aqua Security 公司的一个项目,其可为不导出 BTF 信息的流行发行版内核提供BTF 信息补充。目标内核的 BTF 文件可以在运行时下载,然后与加载库(libbpf、cilium/ebpf 或其他)配合,加载库基于 BTF 文件对程序进行相应的更新(patch)。尽管 BTFHub 做了很大的改进,但是它仍然面临着一些挑战:每个 BTF 文件有数 MB 大小,因此不可能把所有内核的 BTF 文件和应用程序一起打包,因为这可能需要数 GB 的空间占用。另一种方法是在运行时下载当前内核的所需 BTF,但这也带来了一些问题:延迟 eBPF 程序启动,而且在某些情况下,连接到外部主机下载文件也不可行。4. BTFGen其实,通常我们并不需要提供描述所有内核类型的完整 BTF 文件,因为 eBPF 程序通常只需要访问其中的少数类型。一个 "精简版" 的 BTF 文件,只需要提供程序使用类型的信息就足够了。这就是工具 BTFGen 发挥作用:其可以生成一组 eBPF 程序所需的精简的 BTF ,通过该方式生成的 BTF 文件只有数 KB 大小,将其与应用程序打包交付变成了可能。BTFGen 并不是单独提供能力的。它需要具有不同 Linux 发行版的所有内核类型的源 BTF 文件(由 BTFHub 提供),并且 CO-RE 机制(在libbpf、Linux内核或另一个加载库中)在加载程序时通过打补丁方式更新 eBPF 程序。使用 BTFGen 的主要流程如下:开发人员编写基于基于 CO-RE 的 eBPF 程序,并通过 llvm/clang 编译成对象文件;从 BTFHub 或其他来源收集不同 Linux 内核发行版的 BTF 源文件;使用 BTFGen 生成精简版的 BTF 文件;将精简版的 BTF 文件与应用程序打包分发。4.1 内部实现细节BTFGen 在 bpftool 工具中实现,其使用 libbpf CO-RE 逻辑来解决重定位问题。有了这些信息,它就能挑选出重新定位所涉及的类型来生成 "精简版" 的 BTF 文件。这篇文章的目的不是要解释所有的内部实现细节。如果你想知道更多,你可以查看 BTFHub 仓库中的这个文档或实现它的补丁。4.2 如何使用?本节提供了 BTFGen 工具使用的更多细节。在本例中,我们将使用 BTFGen 来实现内核未启用 CONFIG_DEBUG_INFO_BTF 选项的机器上运行特定的 BCC libbpf-tools 工具。其他 eBPF 应用程序集成的方式也是类似。为了实现上述的目的,我们需要以下流程:下载、编译和安装支持 BTFGen 的 bpftool 版本;从 BTFHub 下载所需的 BTF 文件;下载和编译 BCC 工具;使用 BTFGen 为特定的 BCC 工具生成 "精简版" BTF 文件;调整 BCC 工具代码,使其可以从自定义路径加载 BTF 文件;最后进行验证。首先,我们为该演示创建一个临时目录:$ mkdir /tmp/btfgendemo安装 bpftool 工具BTFGen 刚刚被合入 bpftool。在 BTFGen 未被包含在不同发行版的软件包之前,我们需要从源代码进行编译:$ cd /tmp/btfgendemo $ git clone --recurse-submodules https://github.com/libbpf/bpftool.git $ cd bpftool/src $ make $ sudo make install从 BFThub 获取内核对应的 BTF 文件这里为简洁起见,我们只考虑 Ubuntu Focal 系统中使用的场景,该方式也完全适用于 BTFHub 支持的其他发行版本。$ cd /tmp/btfgendemo $ git clone https://github.com/aquasecurity/btfhub-archive $ cd btfhub-archive/ubuntu/focal/x86_64/ $ for f in *.tar.xz; do tar -xf "$f"; done $ ls -lhn *.btf | head -rw-r----- 1 1000 1000 4,5M Sep 29 13:36 5.11.0-1007-azure.btf -rw-r----- 1 1000 1000 4,8M Aug 10 23:33 5.11.0-1009-aws.btf -rw-r----- 1 1000 1000 4,8M Jan 22 12:29 5.11.0-1009-gcp.btf -rw-r----- 1 1000 1000 4,5M Sep 29 13:38 5.11.0-1012-azure.btf -rw-r----- 1 1000 1000 4,5M Sep 29 13:40 5.11.0-1013-azure.btf -rw-r----- 1 1000 1000 4,8M Aug 10 23:39 5.11.0-1014-aws.btf -rw-r----- 1 1000 1000 4,8M Jan 22 12:32 5.11.0-1014-gcp.btf -rw-r----- 1 1000 1000 4,5M Sep 29 13:43 5.11.0-1015-azure.btf -rw-r----- 1 1000 1000 4,8M Sep 7 22:52 5.11.0-1016-aws.btf -rw-r----- 1 1000 1000 4,8M Sep 7 22:57 5.11.0-1017-aws.btf如上述显示,我们可以看到每个内核对应的 BTF 文件的大小约为 4MB。$ find . -name "*.btf" | xargs du -ch | tail -n 1 944M total但是汇总起来看,仅 Ubuntu Focal 就有~944MB 的大小,将其与应用程序一起打包显然不太可行。下载、修改和编译 BCC libbpf 工具我们从 BCC v0.24.0 标签上克隆仓库代码:$ cd /tmp/btfgendemo $ git clone https://github.com/iovisor/bcc -b v0.24.0 --recursive默认情况下,不同的 BCC 工具会尝试从约定目录中加载 BTF 信息。正常情况下,我们不能直接覆盖对应的文件,因为它们极有可能也会被其他工具所依赖。相反,我们可以修改 BCC 工具源码,让其从一个自定义的路径加载 BTF 文件。我们可以使用 LIBBPF_OPTS()来声明一个 bpf_object_open_opts 结构,将其中的 btf_custom_path 字段设置为自定义 BTF 所在的路径,并将其传递给 TOOL_bpf__open_opts()函数。我们尝试使用如下的补丁来修改 opennoop、execsnoop 和 bindsnoop 工具。译者注,约定的加载 BTF 目录如下:{ "/sys/kernel/btf/vmlinux", true /* raw BTF */ },{ "/boot/vmlinux-%1$s" }, { "/lib/modules/%1$s/vmlinux-%1$s" }, { "/lib/modules/%1$s/build/vmlinux" }, { "/usr/lib/modules/%1$s/kernel/vmlinux" }, { "/usr/lib/debug/boot/vmlinux-%1$s" }, { "/usr/lib/debug/boot/vmlinux-%1$s.debug" }, { "/usr/lib/debug/lib/modules/%1$s/vmlinux" },```bash # /tmp/btfgendemo/bcc.patch diff --git a/libbpf-tools/bindsnoop.c b/libbpf-tools/bindsnoop.c index 5d87d484..a336747e 100644 --- a/libbpf-tools/bindsnoop.c +++ b/libbpf-tools/bindsnoop.c @@ -187,7 +187,8 @@ int main(int argc, char **argv) libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); - obj = bindsnoop_bpf__open(); + LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf"); + obj = bindsnoop_bpf__open_opts(&opts); if (!obj) { warn("failed to open BPF object\n"); return 1; diff --git a/libbpf-tools/execsnoop.c b/libbpf-tools/execsnoop.c index 38294816..9bd0d077 100644 --- a/libbpf-tools/execsnoop.c +++ b/libbpf-tools/execsnoop.c @@ -274,7 +274,8 @@ int main(int argc, char **argv) libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); - obj = execsnoop_bpf__open(); + LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf"); + obj = execsnoop_bpf__open_opts(&opts); if (!obj) { fprintf(stderr, "failed to open BPF object\n"); return 1; diff --git a/libbpf-tools/opensnoop.c b/libbpf-tools/opensnoop.c index 557a63cd..cf2c5db6 100644 --- a/libbpf-tools/opensnoop.c +++ b/libbpf-tools/opensnoop.c @@ -231,7 +231,8 @@ int main(int argc, char **argv) libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); - obj = opensnoop_bpf__open(); + LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf"); + obj = opensnoop_bpf__open_opts(&opts); if (!obj) { fprintf(stderr, "failed to open BPF object\n"); return 1; $ cd bcc $ git apply /tmp/btfgendemo/bcc.patch $ cd libbpf-tools/ $ make -j$(nproc)生成 "精简版" BTF 文件这里,我们将使用 bpftool gen min_core_btf 命令为 BCC 工具中的 bindsnoop、execsnoop 和opensnoop 同时生成精简的 BTF 文件。下述的命令对目录中存在的每个 BTF 文件逐次调用 bpftool 工具进行精简。$ OBJ1=/tmp/btfgendemo/bcc/libbpf-tools/.output/bindsnoop.bpf.o $ OBJ2=/tmp/btfgendemo/bcc/libbpf-tools/.output/execsnoop.bpf.o $ OBJ3=/tmp/btfgendemo/bcc/libbpf-tools/.output/opensnoop.bpf.o $ mkdir -p /tmp/btfgendemo/btfs $ cd /tmp/btfgendemo/btfhub-archive/ubuntu/focal/x86_64/ $ for f in *.btf; do bpftool gen min_core_btf "$f" \ /tmp/btfgendemo/btfs/$(basename "$f") $OBJ1 $OBJ2 $OBJ3; \ $ ls -lhn /tmp/btfgendemo/btfs | head total 864K -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1007-azure.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1009-aws.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1009-gcp.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1012-azure.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1013-azure.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1014-aws.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1014-gcp.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1015-azure.btf -rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1016-aws.btf精简后生成的 BTF 文件大约为 1.1KB,Ubuntu Focal 对应的所有文件的大小为 864KB,将其与程序一起打包完全可行。如果我们对生成的文件进一步进行压缩,其大小还可以大幅缩减:$ cd /tmp/btfgendemo/btfs $ tar cvfJ compressed.tar.xz *.btf $ ls -lhn compressed.tar.xz -rw-r--r-- 1 1000 1000 2,5K Feb 17 15:19 compressed.tar.xz压缩率如此之高是因为许多生成的文件相同,我们将在下文中进一步讨论。验证为了验证,我们需要运行一台装有 Ubuntu Focal 的机器。这里提供的 Vagrant 文件可以用来创建对应的虚拟机。请注意,Ubuntu Focal 从内核 5.4.0-92-generic 版本开始启用 BTF 支持,所以我们需要运行其早期的版本进行验证。我们使用 bento/ubuntu-20.04 Vagrant 虚拟机中的 202012.21.0 版本,内核为 5.4.0-58-generic。本文使用 sshfs 在主机和虚拟机之间共享文件,需要我们确保已经安装了 vagrant-sshfs 插件。译者注:$ sudo vagrant plugin install vagrant-sshfs# /tmp/btfgendemo/Vagrantfile Vagrant.configure("2") do | config | config.vm.box = "bento/ubuntu-20.04" config.vm.box_version = "= 202012.21.0" config.vm.synced_folder "/tmp/btfgendemo", "/btfgendemo", type: "sshfs" config.vm.provider "virtualbox" do | vb | vb.gui = false vb.cpus = 4 vb.memory = "4096" end启动虚拟机并使用 ssh 登录:$ vagrant up $ vagrant ssh后续的命令必须在虚拟机内执行。检查内核版本:$ uname -a Linux vagrant 5.4.0-58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux让我们检查内核是否启用了CONFIG_DEBUG_INFO_BTF:$ cat /boot/config-$(uname -r) | grep CONFIG_DEBUG_INFO_BTF CONFIG_DEBUG_INFO_BTF is not set在把 BTF 文件复制到正确路径之前,我们尝试运行以下这些工具:$ sudo /btfgendemo/bcc/libbpf-tools/execsnoop libbpf: failed to parse target BTF: -2 libbpf: failed to perform CO-RE relocations: -2 libbpf: failed to load object 'execsnoop_bpf' libbpf: failed to load BPF skeleton 'execsnoop_bpf': -2 failed to load BPF object: -2正如预期,我们运行工具失败,因为工具不能找到执行 CO-RE 重定位所需的 BTF 信息。接着,我们将该内核版本的 BTF 文件复制到对应目录:$ cp /btfgendemo/btfs/$(uname -r).btf /tmp/vmlinux.btf将复制 BTF 到指定目录后,工具运行正常:$ sudo /btfgendemo/bcc/libbpf-tools/execsnoop PCOMM PID PPID RET ARGS $ sudo /btfgendemo/bcc/libbpf-tools/bindsnoop PID COMM RET PROTO OPTS IF PORT ADDR $ sudo /btfgendemo/bcc/libbpf-tools/opensnoop PID COMM FD ERR PATH ^C当然这只是为了演示工具工作流程的样例。真正的集成需要负责基于主机的内核版本自动提供对应的 BTF 文件。下面的部分通过两个例子展示了对应的集成。4.3 集成样例在本节中,我们将介绍 Inspektor Gadget 和 Tracee 项目是如何使用 BTFGen。Inspektor GadgetInspektor Gadget 是一个用于调试/检查 Kubernetes 资源和应用程序的工具集。由于Inspektor Gadget 是以容器镜像的形式发布的,我们选择在其中为不同的 Linux发行版搭载 BTF 文件。我们在 Docker 文件中添加了一个步骤,使其可以在构建容器镜像时生成 BTF 文件:RUN set -ex; \ if [ "$ENABLE_BTFGEN" = true ]; then \ cd /btf-tools && \ LIBBPFTOOLS=/objs BTFHUB=/tmp/btfhub INSPEKTOR_GADGET=/gadget ./btfgen.sh; \ fi辅助脚本 btfgen.sh 调用 bpftool 为 BTFHub 支持的所有内核生成 BTF 文件。我们修改 entrypoint 脚本,在容器文件系统上安装正确的 BTF 文件,使对应的工具都能运行。Inspektor 工具被设计成总是在容器中运行,因此我们可以将 BTF 文件安装在系统路径(/boot/vmlinux-$(uname -r)),而不影响主机。通过这样做,我们还可以避免修改不同的 BCC 工具的源代码(就像我们在上面的例子中做的那样):echo "Kernel provided BTF is not available: Trying shipped BTF files" SOURCE_BTF=/btfs/$ID/$VERSION_ID/$ARCH/$KERNEL.btf if [-f $SOURCE_BTF]; then objcopy --input binary --output elf64-little --rename-section .data=.BTF $SOURCE_BTF /boot/vmlinux-$KERNEL echo "shipped BTF available. Installed at /boot/vmlinux-$KERNEL" ...PR 完整实现参见 inspektor-gadget/pull/387 。TraceeTracee 是用于 Linux 的运行时安全和取证工具。这里,生成的 BTF 文件可被嵌入到应用程序的二进制文件中。Makefile 有一个 btfhub 目标,然后调用 btfhub.sh。脚本克隆 BTFHub 仓库,并调用 btfgen.sh 来生成 BTF 文件。这些文件被移到 ./dist/btfhub 目录中。# generate tailored BTFs [ ! -f ./tools/btfgen.sh ] && die "could not find btfgen.sh" ./tools/btfgen.sh -a ${ARCH} -o $TRACEE_BPF_CORE # move tailored BTFs to dist [ ! -d ${BASEDIR}/dist ] && die "could not find dist directory" [ ! -d ${BASEDIR}/dist/btfhub ] && mkdir ${BASEDIR}/dist/btfhub rm -rf ${BASEDIR}/dist/btfhub/* mv ./custom-archive/* ${BASEDIR}/dist/btfhub然后,使用 go:embed 指令将 BTF 文件嵌入到 Go 二进制中。//go:build ebpf // +build ebpf package tracee import ( "embed" //go:embed "dist/tracee.bpf.core.o" //go:embed "dist/btfhub/*" var BPFBundleInjected embed.FS在运行时,当前内核的对应的 BTF文件被解压,其路径传递给 libbpf-go,用于 CO-RE 重定位。4.4 限制内核中的 BTF 支持不仅仅是关于导出 BTF 类型。部分 eBPF 程序如 fentry/fexit 和 LSM 钩子需要内核导出 BTF 信息。这些程序将不能使用 BTFGen,唯一的选择是启用 CONFIG_DEBUG_INFO_BTF 的内核。4.5 未来发展当然,我们知道 BTFGen 是一个临时的解决方案,直到大多数系统更新到默认导出 BTF 信息的内核版本。然而,我们认为这需要几年的时间,在这期间,BTFGen 可以帮助填补这一空白。以下是我们可以近期考虑的一些改进。与其他项目的整合部分项目如 BCC 及其基于 libbpf 的工具都可以与 BTFGen 整合获益。 我们提交了一个 PR,通过使用 BTFGen 使上述工具可以在更多的 Linux 发行版中使用。删除重复的文件eBPF 程序通常访问很少的内核类型,因此,两个不同的内核版本生成的文件很有可能是相同的,这对于同一 Linux 发行版的小版本内核来说尤其如此。对 BTFGen 的进一步改进是基于此,通过使用符号链接或类似的方法来避免创建重复的文件。这也可以直接在 BTFHub 上进行,因为有些源 BTF 文件是重复的,就像这个问题中所指出的那样,但即使在这种情况下,出现重复文件的机会还是较低。在线 APIBTFHub 仓库体积很大,而且由于新内核的发布,它的规模还在不断增加。Seekret 创建了一个 API,使用 BTFGen 和 BTFHub 为用户提供的 eBPF 对象按需生成 "精简版" BTF 文件。5. 更多信息如果你想了解更多关于 eBPF、BTF、CO-RE、BTFHub 和 BTFGen 的信息,以下资料无疑是优秀的参考:BPF CO-RE参考指南:从开发者的角度解释了如何使用 CO-RE。BPF CO-RE (Compile Once - Run Everywhere):解释了 CO-RE和该机制中涉及的不同组件。eBPF BTF 生成器:通往真正的可移植 CO-RE eBPF 程序之路:深入探讨 BTFGen 的实现。BPF类型格式(BTF):BTF的内核文档。6. 致谢功能从已经存在的项目中获得了灵感,并由不同的公司联合实现。首先,我们要感谢 Aqua Security 团队在 BTFHub 上所做的出色工作,这是我们的基础项目。其次,我们要感谢在这个功能的开发过程中做出贡献的人员。Aqua Security 的 Rafael David Tinoco 和 Elastic 的 Lorenzo Fontana 和 Leonardo Di Donato。最后,libbpf 的维护者 Andrii Nakryiko 和 Alexei Starovoitov 以及 bpftool 的维护者 Quentin Monnet,他们为实现该功能提供了大量宝贵的反馈和指导。原文地址: https://kinvolk.io/blog/2022/03/btfgen-one-step-closer-to-truly-portable-ebpf-programs/Mauricio 2022 2022/03/16

Ubuntu 21.10 安装调试符号

1. 背景Linux 内核中的调试符号包含源代码级别的信息,如函数名称、函数调用约定、以及源代码行号到指令的映射。这些信息在调试或剖析内核的时候非常有用。在本文中,我将展示如何在 Ubuntu 上获得任何内核的调试符号。通常来说,有 2 种方法可以使用调试符号:使用源码构建带有调试符号的内核源代码,通常适用于自己修改源码编译的场景,构建内核的过程依据编译选项,一般会耗费比较长的时间;使用现成包含编译好的调试符号包进行安装;这里主要讨论安装调试符号包的方式,包括手动下载安装和第三方源安装的方式。本地的环境为 Ubuntu 21.10 版本,代号为 impish,内核版本如下所示:$ uname -a Linux ubuntu21-10 5.13.0-20-generic #20-Ubuntu SMP Fri Oct 15 14:21:35 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux2. 手动搜索下载安装首先我们可以从 Ubuntu 官方网址 中进行调试符号安装包,其中 impish 为 Ubuntu 21.10 的代号。搜索的时候可以使用 "linux-image-unsigned-`uname -r`-dbgsym" 作为关键词,uname -r 为本地安装的内核版本,需要搜索前进行运行替换。5.13.0-20-generic 版本可以直接下载 linux-image-unsigned-5.13.0-20-generic-dbgsym_5.13.0-20.20_amd64.ddeb3. 使用第三方源安装步骤 1:GPG 秘钥导入请确保已经导入系统的 GPG 密钥。对于 Ubuntu 16.04 及以上版本:$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C8CAB6595FDFF622其他旧的版本命令如下:$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ECDCAD72428D7C01步骤 2:添加仓库配置$ codename=$(lsb_release -c | awk '{print $2}') sudo tee /etc/apt/sources.list.d/ddebs.list << EOF deb http://ddebs.ubuntu.com/ ${codename} main restricted universe multiverse deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse deb http://ddebs.ubuntu.com/ ${codename}-updates main restricted universe multiverse deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse EOF步骤 3:更新安装包$ sudo apt-get update步骤 4:安装调试符号包$ sudo apt-get install linux-image-$(uname -r)-dbgsym步骤 5: 验证符号包已经成功安装包含调试信息的文件被称为 vmlinux-XXX-debug,其中 XXX 是内核版本。安装完成后该文件存储在 /usr/lib/debug/boot 下。ubuntu21-10:/usr/lib/debug/boot$ ls -hl total 773M -rw-r--r-- 1 root root 773M Oct 15 21:53 vmlinux-5.13.0-20-generic如果我们想查看 __x64_sys_mount 的汇编指令,则可以使用 gdb vmlinux-5.13.0-20-generic 进入到 gdb 调试工作区,使用 list/disassemble 等命令进行查看。4. 源码安装及关联为了将调试符号与源码关联查看,我们还需要安装源码,然后与安装的 dbgsym 进行关联。$ sudo apt-cache search linux-source linux-source - Linux kernel source with Ubuntu patches linux-source-5.13.0 - Linux kernel source for version 5.13.0 with Ubuntu patches $ sudo apt install linux-source-5.13.0 $ sudo cd /usr/src $ sudo tar -jxvf linux-source-5.13.0.tar.bz2 $ sudo cd /usr/src/linux-source-5.13.05. 最终调测效果# 需要 gdb 首先获取到 vmlinux-5.13.0-20-generic 的编译目录,使用 list *__x64_sys_mount # 会提示对应的编译目录,如果我们在 /usr/src 目录已经安装了源码,建立快捷方式即可 $ mkdir -p /build/linux-lpF6wX/ $ ln -s /usr/src/linux-source-5.13.0 /build/linux-lpF6wX/linux-5.13.0 $ gdb /usr/lib/debug/boot/vmlinux-5.13.0-20-generic (gdb) list *__x64_sys_mount 0xffffffff81352ce0 is in __x64_sys_mount (/build/linux-lpF6wX/linux-5.13.0/fs/namespace.c:3451). warning: Source file is more recent than executable. 3446 /* ... and return the root of (sub)tree on it */ 3447 return path.dentry; 3448 } 3449 EXPORT_SYMBOL(mount_subtree); 3451 SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, 3452 char __user *, type, unsigned long, flags, void __user *, data) 3453 { 3454 int ret; 3455 char *kernel_type; (gdb) disassemble *__x64_sys_mount 0xffffffff81352de3 <+259>: call 0xffffffff813524c0 <path_mount> 0xffffffff81352de8 <+264>: lea -0x40(%rbp),%rdi 0xffffffff81352dec <+268>: movslq %eax,%r12 0xffffffff81352def <+271>: call 0xffffffff813321b0 <path_put> ...通过在 gdb 工作窗口中 list *__x64_sys_mount 我们就可以看到源码相关的定义,一切准备完成,可以愉快地进行相关工作调试了。参考Installing Ubuntu Kernel Debugging SymbolsLinux crash 调试环境搭建

问题排查利器:Linux 原生跟踪工具 Ftrace 必知必会

本文地址:https://www.ebpf.top/post/ftrace_toolsTLDR,建议收藏,需要时查阅。如果你只是需要快速使用工具来进行问题排查,包括但不限于函数调用栈跟踪、函数调用子函数流程、函数返回结果,那么推荐你直接使用 BCC trace 或 Brendan Gregg 封装的 perf-tools 工具即可,本文尝试从手工操作 Ftrace 跟踪工具的方式展示在底层是如何通过 tracefs 实现这些能力的。如果你对某个跟踪主题感兴趣,建议直接跳转到相关的主题查看。快速说明:kprobe 为内核中提供的动态跟踪机制,/proc/kallsym 中的函数几乎都可以用于跟踪,但是内核函数可能随着版本演进而发生变化,为非稳定的跟踪机制,数量比较多。uprobe 为用户空间提供的动态机制;tracepoint 是内核提供的静态跟踪点,为稳定的跟踪点,需要研发人员代码编写,数量有限;usdt 为用户空间提供的静态跟踪点 【本次暂不涉及】Ftrace 是 Linux 官方提供的跟踪工具,在 Linux 2.6.27 版本中引入。Ftrace 可在不引入任何前端工具的情况下使用,让其可以适合在任何系统环境中使用。Ftrace 可用来快速排查以下相关问题:特定内核函数调用的频次 (function)内核函数在被调用的过程中流程(调用栈) (function + stack)内核函数调用的子函数流程(子调用栈)(function graph)由于抢占导致的高延时路径等Ftrace 跟踪工具由性能分析器(profiler)和跟踪器(tracer)两部分组成:性能分析器,用来提供统计和直方图数据(需要 CONFIG_ FUNCTION_PROFILER=y)函数性能分析直方图跟踪器,提供跟踪事件的详情:函数跟踪(function)跟踪点(tracepoint)kprobeuprobe函数调用关系(function_graph)hwlat 等除了操作原始的文件接口外,也有一些基于 Ftrace 的前端工具,比如 perf-tools 和 trace-cmd (界面 KernelShark)等。整体跟踪及前端工具架构图如下:图片来自于 《Systems Performance Enterprise and the Cloud 2nd Edition》 14.1 P706Ftrace 的使用的接口为 tracefs 文件系统,需要保证该文件系统进行加载:$ sysctl -q kernel.ftrace_enabled=1 $ mount -t tracefs tracefs /sys/kernel/tracing $ mount -t debugfs,tracefs tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime) debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime) tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime) $ ls -F /sys/kernel/debug/tracing # 完整目录如下图tracing 目录下核心文件介绍如下表格,当前可仅关注黑体加粗的项,其他项可在需要的时候再进行回顾:文件描述available_tracers可用跟踪器,hwlat blk function_graph wakeup_dl wakeup_rt wakeup function nop,nop 表示不使用跟踪器current_tracer当前使用的跟踪器function_profile_enabled启用函数性能分析器available_filter_functions可跟踪的完整函数列表set_ftrace_filter选择跟踪函数的列表,支持批量设置,例如 *tcp、tcp* 和 *tcp* 等set_ftrace_notrace设置不跟踪的函数列表set_event_pid设置跟踪的 PID,表示仅跟踪 PID 程序的函数或者其他跟踪tracing_on是否启用跟踪,1 启用跟踪 0 关闭跟踪trace_options设置跟踪的选项trace_stat(目录)函数性能分析的输出目录kprobe_events启用 kprobe 的配置uprobe_events启用 uprobe 的配置events ( 目录 )事件(Event)跟踪器的控制文件: tracepoint、kprobe、uprobetrace跟踪的输出 (Ring Buffer)trace_pipe跟踪的输出;提供持续不断的数据流,适用于程序进行读取perf_tools 包含了一个复位所有 ftrace 选型的工具脚本,在跟踪不符合预期的情况下,建议先使用 reset-ftrace 进行复位,然后再进行测试。1. 内核函数调用跟踪基于 Ftrace 的内核函数调用跟踪整体架构如下所示:图片来自于 《Systems Performance Enterprise and the Cloud 2nd Edition》 14.4 P713这里我们尝试对于内核中的系统调用函数 __arm64_sys_openat 进行跟踪(前面两个下划线),需要注意的是 __arm64_sys_openat 是在 arm64 结构体系下 sys_openat 系统调用的包装,如果在 x86_64 架构下则为 __x64_sys_openat() ,由于我们本地的电脑是 M1 芯片,所以演示的样例以 arm64 为主。在不同的体系结构下,可以在 /proc/kallsym 文件中搜索确认。后续的目录,如无特殊说明,都默认位于 /sys/kernel/debug/tracing/ 根目录。# 使用 function 跟踪器,并将其设置到 current_tracer $ sudo echo function > current_tracer # 将跟踪函数 __arm64_sys_openat 设置到 set_ftrace_filter 文件中 $ sudo echo __arm64_sys_openat > set_ftrace_filter # 开启全局的跟踪使能 $ sudo echo 1 > tracing_on # 运行 ls 命令触发 sys_openat 系统调用,新的内核版本中直接调用 sys_openat $ ls -hl $ sudo echo 0 > tracing_on $ sudo echo nop > current_tracer # 需要主要这里的 echo 后面有一个空格,即 “echo+ 空格>" $ sudo echo > set_ftrace_filter # 通过 cat trace 文件进行查看 $ sudo cat trace # tracer: function # entries-in-buffer/entries-written: 224/224 #P:4 # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | sudo-15099 [002] .... 29469.444400: __arm64_sys_openat <-invoke_syscall sudo-15099 [002] .... 29469.444594: __arm64_sys_openat <-invoke_syscall我们可以看到上述的结果表明了函数调用的任务名称、PID、CPU、标记位、时间戳及函数名字。在 perf_tools 工具集中的前端封装工具为 functrace ,需要注意的是该工具默认不会设置 tracing_on 为 1, 需要在启动前进行设置,即 ”echo 1 > tracing_on“。perf_tools 工具集中 kprobe 也可以实现类似的效果,底层基于 kprobe 机制实现,ftrace 机制中的 kprobe 在后续章节会详细介绍。2. 函数被调用流程(栈)在第 1 部分我们获得了内核函数的调用,但是有些场景我们更可能希望获取调用该内核函数的流程(即该函数是在何处被调用),这需要通过设置 options/func_stack_trace 选项实现。$ sudo echo function > current_tracer $ sudo echo __arm64_sys_openat > set_ftrace_filter $ sudo echo 1 > options/func_stack_trace # 设置调用栈选项 $ sudo echo 1 > tracing_on $ ls -hl $ sudo echo 0 > tracing_on $ sudo cat trace # tracer: function # entries-in-buffer/entries-written: 292/448 #P:4 # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | sudo-15134 [000] .... 29626.670430: __arm64_sys_openat <-invoke_syscall sudo-15134 [000] .... 29626.670431: <stack trace> => __arm64_sys_openat => invoke_syscall => el0_svc_common.constprop.0 => do_el0_svc => el0_svc => el0_sync_handler => el0_sync $ sudo echo nop > current_tracer $ sudo echo > set_ftrace_filter $ sudo echo 0 > options/func_stack_trace通过上述跟踪记录,我们可以发现记录同时展示了函数调用的记录和被调用的函数流程,__arm64_sys_openat 的被调用栈如下:=> __arm64_sys_openat => invoke_syscall => el0_svc_common.constprop.0 => do_el0_svc => el0_svc => el0_sync_handler => el0_syncperf_tools 工具集中 kprobe 通过添加 ”-s“ 参数实现同样的功能,运行的命令如下:$ ./kprobe -s 'p:__arm64_sys_openat'3. 函数调用子流程跟踪(栈)如果想要分析内核函数调用的子流程(即本函数调用了哪些子函数,处理的流程如何),这时需要用到 function_graph 跟踪器,从字面意思就可看出这是函数调用关系跟踪。基于 __arm64_sys_openat 子流程调用关系的跟踪的完整设置过程如下:# 将当前 current_tracer 设置为 function_graph $ sudo echo function_graph > current_tracer $ sudo echo __arm64_sys_openat > set_graph_function # 设置跟踪子函数的最大层级数 $ sudo echo 3 > max_graph_depth # 设置最大层级 $ sudo echo 1 > tracing_on $ ls -hl $ sudo echo 0 > tracing_on #$ echo nop > set_graph_function $ sudo cat trace # tracer: function_graph # CPU DURATION FUNCTION CALLS # | | | | | | | 1) | __arm64_sys_openat() { 1) | do_sys_openat2() { 1) 0.875 us | getname(); 1) 0.125 us | get_unused_fd_flags(); 1) 2.375 us | do_filp_open(); 1) 0.084 us | put_unused_fd(); 1) 0.125 us | putname(); 1) 4.083 us | } 1) 4.250 us | }在本样例中 __arm64_sys_openat 函数的调用子流程仅包括 do_sys_openat2() 子函数,而 do_sys_openat2() 函数又调用了 getname()/get_unused_fd_flags() 等子函数。这种完整的子函数调用关系,对于我们学习内核源码和分析线上的问题都提供了便利,排查问题时则可以顺藤摸瓜逐步缩小需要分析的范围。在 perf_tools 工具集的前端工具为 funcgraph ,使用 funcgraph 启动命令如下所示:$./funcgraph -m 3 __arm64_sys_openat如果函数调用栈比较多,直接查看跟踪记录则非常不方便,基于此社区补丁 [PATCH] ftrace: Add vim script to enable folding for function_graph traces 提供了一个基于 vim 的配置,可通过树状关系来折叠和展开函数调用的最终记录,vim 设置完整如下:" Enable folding for ftrace function_graph traces. " To use, :source this file while viewing a function_graph trace, or use vim's " -S option to load from the command-line together with a trace. You can then " use the usual vim fold commands, such as "za", to open and close nested " functions. While closed, a fold will show the total time taken for a call, " as would normally appear on the line with the closing brace. Folded " functions will not include finish_task_switch(), so folding should remain " relatively sane even through a context switch. " Note that this will almost certainly only work well with a " single-CPU trace (e.g. trace-cmd report --cpu 1). function! FunctionGraphFoldExpr(lnum) let line = getline(a:lnum) if line[-1:] == '{' if line =~ 'finish_task_switch() {$' return '>1' endif return 'a1' elseif line[-1:] == '}' return 's1' return '=' endif endfunction function! FunctionGraphFoldText() let s = split(getline(v:foldstart), '|', 1) if getline(v:foldend+1) =~ 'finish_task_switch() {$' let s[2] = ' task switch ' let e = split(getline(v:foldend), '|', 1) let s[2] = e[2] endif return join(s, '|') endfunction setlocal foldexpr=FunctionGraphFoldExpr(v:lnum) setlocal foldtext=FunctionGraphFoldText() setlocal foldcolumn=12 setlocal foldmethod=expr将上述指令保存为 function-graph-fold.vim 文件,在 vim 使用时通过 -S 参数指定上述配置,就可实现按照层级展示跟踪记录。在 vim 中,可通过 za 展开,zc 折叠跟踪记录。(通过文件分析,我们需要在 cat trace 文件时候重定向到文件)。$ vim -S function-graph-fold.vim trace.log4. 内核跟踪点(tracepoint)跟踪可基于 ftrace 跟踪内核静态跟踪点,可跟踪的完整列表可通过 available_events 查看。events 目录下查看到各分类的子目录,详见下图:# available_events 文件中包括全部可用于跟踪的静态跟踪点 $ sudo grep openat available_events syscalls:sys_exit_openat2 syscalls:sys_enter_openat2 syscalls:sys_exit_openat syscalls:sys_enter_openat # 我们可以在 events/syscalls/sys_enter_openat 中查看该跟踪点相关的选项 $ sudo ls -hl events/syscalls/sys_enter_openat total 0 -rw-r----- 1 root root 0 Jan 1 1970 enable # 是否启用跟踪 1 启用 -rw-r----- 1 root root 0 Jan 1 1970 filter # 跟踪过滤 -r--r----- 1 root root 0 Jan 1 1970 format # 跟踪点格式 -r--r----- 1 root root 0 Jan 1 1970 hist -r--r----- 1 root root 0 Jan 1 1970 id --w------- 1 root root 0 Jan 1 1970 inject -rw-r----- 1 root root 0 Jan 1 1970 trigger $ sudo cat events/syscalls/sys_enter_openat/format name: sys_enter_openat ID: 555 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:int __syscall_nr; offset:8; size:4; signed:1; field:int dfd; offset:16; size:8; signed:0; field:const char * filename; offset:24; size:8; signed:0; field:int flags; offset:32; size:8; signed:0; field:umode_t mode; offset:40; size:8; signed:0; print fmt: "dfd: 0x%08lx, filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))这里直接使用 tracepoint 跟踪 sys_openat 系统调用,设置如下:$ sudo echo 1 > events/syscalls/sys_enter_openat/enable $ sudo echo 1 > tracing_on $ sudo cat trace # tracer: nop # entries-in-buffer/entries-written: 19/19 #P:4 # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | cat-16961 [003] .... 47683.934082: sys_openat(dfd: ffffffffffffff9c, filename: ffff9abf20f0, flags: 80000, mode: 0) cat-16961 [003] .... 47683.934326: sys_openat(dfd: ffffffffffffff9c, filename: ffff9ac09f20, flags: 80000, mode: 0) cat-16961 [003] .... 47683.935468: sys_openat(dfd: ffffffffffffff9c, filename: ffff9ab75150, flags: 80000, mode: 0) $ sudo echo 0 > events/syscalls/sys_enter_openat/enable我们通过设置 sys_enter_openat/enable 开启对于 sys_enter_openat 的跟踪,trace 文件中的跟踪记录格式与 sys_enter_openat/format 中的 print 章节的格式一致。print fmt: "dfd: 0x%08lx, filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx" ...Filter 跟踪记录条件过滤关于 sys_enter_openat/filter 文件为跟踪记录的过滤条件设置,格式如下:field operator value其中:field 为 sys_enter_openat/format 中的字段。operator 为比较符整数支持:==,!=,</、,<=,>= 和 & ,字符串支持 ==,!=,~ 等,其中 ~ 支持 shell 脚本中通配符 *,?,[] 等操作。不同的条件也支持 && 和 || 进行组合。如需要通过 format 格式中的 mode 字段过滤:field:umode_t mode; offset:40; size:8; signed:0;只需要将进行如下设置即可:$ sudo echo 'mode != 0' > events/syscalls/sys_enter_openat/filter如果需要清除 filter,直接设置为 0 即可:$ sudo echo 0 > events/syscalls/sys_enter_openat/filter5. kprobe 跟踪kprobe 为内核提供的动态跟踪机制。与第 1 节介绍的函数跟踪类似,但是 kprobe 机制允许我们跟踪函数任意位置,还可用于获取函数参数与结果返回值。使用 kprobe 机制跟踪函数须是 available_filter_functions 列表中的子集。kprobe 设置文件和相关文件如下所示,其中部分文件为设置 kprobe 跟踪函数后,Ftrace 自动创建:kprobe_events设置 kprobe 跟踪的事件属性;完整的设置格式如下,其中 GRP 用户可以直接定义,如果不设定默认为 kprobes:p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] # 设置 probe 探测点 r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] # 函数地址的返回跟踪 -:[GRP/]EVENT # 删除跟踪kprobes/<GRP>/<EVENT>/enabled设置后动态生成,用于控制是否启用该内核函数的跟踪;kprobes/<GRP>/<EVENT>/filter设置后动态生成,kprobe 函数跟踪过滤器,与上述的跟踪点 fliter 类似;kprobes/<GRP>/<EVENT>/format设置后动态生成,kprobe 事件显示格式;kprobe_profilekprobe 事件统计性能数据;Kprobe 跟踪过程可以指定函数参数的显示格式,这里我们先给出 sys_openat 函数原型:SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags, umode_t, mode);**跟踪函数入口参数 **这里仍然以 __arm64_sys_openat 函数为例,演示使用 kpboe 机制进行跟踪:# p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] # GRP=my_grp EVENT=arm64_sys_openat # SYM=__arm64_sys_openat # FETCHARGS = dfd=$arg1 flags=$arg3 mode=$arg4 $ sudo echo 'p:my_grp/arm64_sys_openat __arm64_sys_openat dfd=$arg1 flags=$arg3 mode=$arg4' >> kprobe_events $ sudo cat events/my_grp/arm64_sys_openat/format name: __arm64_sys_openat ID: 1475 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:unsigned long __probe_ip; offset:8; size:8; signed:0; print fmt: "(%lx)", REC->__probe_ip events/my_grp/arm64_sys_openat/format $ sudo echo 1 > events/my_grp/arm64_sys_openat/enable # $ sudo echo 1 > options/stacktrace # 启用栈 $ cat trace # tracer: nop # entries-in-buffer/entries-written: 38/38 #P:4 # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | cat-17025 [002] d... 52539.651096: arm64_sys_openat: (__arm64_sys_openat+0x0/0xb4) dfd=0xffff8000141cbeb0 flags=0x1bf mode=0xffff800011141778 # 关闭,注意需要先 echo 0 > enable 停止跟踪 # 然后再使用 "-:my_grp/arm64_sys_openat" 停止,否则会正在使用或者忙的错误 $ sudo echo 0 > events/my_grp/arm64_sys_openat/enable $ sudo echo '-:my_grp/arm64_sys_openat' >> kprobe_events**跟踪函数返回值 **kprobe 可用于跟踪函数返回值,格式如下:r[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]例如:$ sudo echo 'r:my_grp/arm64_sys_openat __arm64_sys_openat ret=$retval' >> kprobe_events变量 $retval 参数表示函数返回值,其他的使用格式与 kprobe 类似。6. uprobe 跟踪uprobe 为用户空间的动态跟踪机制,格式和使用方式与 kprobe 的方式类似,但是由于是用户态程序跟踪需要指定跟踪的二进制文件和偏移量。p[:[GRP/]EVENT]] PATH:OFFSET [FETCHARGS] # 跟踪函数入口 r[:[GRP/]EVENT]] PATH:OFFSET [FETCHARGS] # 跟踪函数返回值 -:[GRP/]EVENT] # 删除跟踪点这里以跟踪 /bin/bash 二进制文件中的 readline() 函数为例:$ readelf -s /bin/bash | grep -w readline 920: 00000000000d6070 208 FUNC GLOBAL DEFAULT 13 readline $ echo 'p:my_grp/readline /bin/bash:0xd6070' >> uprobe_events $ echo 1 > events/my_grp/readline/enable $ cat trace # tracer: nop # entries-in-buffer/entries-written: 1/1 #P:4 # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | bash-14951 [003] .... 54570.055093: readline: (0xaaaab3ce6070) $ echo 0 > events/my_grp/readline/enable $ echo '-:my_grp/readline' >> uprobe_eventsuprobe 跟踪是跟踪用户态的函数,因此需要指定二进制文件+符号偏移量才能进行跟踪。不同系统中的二进制版本或者编译方式不同,会导致函数符号表的位置不同,因此需要跟踪前进行确认。7. 总结至此,我们完整介绍 Ftrace 的整体应用场景,也通过具体的设置,学习了使用的完整流程。实际问题排查中,考虑到效率和易用性,推荐大家这样选择:如果排查问题机器上支持 eBPF技术,首选 BCC trace 及相关工具;否则推荐使用 perf-tools ;最后的招数就是使用本文 Ftrace 的完整流程了。但目前基于 eBPF 的工具还未支持 function_graph 跟踪器,特定场景下还需要 ftrace 的 function_graph 跟踪器的配合。Ftrace 与 eBPF 并非是相互替代,而是相互补充协同关系,在后续的问题排查案例中我们将看到这一点。参考高效分析 Linux 内核源码 , 相关代码参见这里 。Linux kprobe 调试技术使用ftrace 在实际问题中的应用《Systems Performance Enterprise and the Cloud 2nd Edition》

BumbleBee: 如丝般顺滑构建、交付和运行 eBPF 程序

本文地址:https://www.ebpf.top/post/bumblebee1. 前言不久前,Solo.io 公司在官网博客宣布了开源了一个名称为 BumbleBee 的新项目。该项目专注于简化构建 eBPF 工具的门槛和优化使用体验,通过将 eBPF 程序打包成 OCI 镜像,带来了与使用 Docker 一致的体验的构建、分发和运行 eBPF 程序。BumbleBee 目的是让我们专注于编写 eBPF 代码,其负责自动生成与 eBPF 程序相关的用户空间的代码功能,包括加载 eBPF 程序和将 eBPF 程序的数据作为日志、指标和直方图进行展示。那么我们为什么需要 BumbleBee 项目来管理 eBPF 程序呢?这需要从 eBPF 技术的特点讲起。2. 构建和分发 eBPF 工具的挑战eBPF 技术被称之为近 50 年来操作系统最大的变革,解决了 Linux 内核在上游开发、合并和发行功能缓慢的窘境。 eBPF 技术为内核提供了不通过上游实现内核的定制功能的能力,当前已经在可观测、网络和安全等多个领域得到了广泛的应用,尤其是在云原生的技术潮流中,eBPF 技术发挥的能力也越来越重要,诸如当前风头正盛的 Cilium 项目。但是开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,社区先后推出了 BCC、BPFTrace 等前端绑定工作,大大降低了编写和使用 eBPF 技术的门槛,但是这些工具的源码交付方式,需要运行 eBPF 程序的 Linux 系统上安装配套的编译环境,对于分发带来了不少的麻烦,同时将内核适配的问题在运行时才能验证,也不利于提前发现和解决问题。近年来,为解决不同内核版本中 eBPF 程序的分发和运行问题,社区基于 BTF 技术推出了 CO-RE 功能(“一次编译,到处运行”),一定程度上实现了通过 eBPF 二进制字节码分发,同时也解决了运行在不同内核上的移植问题,但是对于如何打包和分发 eBPF 二进制代码还未有统一简洁的方式。除了 eBPF 程序,目前我们还需要编写用于加载 eBPF 程序和用于读取 eBPF 程序产生数据的各种代码,这往往涉及到通过源码复制粘贴来解决部分问题。此外,libbpf-bootstrap 通过 bpftool 工具生成相关的脚手架代码,一定程度上解决了通用代码重复编写的问题,但是对于构建、分发和运行 eBPF 程序上提供的帮助有限。3. BumbleBee 简介BumbleBee 项目正是 Solo 公司在企业服务网格 Gloo-Mesh 项目中为方便应用 eBPF 技术而诞生中,其用于解决在构建、分发和运行 eBPF 程序遇到的重复性挑战,当前该项目还在早期(当前版本 0.0.9),提供的功能场景(Network 和 FileSystem)有限,但是基于特定模板能力来构建 OCI 镜像的思路,为我们在管理 eBPF 程序方面提供了一种高效简洁的实现,值得我们关注。使用 BumbleBee 工具的前置依赖: 运行的 eBPF 的操作系统开启了 BTF 支持,编写的 eBPF 代码也需要使用 CO-RE 相关函数,关于 CO-RE 相关的技术可以参考这里。BumbleBee 提供了与 Docker 一致的体验感觉。下图是 Docker 的高层次示意图,BumbleBee 工具完全参考了这个流程。3.1 构建BumbleBee 打造 "恰到好处" 的 eBPF 工具链,将 eBPF 程序的构建过程自动化,让你专注于代码本身。 BumbleBee 的 eBPF 代码打包成一个 OCI 标准镜像,这样就可以在基础设施中进行分发。下述命令可实现将 eBPF 程序 probe.c 的直接编译和打包成镜像 my_probe:v1 。$ bee build probe.c username/my_probe:v13.2 发布利用 BTF 和 OCI 打包能力,BumbleBee 编写的 eBPF 代码是可移植的,并且可以嵌入到现有的发布工作流程中。 通过将 eBPF 代码构建的镜像,推送到任何符合 OCI 标准的镜像仓库,就可以实现发布给其他用户使用。下述命令实现了将镜像发布至镜像仓库的功能,使用时可直接使用 bee run 基于镜像运行。# 推送 $ bee push username/my_probe:v1 $ bee pull username/my_probe:v13.3 运行使用 BumbleBee 提供的 CLI 界面和保存在镜像仓库中的镜像,我们可快速在其他地方运行。 BumbleBee 不但构建了用户空间代码,而且可以利用 eBPF map,来展示日志、指标和柱状图信息。 BumbleBee 使用了 BTF 格式自审能力,获知到需要显示哪些数据类型。$ bee run my_probe:v1下面让我们通过一个完整的样例,来体验 BumbleBee 带给我们管理 eBPF 程序的便利。4. 完整体验4.1 bee 安装首先我们需要一个运行支持 BTF 内核的 Linux 操作系统,这里推荐直接使用 ubuntu 2110 版本,搭载的内核已经默认支持了 BTF。如果你选择使用 Vagrant 来管理虚拟机,BumbleBee 仓库中提供的 Vagrantfile 文件可以直接使用。或者你可以使用 mulipass 工具直接启动一个 ubuntu 2110 版本的系统。这里使用仓库提供的脚本安装,当然也可以直接通过 git clone 仓库的方式进行。为了快速体验,避免某些场景中的权限问题,这里建议直接使用 root 用户进行安装。ubuntu@ubuntu21-10:~# curl -sL https://run.solo.io/bee/install | BUMBLEBEE_VERSION=v0.0.9 sh Attempting to download bee version v0.0.9 Downloading bee-linux-amd64... Download complete!, validating checksum... Checksum valid. bee was successfully installed 🎉 Add the bumblebee CLI to your path with: export PATH=$HOME/.bumblebee/bin:$PATH Now run: bee init # Initialize simple eBPF program to run with bee Please see visit the bumblebee website for more info: https://github.com/solo-io/bumblebee安装完成后,bee 的主要命令如下:# bee --help Usage: bee [command] Available Commands: build Build a BPF program, and save it to an OCI image representation. completion generate the autocompletion script for the specified shell describe Describe a BPF program via it's OCI ref help Help about any command init Initialize a sample BPF program login Log in so you can push images to the remote server. run Run a BPF program file or OCI image. version Flags: -c, --config stringArray path to auth configs --config-dir string Directory to bumblebee configuration (default "/root/.bumblebee") -h, --help help for bee --insecure allow connections to SSL registry without certs -p, --password string registry password --plain-http use plain http and not https --storage string Directory to store OCI images locally (default "/root/.bumblebee/store") -u, --username string registry username -v, --verbose verbose output Use "bee [command] --help" for more information about a command.4.2 Bee init 生成 eBPF 程序脚手架Bee init 命令可通过问题向导模式生成 eBPF 代码脚手架,功能与 libbpf-bootstrap 有些类似,但是通过向导的方式进行更加容易上手。$ export PATH=$HOME/.bumblebee/bin:$PATH # ebpf-test && cd ebpf-test # bee init Use the arrow keys to navigate: ↓ ↑ → ← ? What language do you wish to use for the filter: # 步骤 选择编写 eBPF 代码的语言 ▸ C # 当前仅支持 C,Rust 可能在未来支持 --------------------------------------------- # 步骤 2 选择 eBPF 程序类型 INFO Selected Language: C Use the arrow keys to navigate: ↓ ↑ → ← ? What type of program to initialize: ▸ Network # 选择编写 eBPF 程序的类型,当前支持 Network 和 File System File system # 生成的模板分别对应于 tcp_connet 和 open 函数 --------------------------------------------- # 步骤 3 选择 map 类型 INFO Selected Language: C INFO Selected Program Type: Network Use the arrow keys to navigate: ↓ ↑ → ← ? What type of map should we initialize: ▸ RingBuffer HashMap --------------------------------------------- # 步骤 4 选择 map 导出类型 INFO Selected Language: C INFO Selected Program Type: Network INFO Selected Map Type: HashMap Use the arrow keys to navigate: ↓ ↑ → ← ? What type of output would you like from your map: ▸ print # map 数据的展现方式,日志打印、计数或者指标导出 counter gauge --------------------------------------------- # 步骤 5 eBPF 程序保存文件名 INFO Selected Language: C INFO Selected Program Type: Network INFO Selected Map Type: HashMap INFO Selected Output Type: print ✔ BPF Program File Location: probe.c ---------------------------------------------- # 最终完成整个代码生成向导 INFO Selected Language: C INFO Selected Program Type: Network INFO Selected Map Type: HashMap INFO Selected Output Type: print INFO Selected Output Type: BPF Program File Location probe.c SUCCESS Successfully wrote skeleton BPF program # ls -hl total 4.0K -rw-rw-r-- 1 ubuntu ubuntu 2.0K Feb 11 11:33 probe.c通过 init 命令生成的 probe.c 文件格式大体如下:#include "vmlinux.h" #include "bpf/bpf_helpers.h" #include "bpf/bpf_core_read.h" #include "bpf/bpf_tracing.h" #include "solo_types.h" // 1. Change the license if necessary char __license[] SEC("license") = "Dual MIT/GPL"; struct event_t { // 2. Add ringbuf struct data here. } __attribute__((packed)); // This is the definition for the global map which both our // bpf program and user space program can access. // More info and map types can be found here: https://www.man7.org/linux/man-pages/man2/bpf.2.html struct { __uint(max_entries, 1 << 24); __uint(type, BPF_MAP_TYPE_RINGBUF); __type(value, struct event_t); } events SEC(".maps.print"); SEC("kprobe/tcp_v4_connect") int BPF_KPROBE(tcp_v4_connect, struct sock *sk) // Init event pointer struct event_t *event; // Reserve a spot in the ringbuffer for our event event = bpf_ringbuf_reserve(&events, sizeof(struct event_t), 0); if (!event) { return 0; // 3. set data for our event, // For example: // event->pid = bpf_get_current_pid_tgid(); bpf_ringbuf_submit(event, 0); return 0; }基于生成的代码模板,我们需要填写自己的逻辑,这里不是重点,先略过相关代码,完整代码可在官方开始文档中查看。4.3 构建 eBPF 程序构建过程需要使用 Docker 或者类型 Docker 的容器引擎,需要提前进行安装。# apt install docker.io # 安装 docker # bee build probe.c my_probe:v1 SUCCESS Successfully compiled "probe.c" and wrote it to "probe.o" SUCCESS Saved BPF OCI image to my_probe:v1整个构建过程中我们不需要再涉及 clang 等相关编译命令,只需要通过 bee build 命令输入 eBPF 程序文件名和期望生成的镜像即可,编译完成后,eBPF 程序的二进制字节码 probe.o 会自动添加到镜像 my_probe:v1 中,我们可以使用 bee tag 完成镜像仓库的重新定义。4.4 发布 eBPF 程序我们可以通过 bee tag 和 push 子命令完成进行镜像仓库的发布工作。# bee tag my_probe:v1 dwh0403/my_probe:v1 # bee login # bee push dwh0403/my_probe:v1看一下上述的几条命令,是不是有些似曾相识的感觉?4.5 运行 eBPF 程序构建镜像后,在本地可直接通过 bee run 来运行,运行后 bee 会自动启动 TUI 界面,来展示我们编写 eBPF 程序中的 map 内容,自动生成的 map 名字有些特殊后缀用于 bee TUI 用户空间的程序来读取对应 map 中数据进行展示,比如生成代码模板中的SEC(".maps.print"),表示该 map 用于打印。# bee run my_probe:v1 SUCCESS Fetching program from registry: my_probe:v1 SUCCESS Loading BPF program and maps into Kernel SUCCESS Linking BPF functions to associated probe/tracepoint INFO Rendering TUI..5. 总结至此,我们完成了整个项目功能的体验,bee init 工具可通过向导模式帮助我们生成 eBPF 代码框架,尽管功能还有些单薄,但是对于我们特定场景的使用不失是一种快速便捷的方式。bee build/push/run 等子命令,将编译的命令、打包镜像、发布镜像和运行镜像的等诸多步骤进行了极大的精简,非常易用,极大地降低了构建、发布和运行 eBPF 程序的重复成本,不得不为作者的思路点赞。由于通过 bee 生成的工具基于特定场景,功能丰富度还有限,对于编写复杂情况下的 eBPF 程序和功能丰富的用户空间程序还不能适用,但是其构建、发布和运行的整体思路(甚至部分基础功能)却是我们可以直接使用或者借鉴的。6. 参考资料TutorialSolo.io 开源 BumbleBee,用类 Docker 的体验使用 eBPFBumbleBee: Build, Ship, Run eBPF toolseCHO episode 33: Bumblebee

揭秘 BPF map 前生今世

揭秘 BPF map 前生今世本文地址:https://www.ebpf.top/post/map_internal1. 前言众所周知,map 可用于内核 BPF 程序和用户应用程序之间实现双向的数据交换, 为 BPF 技术中的重要基础数据结构。在 BPF 程序中可以通过声明 struct bpf_map_def 结构完成创建,这其实带给我们一种错觉,感觉这和普通的 C 语言变量没有区别,然而事实真的是这样的吗? 事情远没有这么简单,读完本文以后相信你会有更大的惊喜。struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_ARRAY, // ... };我们知道最终 BPF 程序是需要在内核中执行,但是 map 数据结构是用于用户空间和内核 BPF 程序双向的数据结构,那么问题来了:通过 struct bpf_map_def 定义的变量究竟是如何创建的,是在用户空间创建还是内核中直接创建的?如何实现创建后的 map 的结构,在用户空间与内核中 BPF 程序关联?你可能注意到在用户空间中对于 map 的访问是通过 map 文件句柄 fd 完成(类型为 int),但是在 BPF 程序中是通过 struct bpf_map * 结构完成的。毕竟数据交换跨越了用户空间和内核空间,本文将从深入浅出为各位看官揭开 map 整个生命管理的 "大瓜"。2. 简单的使用样例本样例来自于 samples/bpf/sockex1_user.c 和 sockex1_kern.c,略有修改和删除。sockex1_user.c 用户空间程序主要内容如下(为方便展示,部分内容有删除和修改):int main(int argc, char **argv) struct bpf_object *obj; int map_fd, prog_fd; // ... // 加载 BPF 程序至 bpf_object 对象中, bpf_prog_load("sockex_kern.o", BPF_PROG_TYPE_SOCKET_FILTER, &obj, &prog_fd)) // 获取 my_map 对应的 map_fd 句柄 map_fd = bpf_object__find_map_fd_by_name(obj, "my_map"); // == 本次关注 == // 通过 setsockopt 将 BPF 字节码加载到内核中 sock = open_raw_sock("lo"); setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)); popen("ping -4 -c5 localhost", "r"); // 产生报文 // 从 my_map 中读取 5 次 IPPROTO_TCP 的统计 for (i = 0; i < 5; i++) { long long tcp_cnt; int key = IPPROTO_TCP; assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); // == 本次关注 == // ... sleep(1); return 0; }sockex1_user.c 文件中的 bpf_map_lookup_elem 调用的函数原型如下,定义在文件 tools/lib/bpf/bpf.c 中:int bpf_map_lookup_elem(int fd, const void *key, void *value)函数底层通过 sys_bpf(cmd=BPF_MAP_LOOKUP_ELEM,...) 实现,为我们方便 map 操作的用户空间封装函数, bpf 系统调用可参考 man 2 bpf。其中 sockex1_kern.c 主要内容如下:// map 定义 struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(u32), .value_size = sizeof(long), .max_entries = 256, // BPF 程序,获取到报文协议类型并进行计数更新 SEC("socket1") int bpf_prog1(struct __sk_buff *skb) int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; value = bpf_map_lookup_elem(&my_map, &index); // 查找索引并更新 map 对应的值,== 本次关注 == if (value) __sync_fetch_and_add(value, skb->len); return 0; char _license[] SEC("license") = "GPL";sockex1_kern.c 文件中的 bpf_map_lookup_elem 函数为内核中提供的 BPF 辅助函数,原型声明如下,详情可参考 man 7 bpf-helper:void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)用户空间与内核 BPF 辅助函数参数对比通过分析 sockex1_user.c 和 sockex1_kern.c 函数中的 bpf_map_lookup_elem 使用姿势,这里我们做个简单对比:// 用户空间 map 查询函数 int bpf_map_lookup_elem(int fd, const void *key, void *value) // 内核中 BPF 辅助函数 map 查询函数 void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)那么如何将 int fd 与 struct bpf_map *map 共同关联一个对象呢? 这需要我们通过分析 BPF 字节码来进行解密。3. 深入指令分析首先我们将 sockex1_kern.c 文件使用 llvm/clang 将之编译成 ELF 的 BPF 字节码。对于生成的 sockex1_kern.o 文件可以用 llvm-objdump 来查看相对应的文件格式,这里我们仅关注 map 相关的部分。3.1 查看 BPF 指令$ clang -O2 -target bpf -c sockex1_kern.c -o sockex1_kern.o $ llvm-objdump -S sockex1_kern.o 0000000000000000 <bpf_prog1>: // ... ; value = bpf_map_lookup_elem(&my_map, &index); # 备注:编译的机器启用了 BTF 7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 9: 85 00 00 00 01 00 00 00 call 1 // ...上述结果展示了 BPF 程序中 socket1 部分的函数 bpf_prog1 的 BPF 指令,但是其中对于涉及到的变量 my_map 的引用都未有解决。上述的反汇编部分打印了 map_lookup_elem() 函数调用涉及的指令:根据 BPF 程序调用的约定,寄存器 r1 为函数调用的第 1 个参数,这里即 bpf_map_lookup_elem(&my_map, &index) 调用中的 my_map 。7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接数赋值 , r1 = 0 9: 85 00 00 00 01 00 00 00 call 1 # 调用 bpf_map_lookup_elem,编号为 1上述 "7:" 行 表了为一条 16 个字节的 BPF 指令,表示加载一个 64 位立即数。这里无需担心相关的 BPF 指令集,后续我们会详细展开解释。1 个 BPF 指令有 8 个字节组成,格式定义如下:struct bpf_insn { __u8 code; /* opcode */ __u8 dst_reg:4; /* dest register */ __u8 src_reg:4; /* source register */ __s16 off; /* signed offset */ __s32 imm; /* signed immediate constant */ };通过上述结构对应拆解一下 ”7:“ 行(其中包含了 2 条 BPF 指令,为 BPF 指令中的特殊指令,运行时会被解析成 1 条指令执行) ,第 1 条 BPF 指令详细的信息如下:(这里忽略了 off 字段)opcode 为 0x18,即 BPF_LD | BPF_IMM | BPF_DW。该 opcode 表示要将一个 64 位的立即数加载到目标寄存器。dst_reg 是 1(4 个 bit 位),代表寄存器 r1。src_reg 是 0(4 个 bit 位),表示立即数在指令内。imm 为 0,因为 my_map 的值在生成 BPF 字节码的时候还未进行创建。第 2 条指令主要负责保存 imm 的高 32 位。3.2 加载器创建 map 对象当加载器(loader)在加载 ELF 对象 sockex1_kern.o 时,其首先会从 ELF 格式的 maps 区域获取到定义的 map 对象 my_map 及相关的属性, 然后通过调用 bpf() 系统调用来创建 my_map 对象,如果创建成功,那么 bpf() 系统调用返回一个文件描述符 (map fd)。同时,加载器也会对于基于 map 元信息(比如名称 my_map)与通过 bpf() 系统调用创建 map 后返回的 map fd 建立起对应关系,此后用户空间空间程序就可以使用 my_map 作为关键字获取到其对应的 fd,具体代码如下:map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");用户空间获取到了 map 对象的 fd,后续可用于 map_lookup_elem(map_fd, ...) 函数进行 map 的查询等操作。3.3 第一次变身: map fd 替换以上完成了 my_map 对象的创建,但是在 BPF 字节码程序加载到内核前,还需要将 map fd 在 BPF 指令集中完成第一次变身,如函数 lib/bpf.c: bpf_apply_relo_map() 的代码片段所示:prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD; // 值在内核中定义为 1 prog->insns[insn_off].imm = ctx->map_fds[map_idx]; // ctx->map_fds[map_idx] 即为保存的 map fd 值。这里假设获取到的 map 文件描述符为 6,那么在加载的 BPF 程序完成 bpf_apply_relo_map 的替换后上述的指令对比如下:ELF 文件中的字节码:7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接数赋值 , r1 = 0 9: 85 00 00 00 01 00 00 00 call 1 # 调用 bpf_map_lookup_elem,编号为 1替换 map fd 后的字节码:7: 18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接数赋值 , r1 = 6 9: 85 00 00 00 01 00 00 00 call 1 # 调用 bpf_map_lookup_elem,编号为 13.4 第二次变身: map fd 替换成 map 结构指针当上述经过第一次变身的 BPF 字节码加载到内核后,还需要进行一次变身,才能真正在内核中工作,这次 BPF 验证器(verifier)扛过大旗。验证器将加载器注入到指令中的 map fd 替换成内核中的 map 对象指针。调用堆栈的情况如下:sys_bpf() --> bpf_prog_load() --> bpf_check() --> replace_map_fd_with_map_ptr() --> do_check() --> check_ld_imm() ==> check_func_arg() --> convert_pseudo_ld_imm64()函数 replace_map_fd_with_map_ptr() 通过以下代码完成第二次大变身,实现了内核中 BPF 字节码的 imm 摇身一变成为 map ptr 地址。f = fdget(insn[0].imm); // 从第 1 条指令中的 imm 字段获取到加载器设置的 map fd map = __bpf_map_get(f); // 基于 map fd 获取到 map 对象指针 addr = (unsigned long)map; insn[0].imm = (u32)addr; // 将 map 对象指针低 32 位放入第一条指令中的 imm 字段 insn[1].imm = addr >> 32; // 将 map 对象指针高 32 位放入第二条指令中的 imm 字段于此同时,函数 convert_pseudo_ld_imm64() 还需要清理加载器设置的 src_reg = BPF_PSEUDO_MAP_FD 操作( prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD;), 用于表明完成了整个指令的重写工作:if (insn->code == (BPF_LD | BPF_IMM | BPF_DW)) insn->src_reg = 0;如果这里的 my_map 在内核中 64 位地址为 0xffff8881384aa200,那么验证器完成第二次变身后的 BPF 字节码对比如下。替换 map fd 后的字节码:7: 18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接数赋值 , r1 = 6 9: 85 00 00 00 01 00 00 00 call 1 # 调用 bpf_map_lookup_elem,编号为 1替换为 map 对象指针后的字节码如下:7: 18 01 00 00 00 a2 4a 38 00 00 00 00 81 88 ff ff # 64 位直接数赋值 , r1 = 0xffff8881384aa200 9: 85 00 00 00 30 86 01 00 # 调用 bpf_map_lookup_elem,编号为 1在完成了上述两次变身后,当在内核中调用 map_lookup_elem() 时,第一个参数 my_map 的值为 0xffff8881384aa200,从而实现了从最早的 ELF 中的 0 ,替换成了 map_fd (6),直到最后的 map 对象 struct bpf_map * (0xffff8881384aa200)。提示,内核中 bpf_map_lookup_elem 辅助函数的原型定义为:static void *(*bpf_map_lookup_elem)(struct bpf_map *map, void *key)4. 整个流程总结通过上述 map 访问指令的 2 次大变身,我们可以清晰了解 map 创建、map fd 指令重写和 map ptr 对象的重写,也能够彻底明白用户空间 map fd 与内核中 map 对象指针的关联关系。俗话说一图胜千言,这里我们用一张图进行整个流程的总结:原始图片来自于这里 ,略有修改。参考Linux bpf map internalseCHO episode 11: Exploring bpftool with Quentin Monnetebpf: BPF_FUNC_map_lookup_elem calling convention边缘网络 eBPF 超能力:eBPF map 原理与性能解析

百页 PPT BPF 技术全览 - 深入浅出 BPF 技术

eBPF 从创建开始,短短数年(7年),至今就已经被认为是过去 50 年来操作系统最大的变更,那么 eBPF 技术到底给我们带来了什么样的超能力,以至于得到如此高的评价? 本文从以下内容入手,对 eBPF 技术进行了全面的概述:eBPF 是什么?eBPF 的应用场景有哪些?eBPF 是怎么工作的?eBPF 软件开发的生态eBPF 未来发展趋势从 cBPF 的诞生、到 ebPF 的崛起,再到 eBPF 在可观测性/跟踪、网络和安全等各个领域中的应用,其中详细介绍了 eBPF 技术在国内外巨头互联网公司的应用场景,eBPF 人们的开源项目 Katran/Cilium/BCC/BPFTrace/Kubectl-Trace/Tracee/Falco/eBPF Exporter/Pixe 等,可快速熟悉 eBPF 的整体生态。接着,在 eBPF 开发场景中从 BPFTrace/Python/C/Go 等各种语言或者工具入手,介绍了开发 eBPF 的差异点。最后简单介绍了 eBPF 未来在网络、安全、观测等维度的后续发展方向。全文共 100 多页,详细兼顾了 eBPF 的各个维度,可以说目前最全面的一篇介绍文章。本文地址:https://www.ebpf.top/post/head_first_bpf完整 PDF 版本可以关注公众号,回复 “pdf” 下载。

基于 Ubuntu 21.04 BPF 开发环境全攻略

本文地址:https://www.ebpf.top/post/ubuntu_2104_bpf_env1. 系统安装1.1 VagrantVagrant 是一款用于构建及配置虚拟开发环境的软件,基于 Ruby,主要以命令行的方式运行。Vagrant 由 HashiCorp 官方出品,相信提到大名鼎鼎的 HashiCorp 公司,大家还能够联想到著名的安全密码存储 Valut、服务注册发现 Consul、大规模调度系统 Normad 等等。Vagrant 可基于官方提供的各种虚拟机打包文件(比如 VirtualBox 的 Box 文件)快速搭建各种系统的验证环境,也可以灵活通过配置式的文件管理多 VM 环境,同时具有平台适配性,是非常好的 VM 管理工具。但其自身并不提供虚拟化环境,需要配合 VirtualBox、WMware、KVM 等虚拟化技术,1.6 版本底层也支持使用容器,可以将容器替代完整的虚拟系统。Vagrant 的架构使用 "Provisioners" 和 "Providers" 作为开发环境的构建模块。|--vagrant |--Providers 如:VirtualBox、Hyper-V、Docker、VMware、AWS |--Boxex 如:Centos7。与镜像类似 |--Provisioners 如:'yum intall -y python' 等自定义自动化脚本Vagrant 作为最外层的虚拟软件,目的是帮助开发者更容易地与 Providers 互动。Vagrantfile 记录 Providers 和 Provisioners 的相关信息。Providers 作为服务,帮助 vagrant 使用 Boxes 建立和创建虚拟环境。Vagrant 提供的内嵌的 Provider 有 VirtualBox、Hyper-V、Docker、VMware 等。在 Mac 系统中如果使用 Brew 管理包,则可以通过 brew 命令直接安装。其他系统的安装方式可参考 下载 Vagrant,查看详情。$ brew install vagrant在安装 vagrant 成功后,我们还需要安装底层的虚拟化软件(即上述架构中的 Providers)才能正常工作,这里我们使用 VirtualBox。1.2 VirtualBoxOracle VirtualBox 是德国公司 InnoTek 出品的虚拟机软件,现在由 Oracle 公司管理和发行,适用于多平台系统。用户可以在 VirtualBox 上安装并且运行 Solaris/Windows/Linux 等系统作为客户端操作系统。在 Mac 系统上可以通过下载 dmg 文件进行安装。其他系统的安装参见 Oracle VM VirtualBox 基础包 。在 Vagrant 和 VirtualBox 安装完成后,我们可以使用以下命令快速搭建一个 Ubuntu 的 VM:$ vagrant init ubuntu/hirsute64 $ vagrant up在启动成功后,我们可以通过 vagrant ssh 直接登录到 VM 系统中。1.3 系统环境搭建2021 年 4 月 22 号,Ubuntu 发布了 21.04 Hirsute Hippo 版本 [1],内核采用 5.11.0 版本。这里选择最新的 Ubuntu 发行版本,主要考虑 BPF 技术演进较快,新功能基本都需要高版本内核,采用最新发行的 Ubuntu 发行版本,方便后续的 BPF 功能学习和内核版本升级。更多 Ubuntu 版本发布的详情可参见官方 Wiki。这里我们使用 Vagrant 虚拟机的方式快速搭建环境,Hirsute Box 镜像 可在官方提供的 Vagrant Boxs 市场 中查询,ubuntu/hirsute64 Box 的镜像大小为 600M 左右。$ vagrant init ubuntu/hirsute64 $ vagrant up # 如果 box 有更新,可以使用 update 子命令更新 #$ vagrant box update如果从国外下载镜像过慢,可以通过其他方式下载 Box 文件,使用命令: vagrant box add <name> <boxpath> 然后导入到本地后再启动上述命令。为方便大家快速下载我已经上传了一份到 百度网盘 ,提取码【bgfg】。$ vagrant box add ubuntu/hirsute64 ~/Downloads/ubuntu_2104.box $ vagrant box list ubuntu/hirsute64 (virtualbox, 20210923.0.0)另外,我们也可以使用以下命令将正在运行的 VM 重新打包成 Box 文件:$ vagrant package --output hirsute64_my.box如果上述安装顺利,我们可以通过 ssh 的方式登录到新创建的 VM 机器中。$ vagrant ssh Welcome to Ubuntu 21.04 (GNU/Linux 5.11.0-36-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sat Sep 25 11:22:52 UTC 2021 System load: 0.0 Processes: 105 Usage of /: 3.4% of 38.71GB Users logged in: 0 Memory usage: 18% IPv4 address for enp0s3: 10.0.2.15 Swap usage: 0% * Super-optimized for small spaces - read how we shrank the memory footprint of MicroK8s to make it the smallest full K8s around. https://ubuntu.com/blog/microk8s-memory-optimisation 0 updates can be applied immediately. Last login: Sat Sep 25 08:13:09 2021 from 10.0.2.2 vagrant@ubuntu-hirsute:~$ uname -a Linux ubuntu-hirsute 5.11.0-36-generic #40-Ubuntu SMP Fri Sep 17 18:15:22 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux通过检查 CONFIG_DEBUG_INFO_BTF 内核编译选项确认系统是否已经支持 BTF 能力,这是 BPF CO-RE (Compile Once – Run Everywhere) 能力的基础。$ grep CONFIG_DEBUG_INFO_BTF /boot/config-5.11.0-36-generic CONFIG_DEBUG_INFO_BTF=y CONFIG_DEBUG_INFO_BTF_MODULES=y至此,我们的 BPF VM 环境已基本准备完成。2. BPF 程序编译对于我们编译 BPF 程序而言需要系统已经安装了必备的 linux-headers 包。在 Ubuntu/Debian/Arch/Manjaro 发行版系统中需要安装 linux-headers 包,而在 CentOS/Fedora 发行版系统中需要安装 kernel-headers 和 kernel-devel 两个包。首先我们在 VM 中安装 linux-headers 包:$ sudo apt update $ sudo apt install linux-headers-$(uname -r) Reading package lists... Done Building dependency tree... Done Reading state information... Done linux-headers-5.11.0-36-generic is already the newest version (5.11.0-36.40).如果是通过内核升级版本的场景,务必确认 linux-headers 版本与当前运行的内核版本一致。为了能够编译 BPF 程序,我们还需要安装编译所依赖的工具包:$ sudo apt install -y bison flex build-essential git cmake make libelf-dev clang llvm strace tar libfl-dev libssl-dev libedit-dev zlib1g-dev python python3-distutils $ clang --version Ubuntu clang version 12.0.0-3ubuntu1~21.04.2 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin $ llc --version LLVM (http://llvm.org/): LLVM version 12.0.0 Optimized build. Default target: x86_64-pc-linux-gnu Host CPU: haswell Registered Targets: bpf - BPF (host endian) bpfeb - BPF (big endian) bpfel - BPF (little endian) ...通过上述 clang/llvm 版本信息查看,我们可以知道在 Ubuntu 21.04 版本中,安装的为 12.0 的版本,这可以满足我们对于 CO-RE 能力的要求(>= 9.0 版本)。我们使用 libbpf-bootstrap 来进行编译验证,同时使用 --recursive 参数将 libbpf-bootstrap 仓库中的依赖子模块 libbpf 同时下载到本地。$ git clone --recursive https://github.com/libbpf/libbpf-bootstrap.git Cloning into 'libbpf-bootstrap'... remote: Enumerating objects: 260, done. remote: Counting objects: 100% (172/172), done. remote: Compressing objects: 100% (96/96), done. remote: Total 260 (delta 74), reused 149 (delta 64), pack-reused 88 Receiving objects: 100% (260/260), 1.65 MiB | 280.00 KiB/s, done. Resolving deltas: 100% (118/118), done. Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf' # 子模块 libbpf 下载 Cloning into '/home/vagrant/libbpf-bootstrap/libbpf'... remote: Enumerating objects: 6174, done. remote: Counting objects: 100% (1222/1222), done. remote: Compressing objects: 100% (336/336), done. remote: Total 6174 (delta 965), reused 942 (delta 866), pack-reused 4952 Receiving objects: 100% (6174/6174), 3.59 MiB | 547.00 KiB/s, done. Resolving deltas: 100% (4087/4087), done. Submodule path 'libbpf': checked out '8bf016110e683df2727a22ed90c9c9860c966544' $ cd libbpf-bootstrap/examples/c ~/libbpf-bootstrap/examples/c$ make如没有错误提示,且通过下述的 ls 命令可查看到对应的二进制文件,则表明编译成功。$ ls -hl -rwxrwxr-x 1 vagrant vagrant 1.4M Sep 25 13:30 bootstrap -rwxrwxr-x 1 vagrant vagrant 1.3M Sep 25 13:30 fentry -rwxrwxr-x 1 vagrant vagrant 1.3M Sep 25 13:30 kprobe -rwxrwxr-x 1 vagrant vagrant 1.3M Sep 25 13:30 minimal -rwxrwxr-x 1 vagrant vagrant 1.3M Sep 25 13:30 uprobe # 通过 bootstrap 程序运行验证 ~/libbpf-bootstrap/examples/c$ sudo ./bootstrap TIME EVENT COMM PID PPID FILENAME/EXIT CODE 13:41:14 EXEC ls 16473 7633 /usr/bin/ls 13:41:14 EXIT ls 16473 7633 [0] (1ms) # 用户空间程序与内核中的 BPF 通信的唯一入口是 bpf() 系统调用,可以通过 strace 查看整个交互流程 ~/libbpf-bootstrap/examples/c$ sudo strace -e bpf ./bootstrap bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_SOCKET_FILTER, insn_cnt=2, insns=0x7fffd2d6e3c0, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=0, func_info_rec_size=0, func_info=NULL, func_info_cnt=0, line_info_rec_size=0, line_info=NULL, line_info_cnt=0, attach_btf_id=0, attach_prog_fd=0}, 128) = 3 bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\20\0\0\0\20\0\0\0\5\0\0\0\1\0\0\0\0\0\0\1"..., btf_log_buf=NULL, btf_size=45, btf_log_size=0, btf_log_level=0}, 128) = 3 ...至此我们可以将 VM 导出为 Box 文件以便作为后续的基础,需要在 VM Vagrant 文件所在的目录操作运行,导出过程会自动关闭 VM 虚拟机。ubuntu_21_04$ vagrant package --output ubuntu_21_04_bpf.box ==> default: Attempting graceful shutdown of VM... ==> default: Clearing any previously set forwarded ports... ==> default: Exporting VM...ubuntu_21_04_bpf.box 文件已经上传 百度云盘 ,提取码【if2j】,文件大小 1.1G。3. 内核代码安装和 sample/bpf 模块编译安装 Ubuntu 21.04 源代码,并解压源代码:$ sudo apt-cache search linux-source linux-source - Linux kernel source with Ubuntu patches linux-source-5.11.0 - Linux kernel source for version 5.11.0 with Ubuntu patches $ sudo apt install linux-source-5.11.0 $ sudo cd /usr/src $ sudo tar -jxvf linux-source-5.11.0.tar.bz2 $ sudo cd /usr/src/linux-source-5.11.0源码编译 sample/bpf 模块:( 为方便编译,统一切换成 root 用户 )# cp -v /boot/config-$(uname -r) .config # make oldconfig && make prepare # make headers_install # apt-get install libcap-dev // 解决 sys/capability.h: No such file or directory # make M=samples/bpf // 编译最后,可能会报部分 WARNING,可以忽略 samples/bpf/Makefile:231: WARNING: Detected possible issues with include path. samples/bpf/Makefile:232: WARNING: Please install kernel headers locally (make headers_install). WARNING: Symbol version dump "Module.symvers" is missing. Modules may not have dependencies or modversions. MODPOST samples/bpf/Module.symvers WARNING: modpost: Symbol info of vmlinux is missing. Unresolved symbol check will be entirely skipped. # ls -hl samples/bpf/ // 查看是否生产需要的样例代码,如果生成了可执行文件,这表明编译成功 # ./tracex1 libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1 libbpf: elf: skipping unrecognized data section(17) .eh_frame libbpf: elf: skipping relo section(18) .rel.eh_frame for section(17) .eh_frame <...>-36521 [000] d.s1 2056.935795: bpf_trace_printk: skb 00000000a32b8f51 len 84 <...>-36521 [000] d.s1 2056.935817: bpf_trace_printk: skb 0000000094918e19 len 844. 附加 BPF 样例编译在 samples/bpf 目录包含接近 60 个相关的程序,给与我们学习提供了便利,同时也提供了一个组织编译的 Makefile 框架。如果我们用 C 语言编写的 BPF 程序编译可以直接在该目录提供的环境中进行编译。如果是需要单独编译的场景,也可以参考 BCC 仓库中的 libbpf-tools 下的样例。samples/bpf 下的程序一般组成方式是 xyz_user.c 和 xyz_kern.c:xyz_user.c 为用户空间的程序用于设置 BPF 程序的相关配置、加载 BPF 程序至内核、设置 BPF 程序中的 map 值和读取 BPF 程序运行过程中发送至用户空间的消息等。目前 xyz_user.c 与 xyz_kern.c 程序在交互实现都是基于 bpf() 系统调用完成的。直接使用 bpf() 系统调用涉及的参数和细节比较多,使用门槛较高,因此为了方便用户空间程序更加易用,内核提供了 libbpf 库封装了对于 bpf() 系统调用的细节。xyz_kern.c 为 BPF 程序代码,通过 clang 编译成字节码加载至内核中,在对应事件触发的时候运行,可以接受用户空间程序发送的各种数据,并将运行时产生的数据发送至用户空间程序。完整的样例代码参见 hello_world_bpf_ex。hello_user.c#include <stdio.h> #include <unistd.h> #include <bpf/libbpf.h> #include "trace_helpers.h" int main(int ac, char **argv) struct bpf_link *link = NULL; struct bpf_program *prog; struct bpf_object *obj; char filename[256]; snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); obj = bpf_object__open_file(filename, NULL); if (libbpf_get_error(obj)) { fprintf(stderr, "ERROR: opening BPF object file failed\n"); return 0; prog = bpf_object__find_program_by_name(obj, "bpf_hello"); if (!prog) { fprintf(stderr, "ERROR: finding a prog in obj file failed\n"); goto cleanup; /* load BPF program */ if (bpf_object__load(obj)) { fprintf(stderr, "ERROR: loading BPF object file failed\n"); goto cleanup; link = bpf_program__attach(prog); if (libbpf_get_error(link)) { fprintf(stderr, "ERROR: bpf_program__attach failed\n"); link = NULL; goto cleanup; read_trace_pipe(); cleanup: bpf_link__destroy(link); bpf_object__close(obj); return 0; }hello_kern.c#include <uapi/linux/bpf.h> #include <linux/version.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> SEC("tracepoint/syscalls/sys_enter_execve") int bpf_prog1(struct pt_regs *ctx) char fmt[] = "Hello %s !\n"; char comm[16]; bpf_get_current_comm(&comm, sizeof(comm)); bpf_trace_printk(fmt, sizeof(fmt), comm); return 0; char _license[] SEC("license") = "GPL"; u32 _version SEC("version") = LINUX_VERSION_CODE;Makefile 文件修改# diff -u Makefile.old Makefile --- Makefile.old 2021-09-26 03:16:16.883348130 +0000 +++ Makefile 2021-09-26 03:20:46.732277872 +0000 @@ -55,6 +55,7 @@ tprogs-y += xdp_sample_pkts tprogs-y += ibumad tprogs-y += hbm +tprogs-y += hello # Libbpf dependencies LIBBPF = $(TOOLS_PATH)/lib/bpf/libbpf.a @@ -113,6 +114,7 @@ xdp_sample_pkts-objs := xdp_sample_pkts_user.o ibumad-objs := ibumad_user.o hbm-objs := hbm.o $(CGROUP_HELPERS) +hello-objs := hello_user.o $(TRACE_HELPERS) # Tell kbuild to always build the programs always-y := $(tprogs-y) @@ -174,6 +176,7 @@ always-y += hbm_out_kern.o always-y += hbm_edt_kern.o always-y += xdpsock_kern.o +always-y += hello_kern.o ifeq ($(ARCH), arm) # Strip all except -D__LINUX_ARM_ARCH__ option needed to handle linux在当前目录重新运行 make 命令即可。编译完成后,启动命令后在其他窗口执行 ls -hl ,显示效果如下:$ sudo ./hello <...>-46054 [000] d... 7627.000940: bpf_trace_printk: Hello bash ! $ ldd hello linux-vdso.so.1 (0x00007ffd71306000) libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007f99d67fd000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f99d67e1000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f99d65f5000) /lib64/ld-linux-x86-64.so.2 (0x00007f99d6cf1000)5. 系统内核升级5.1 Ubuntu 内核官方包升级mainline 为 Ubuntu 提供可视化的内核升级工具,早期版本为 ukuu 。该工具支持的特性如下:从 Ubuntu Mainline PPA 中获取可用的内核列表当有新的内核更新时,可以选择观察并显示通知自动下载和安装软件包方便地显示可用和已安装的内核从界面上安装 / 卸载内核对于每个内核,相关的软件包(头文件和模块)会同时安装或卸载使用 Vagrant 快速搭建一个 VM 环境:$ vagrant box add ubuntu/hirsute64-ukuu ~/Downloads/ubuntu_21_04_bpf.box $ mkdir ubuntu_21_04_ukuu && cd ubuntu_21_04_ukuu $ vagrant init ubuntu/hirsute64-ukuu $ vagrant up可以通过以下命令自动安装:$ sudo add-apt-repository ppa:cappelikan/ppa $ sudo apt update $ sudo apt install mainline $ mainline -h mainline 1.0.15 Distribution: Ubuntu 21.04 Architecture: amd64 Running kernel: 5.11.0-36-generic mainline 1.0.15 - Ubuntu Mainline Kernel Installerlist 子命令会显示出来当前系统支持升级的或者已经安装的系统:$ mainline --list mainline 1.0.15 Distribution: Ubuntu 21.04 Architecture: amd64 Running kernel: 5.11.0-36-generic Fetching index from kernel.ubuntu.com... Fetching index... ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100 % ---------------------------------------------------------------------- Found installed: 5.11.0.36.38 Found installed: 5.11.0-36.40 ---------------------------------------------------------------------- ---------------------------------------------------------------------- ====================================================================== Available Kernels ====================================================================== 5.14.8 5.14.7 $ sudo mainline --install 5.14.7 # 则会启动对应的内核安装 mainline 1.0.15 Distribution: Ubuntu 21.04 Architecture: amd64 Running kernel: 5.11.0-36-generic Fetching index from kernel.ubuntu.com... Fetching index... ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100 % ---------------------------------------------------------------------- Found installed: 5.11.0.36.38 Found installed: 5.11.0-36.40 ---------------------------------------------------------------------- ---------------------------------------------------------------------- Downloading: 'amd64/linux-headers-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb'... Downloading: 'amd64/linux-image-unsigned-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb'... Downloading: 'amd64/linux-headers-5.14.7-051407_5.14.7-051407.202109221210_all.deb'... Downloading: 'amd64/linux-modules-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb'... ...通过上述输出我们也可以看出,和手工安装的过程类似,同样需要 4 个主要文件,( 手动安装可以到 http://kernel.ubuntu.com/~kernel-ppa/mainline/ 这里下载 ):linux-headers-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb(通用头文件) 和 amd64/linux-headers-5.14.7-051407_5.14.7-051407.202109221210_all.deb(基础头文件)amd64/linux-image-unsigned-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.debamd64/linux-modules-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb在本次升级到 5.14.7 的过程中,modules 和 linux images 成功,但是安装 linux-headers-5.14.7-051407-generic 头文件的时候,遇到了以下的问题:dpkg: dependency problems prevent configuration of linux-headers-5.14.7-051407-generic: linux-headers-5.14.7-051407-generic depends on libc6 (>= 2.34); however: Version of libc6:amd64 on system is 2.33-0ubuntu5. dpkg: error processing package linux-headers-5.14.7-051407-generic (--install): dependency problems - leaving unconfigured Errors were encountered while processing: linux-headers-5.14.7-051407-generic E: Installation completed with errors错误提示的大意是我们安装的 linux-headers-5.14.7-051407 需要 libc 的版本 >= 2.34,而我们系统的 libc 版本为 2.33。而 linux-headers 是我们 BPF 需要的环境 linux-headers,所以还需要手动修复安装。$ sudo reboot $ uname -a Linux ubuntu-hirsute 5.14.7-051407-generic #202109221210 SMP Wed Sep 22 15:15:48 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux系统重启后已经证明我们成功升级了系统,这里我们继续手工修复 linux-headers 问题,首先尝试使用手工安装:$ sudo apt install linux-headers-$(uname -r) Reading package lists... Done Building dependency tree... Done Reading state information... Done linux-headers-5.14.7-051407-generic is already the newest version (5.14.7-051407.202109221210). You might want to run 'apt --fix-broken install' to correct these. The following packages have unmet dependencies: linux-headers-5.14.7-051407-generic : Depends: libc6 (>= 2.34) but 2.33-0ubuntu5 is to be installed E: Unmet dependencies. Try 'apt --fix-broken install' with no packages (or specify a solution).错误提示我们使用 apt --fix-broken install 来修订错误,该命令其实做的事情就是删除存在问题的头文件包:$ sudo apt --fix-broken install Reading package lists... Done Building dependency tree... Done Reading state information... Done Correcting dependencies... Done The following packages will be REMOVED: linux-headers-5.14.7-051407-generic 0 upgraded, 0 newly installed, 1 to remove and 4 not upgraded. 1 not fully installed or removed. After this operation, 23.3 MB disk space will be freed. Do you want to continue? [Y/n] y (Reading database ... 162397 files and directories currently installed.) Removing linux-headers-5.14.7-051407-generic (5.14.7-051407.202109221210) ... $ dpkg -l |grep -i linux-headers-5.14.7 ii linux-headers-5.14.7-051407 5.14.7-051407.202109221210 all Header files related to Linux kernel version 5.14.7从网上搜索 libc-2.34 版本的安装包 ,下载并安装:$ wget http://launchpadlibrarian.net/560614488/libc6_2.34-0ubuntu3_amd64.deb # 因为本地已经安装了 libc6_2.33,安装会提示冲突,这里使用 --force-all 参数 $ sudo dpkg -i --force-all libc6_2.34-0ubuntu3_amd64.deb $ dpkg -l|grep libc6 ii libc6:amd64 2.34-0ubuntu3 amd64 GNU C Library: Shared libraries到 Ubunt 5.14.7 主线 下载安装出错的 linux-headers-5.14.7-051407-generic,然后手工进行安装:$sudo dpkg -i linux-headers-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb Selecting previously unselected package linux-headers-5.14.7-051407-generic. (Reading database ... 152977 files and directories currently installed.) Preparing to unpack linux-headers-5.14.7-051407-generic_5.14.7-051407.202109221210_amd64.deb ... Unpacking linux-headers-5.14.7-051407-generic (5.14.7-051407.202109221210) ... Setting up linux-headers-5.14.7-051407-generic (5.14.7-051407.202109221210) ...这种强制安装的 pkg 的方式,会影响到其他依赖包的管理,一般不推荐。 这种情况一般是我们升级的内核版本比较新,其他的对应组件还未能完全更新。如果是升级到 Ubuntu 的官方新发布版本(比如从 20.04 升级到 21.04),建议通过官方提供的方式升级,内核和其他依赖包都可以一起升级,保证整体完整型,可参考 如何升级到 Ubuntu 20.04。5.2 内核源码编译升级内核(进阶版)某些情况我们需要更新版本的内核,如果 Ubuntu 官方还未提供该版本的安装包及内核源码,那么我们就需要通过源码编译和安装的方式进行。本文从 Ubuntu 环境中编译,其他的 Linux 发行版可以参考 内核源码编译指南 。5.2.1 源码下载及编译工具安装基于 Ubuntu 21.04 的系统,我们从 内核网站 上选择 5.14.7 版本进行编译。$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.14.7.tar.xz $ xz -d linux-5.14.7.tar.xz $ tar xvf linux-5.14.7.tar安装必要的内核编译工具:$ sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev -yLinux BTF 功能需要 pahole 工具 (>= 1.16) ,该工具位于 dwarves 包中,需要提前进行安装。$ sudo apt install dwarves -y5.2.2 源码编译编译前需要将系统中运行的 config 文件复制到内核编译目录中为 .config 文件。$ cd linux-5.14.7/ ~/linux-5.14.7$ cp -v /boot/config-5.11.0-36-generic .config '/boot/config-5.11.0-36-generic' -> '.config' $ make menuconfig因为当前目录已经存在 .config 文件,make menuconfig 会直接使用 .config 作为默认值,通过图形界面修改值以后,默认会将当前的 .config 备份成 .config.old,并生成新的 .config 文件。这里如果采用 make oldconfig,那么则是通过命令界面配置内核,会自动载入既有的 .config 配置文件,并且只有在遇到先前没有设定过的选项时,才会要求我们手动设定。同样也会将老的 .config 备份成 .config.old 文件。运行 make 命令编译, -j 参数可指定并行 CPU 核数:$ make -j $(getconf _NPROCESSORS_ONLN)如果编译过程中遇到 No rule to make target 'debian/canonical-certs.pem' 的错误,可以通过禁用证书或者将运行系统中源码的相关证书复制到当前目录下的 debian 目录。** 方案 1:复制系统中的证书 **将当前运行系统源码中的 /usr/src/linux-source-5.11.0/debian/canonical-certs.pem 和 /usr/src/linux-source-5.11.0/debian/canonical-revoked-certs.pem 复制到当前 debian 目录中:$ mkdir debian $ cp /usr/src/linux-source-5.11.0/debian/canonical-certs.pem ./debain/ $ cp /usr/src/linux-source-5.11.0/debian/canonical-revoked-certs.pem ./debain/** 方案 2:采用禁用 SYSTEM_TRUSTED_KEYS 的方式 **$ scripts/config --disable SYSTEM_TRUSTED_KEYS # 关闭禁止证书,修订 No rule to make target 'debian/canonical-certs.pem'待证书设置以后,就可以运行 make 命令进行编译。编译的时长使用的 CPU 核数相关,编译大概占用 20G 左右的产品空间。5.2.3 内核安装和启动选择编译成功后,使用以下命令安装内核:$ sudo make modules_install && make install && make headers_install $ sudo reboot [....] $ uname -a Linux ubuntu-hirsute 5.14.7 #1 SMP Sun Sep 26 11:32:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linuxreboot 重启后,我们使用使用 uname 查看,发现系统内核已经更新为了 5.14.7 版本。Ubuntu 21.04 采用 GRUB2 来管理启动菜单,对应的文件为 /boot/grub/grub.cfg ,该文件为 update-grub 命令依据 /etc/default/grub 文件自动生成。如果需要调整菜单选项,这需要通过 /etc/default/grub 文件修改,然后通过运行 update-grub 命令生效。/etc/default/grub 文件中的:GRUB_DEFAULT=0 GRUB_TIMEOUT_STYLE=hidden GRUB_TIMEOUT=0GRUB_DEFAULT 指定了默认启动的菜单选项,0 表示第一个菜单选项。GRUB_TIMEOUT 选项表明了启动菜单显示的时间,方便用于用户选择,默认为 0,在调试新的内核时建议设置成一个非 0 值,同时需要将 GRUB_TIMEOUT_STYLE 的值从 hidden 调整为 menu:GRUB_TIMEOUT_STYLE=menu GRUB_TIMEOUT=5然后通过 update-grub 命令更新生效,再重新启动就可以看到启动菜单。其他参数详细配置可参见 Grub 配置 。todo: 在 VirtualBox 中能看到停顿,但是未能够看到启动界面5.2.4 其他 - 编译内核以后磁盘管理Virtualbox 底层使用 vmdk 的磁盘格式,当前会存在只是单向增大不会自动收缩的问题(即使我们在编译内核后,删除了编译相关的问题),请参见 VirtualBox VM 空间瘦身记(vmdk)。6. 参考Ubuntu 21.04 is here ↩︎

【译】eBPF 概述:第 5 部分:跟踪用户进程

1. 前言在之前的部分中,我们专注于 Linux 内核跟踪,在我们看来,基于 eBPF 的项目是最安全、最广泛可用和最有效的方法(eBPF 在 Linux 中完全是上游支持的,保证稳定的 ABI,在几乎所有的发行版中都默认启用,并可与所有其他跟踪机制集成)。 eBPF 成为内核工作的不二之选。 然而,到目前为止,我们故意避免深入讨论用户空间跟踪,因为它值得特别对待,因此我们在第 5 部分中专门讨论。首先,我们将讨论为什么使用,然后我们将 eBPF 用户跟踪分为静态和动态两类分别讨论。2. 为什么要在用户空间使用 eBPF?最重要的用户问题是,既然有这么多其他的调试器/性能分析器/跟踪器,这么多针对特定语言或操作系统的工具为同样的任务而开发,为什么还要使用 eBPF 来跟踪用户空间进程?答案并不简单,根据不同的使用情况,eBPF 可能不是最佳解决方案;在庞大的用户空间生态系统中,并没有一个适合所有情况的调试/跟踪的项目。eBPF 跟踪具有以下优势:它为内核和用户空间提供了一个统一的跟踪接口,与其他工具([k,u]probe, (dtrace)tracepoint 等)使用的机制兼容。2015 年的文章选择 linux 跟踪器 虽然有些过时,但其提供了很好的见解,说明使用所有不同的工具有多困难,要花多少精力。有一个统一的、强大的、安全的、可广泛使用的框架来满足大多数跟踪的需要,是非常有价值的。一些更高级别的工具,如 Perf/SystemTap/DTrace,正在 eBPF 的基础上重写(成为 eBPF 的前端),所以了解 eBPF 有助于使用它们。eBPF 是完全可编程的。Perf/ftrace 和其他工具都需要在事后处理数据,而 eBPF 可直接在内核/应用程序中运行自定义的高级本地编译的 C/Python/Go 检测代码。它可以在多个 eBPF 事件运行之间存储数据,例如以基于函数状态/参数计算每个函数调用统计数据。eBPF 可以跟踪系统中的一切,它并不局限于特定的应用程序。例如可以在共享库上设置 uprobes 并跟踪链接和调用它的所有进程。很多调试器需要暂停程序来观察其状态或降低运行时性能,从而难以进行实时分析,尤其是在生产工作负载上。因为 eBPF 附加了 JIT 的本地编译的检测代码,它的性能影响是最小的,不需要长时间暂停执行。诚然,eBPF 也有一些缺点:eBPF 不像其他跟踪器那样可以移植。该虚拟机主要是在 Linux 内核中开发的(有一个正在进行的 BSD 移植),相关的工具是基于 Linux 开发的。eBPF 需要一个相当新的内核。例如对于 MIPS 的支持是在 v4.13 中加入的,但绝大多数 MIPS 设备在运行的内核都比 v4.13 老。一般来说,eBPF 不容易像语言或特定应用的用户空间调试器那样提供一样多的洞察力。例如,Emacs 的核心是一个用 C 语言编写的 ELISP 解释器:eBPF 可以通过挂载在 Emacs 运行时的 C 函数调用来跟踪/调试 ELISP 程序,但它不了解更高级别的 ELISP 语言实现,因此使用 Emacs 提供的特殊 ELISP 语言特定跟踪器和调试器变得更加有用。另一个例子是调试在 Web 浏览器引擎中运行的 JavaScript 应用程序。因为 "普通 eBPF" 在 Linux 内核中运行,所以每次 eBPF 检测用户进程时都会发生内核 - 用户上下文切换。这对于调试性能关键的用户空间代码来说可能很昂贵(也许可以使用用户空间 eBPF 虚拟机项目来避免这种切换成本?)。这对于调试性能关键的用户空间代码来说是很昂贵的(也许用户空间 eBPF VM 项目可以用来避免这种切换成本?)。这种上下文切换比正常的调试器(或像 strace 这样的工具)要便宜得多,所以它通常可以忽略不计,但在这种情况下,像 LTTng 这样能够完全运行在用户空间的跟踪器可能更合适。3. 静态跟踪点(USDT 探针)静态跟踪点(tracepoint),在用户空间也被称为 USDT(用户静态定义的跟踪)探针(应用程序中感兴趣的特定位置),跟踪器可以在此处挂载检查代码执行和数据。它们由开发人员在源代码中明确定义,通常在编译时用 "--enable-trace" 等标志启用。静态跟踪点的优势在于它们不会经常变化:开发人员通常会保持稳定的静态跟踪 ABI,所以跟踪工具在不同的应用程序版本之间工作,这很有用,例如当升级 PostgreSQL 安装并遇到性能降低时。3.1 预定义的跟踪点BCC-tools 包含很多有用的且经过测试的工具,可以与特定应用程序或语言运行时定义的跟踪点进行交互。对于我们的示例,我们将跟踪 Python 应用程序。确保你在构建了 python3 时启用了 "--enable-trace" 标识,并在 python 二进制文件或 libpython(取决于你构建方式)上运行 tplist 以确认跟踪点被启用:$ tplist -l /usr/lib/libpython3.7m.so b'/usr/lib/libpython3.7m.so' b'python':b'import__find__load__start' b'/usr/lib/libpython3.7m.so' b'python':b'import__find__load__done' b'/usr/lib/libpython3.7m.so' b'python':b'gc__start' b'/usr/lib/libpython3.7m.so' b'python':b'gc__done' b'/usr/lib/libpython3.7m.so' b'python':b'line' b'/usr/lib/libpython3.7m.so' b'python':b'function__return' b'/usr/lib/libpython3.7m.so' b'python':b'function__entry'首先我们使用 BCC 提供的一个很酷的跟踪工具 uflow,来跟踪 python 的简单 http 服务器的执行流程。跟踪应该是不言自明的,箭头和缩进表示函数的进入/退出。我们在这个跟踪中看到的是一个工作线程如何在 CPU 3 上退出,而主线程则准备在 CPU 0 上为其他传入的 http 请求提供服务。$ python -m http.server >/dev/null & sudo ./uflow -l python $! [4] 11727 Tracing method calls in python process 11727... Ctrl-C to quit. CPU PID TID TIME(us) METHOD 3 11740 11757 7.034 /usr/lib/python3.7/_weakrefset.py._remove 3 11740 11757 7.034 /usr/lib/python3.7/threading.py._acquire_restore 0 11740 11740 7.034 /usr/lib/python3.7/threading.py.__exit__ 0 11740 11740 7.034 /usr/lib/python3.7/socketserver.py.service_actions 0 11740 11740 7.034 /usr/lib/python3.7/selectors.py.select 0 11740 11740 7.532 /usr/lib/python3.7/socketserver.py.service_actions 0 11740 11740 7.532接下来,我们希望在跟踪点被命中时运行我们的自定义代码,因此我们不完全依赖 BCC 提供的任何工具。 以下示例将自身挂钩到 python 的 function__entry 跟踪点(请参阅 python 检测文档)并在有人下载文件时通知我们:#!/usr/bin/env python from bcc import BPF, USDT import sys bpf = """ #include <uapi/linux/ptrace.h> static int strncmp(char *s1, char *s2, int size) {for (int i = 0; i < size; ++i) if (s1[i] != s2[i]) return 1; return 0; int trace_file_transfers(struct pt_regs *ctx) { uint64_t fnameptr; char fname[128]={0}, searchname[9]="copyfile"; bpf_usdt_readarg(2, ctx, &fnameptr); bpf_probe_read(&fname, sizeof(fname), (void *)fnameptr); if (!strncmp(fname, searchname, sizeof(searchname))) bpf_trace_printk("Someone is transferring a file!\\n"); return 0; """ u = USDT(pid=int(sys.argv[1])) u.enable_probe(probe="function__entry", fn_name="trace_file_transfers") b = BPF(text=bpf, usdt_contexts=[u]) while 1: (_, _, _, _, ts, msg) = b.trace_fields() except ValueError: continue print("%-18.9f %s" % (ts, msg))我们通过再次附加到简单的 http-server 进行测试:$ python -m http.server >/dev/null & sudo ./trace_simplehttp.py $! [14] 28682 34677.450520000 b'Someone is transferring a file!'上面的例子告诉我们什么时候有人在下载文件,但它不能比这更详细的信息,比如关于谁在下载、下载什么文件等。这是因为 python 只默认启用了几个非常通用的跟踪点(模块加载、函数进入/退出等)。为了获得更多的信息,我们必须在感兴趣的地方定义我们自己的跟踪点,以便我们能够提取相关的数据。3.2 定义我们自己的跟踪点到目前为止,我们只使用别人定义的跟踪点,但是如果我们的应用程序并没有提供任何跟踪点,或者我们需要添加比现有跟踪点更多的信息,那么我们将不得不添加自己的跟踪点。添加跟踪点方式有多种,例如 python-core 通过 pydtrace.h 和 pydtrace.d 使用 systemtap 的开发包 "systemtap-sdt-dev",但我们将采取另一种方法,使用 libstapsdt,因为它有一个更简单的 API,更轻巧(只依赖于 libelf),并支持多种语言绑定。为了保持一致性,我们再次把重点放在 python 上,但是跟踪点也可以用其他语言添加,这里有一个 C 语言示例。首先,我们给简单的 http 服务器打上补丁,公开跟踪点。代码应该是不言自明的:注意跟踪点的名字 file_transfer 及其参数,足够存储两个字符串指针和一个 32 位无符号整数,代表客户端 IP 地址,文件路径和文件大小。diff --git a/usr/lib/python3.7/http/server.py b/usr/lib/python3.7/http/server.py index ca2dd50..af08e10 100644 --- a/usr/lib/python3.7/http/server.py +++ b/usr/lib/python3.7/http/server.py @@ -107,6 +107,13 @@ from functools import partial from http import HTTPStatus +import stapsdt +provider = stapsdt.Provider("simplehttp") +probe = provider.add_probe("file_transfer", + stapsdt.ArgTypes.uint64, + stapsdt.ArgTypes.uint64, + stapsdt.ArgTypes.uint32)+provider.load() # Default error message template DEFAULT_ERROR_MESSAGE = """\ @@ -650,6 +657,8 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): f = self.send_head() if f: + path = self.translate_path(self.path) + probe.fire(self.address_string(), path, os.path.getsize(path)) self.copyfile(f, self.wfile) finally: f.close()运行打过补丁的服务器,我们可以使用 tplist 验证我们的 file_transfer 跟踪点在运行时是否存在:$ python -m http.server >/dev/null 2>&1 & tplist -p $! [1] 13297 b'/tmp/simplehttp-Q6SJDX.so' b'simplehttp':b'file_transfer' b'/usr/lib/libpython3.7m.so.1.0' b'python':b'import__find__load__start' b'/usr/lib/libpython3.7m.so.1.0' b'python':b'import__find__load__done'我们将对上述示例中的跟踪器示例代码进行以下最重要的修改:它将其逻辑挂接到我们自定义的 file_transfer 跟踪点。它使用 PERF EVENTS 来存储可以将任意结构传递到用户空间的数据,而不是我们之前使用的 ftrace 环形缓存区只能传输单个字符串。它不使用 bpf_usdt_readarg 来获取 USDT 提供的指针,而是直接在处理程序函数签名中声明它们。 这是一个显着的质量改善,可用于所有处理程序。此跟踪器明确使用 python2,即使到目前为止我们所有的示例(包括上面的 python http.server 补丁) 使用 python3。 希望将来所有 BCC API 和文档都能移植到 python 3。#!/usr/bin/env python2 from bcc import BPF, USDT import sys bpf = """ #include <uapi/linux/ptrace.h> BPF_PERF_OUTPUT(events); struct file_transf {char client_ip_str[20]; char file_path[300]; u32 file_size; u64 timestamp; int trace_file_transfers(struct pt_regs *ctx, char *ipstrptr, char *pathptr, u32 file_size) {struct file_transf ft = {0}; ft.file_size = file_size; ft.timestamp = bpf_ktime_get_ns(); bpf_probe_read(&ft.client_ip_str, sizeof(ft.client_ip_str), (void *)ipstrptr); bpf_probe_read(&ft.file_path, sizeof(ft.file_path), (void *)pathptr); events.perf_submit(ctx, &ft, sizeof(ft)); return 0; """ def print_event(cpu, data, size): event = b["events"].event(data) print("{0}: {1} is downloding file {2} ({3} bytes)".format(event.timestamp, event.client_ip_str, event.file_path, event.file_size)) u = USDT(pid=int(sys.argv[1])) u.enable_probe(probe="file_transfer", fn_name="trace_file_transfers") b = BPF(text=bpf, usdt_contexts=[u]) b["events"].open_perf_buffer(print_event) while 1: b.perf_buffer_poll() except KeyboardInterrupt: exit()跟踪已打过补丁的服务器:$ python -m http.server >/dev/null 2>&1 & sudo ./trace_stapsdt.py $! [1] 5613 325540469950102: 127.0.0.1 is downloading file /home/adi/ (4096 bytes) 325543319100447: 127.0.0.1 is downloading file /home/adi/.bashrc (827 bytes) 325552448306918: 127.0.0.1 is downloading file /home/adi/workspace/ (4096 bytes) 325563646387008: 127.0.0.1 is downloading file /home/adi/workspace/work.tar (112640 bytes) (...)上面自定义的 file_transfer 跟踪点看起来很简单(直接 python 打印或日志记录调用可能有相同的效果),但它提供的机制非常强大:良好放置的跟踪点保证 ABI 稳定性,提供动态运行的能力安全、本地快速、可编程逻辑可以非常有助于快速分析和修复各种问题,而无需重新启动有问题的应用程序(重现问题可能需要很长时间)。4. 动态探针(uprobes)上面举例说明的静态跟踪点的问题在于,它们需要在源代码中明确定义,并且在修改跟踪点时需要重新构建应用程序。保证现有跟踪点的 ABI 稳定性对维护人员如何重新构建/重写跟踪点数据的代码施加了限制。因此,在某些情况下,完全运行时动态用户空间探测器(uprobes)是首选:它们以特别的方式直接在运行应用程序的内存中进行探测,而无需任何特殊的源代码定义。动态探测器可能会比较容易在应用程序版本之间失效,但即便如此,它们对于实时调试正在运行的实例也很有用。虽然静态跟踪点对于跟踪用 Python 或 Java 等高级语言编写的应用程序很有用,但 uprobes 对此不太有用,因为它们工作比较底层,并且不了解语言运行时实现(静态跟踪点之所以可以工作,因为开发人员自行承担公开高级应用程序的相关数据)。然而,动态探测器对于调试语言实现/引擎本身或用没有运行时的语言(如 C)编写的应用程序很有用。可以将 uprobe 添加到优化过(stripped)的二进制文件中,但用户必须手动计算进程内内存偏移位置,uprobe 应通过 objdump 和 /proc//maps 等工具附加到该位置(参见示例),但这种方式比较痛苦且不可移植。 由于大多数发行版都提供调试符号包(或使用调试符号构建的快速方法)并且 BCC 使得使用带有符号名称解析的 uprobes 变得简单,因此绝大多数动态探测使用都是以这种方式进行的。gethostlatency BCC 工具通过将 uprobes 附加到 gethostbyname 和 libc 中的相关函数来打印 DNS 请求延迟。 要验证 libc 未优化(stripped)以便可以运行该工具(否则会引发 sybol-not-found 错误):$ file /usr/lib/libc-2.28.so /usr/lib/libc-2.28.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, (...), not stripped $ nm -na /usr/lib/libc-2.28.so | grep -i -e getaddrinfo 0000000000000000 a getaddrinfo.cgethostlatency 代码与我们上面检查的跟踪点示例非常相似(并且在某些地方相同,它还使用 BPF_PERF_OUTPUT) ,所以我们不会在这里完整地发布它。 最相关的区别是使用 BCC uprobe API:b.attach_uprobe(name="c", sym="getaddrinfo", fn_name="do_entry", pid=args.pid) b.attach_uretprobe(name="c", sym="getaddrinfo", fn_name="do_return", pid=args.pid)这里需要理解和记住的关键思想是:只要对我们的 BCC eBPF 程序做一些小的改动,我们就可以通过静态和动态探测来跟踪非常不同的应用程序、库甚至是内核。之前我们是静态跟踪 Python 应用程序,现在我们是动态地测量 libc 的主机名解析延时。通过对这些小的(小于 150LOC,很多是模板)例子进行类似的修改,可在运行的系统中跟踪任何内容,这非常安全,没有崩溃的风险或其他工具引起的问题(如调试器应用程序暂停/停顿)。5. 总结在第 5 部分中,我们研究了如何使用 eBPF 程序来跟踪用户空间应用程序。 使用 eBPF 完成这项任务的最大优势是它提供了一个统一的接口来安全有效地跟踪整个系统:可以在应用程序中重现错误,然后进一步跟踪到库或内核中,通过统一的编程框架/接口提供完整的系统可见性。 然而,eBPF 并不是银弹,尤其是在调试用高级语言编写的应用程序时,特定语言的工具可以更好地提供洞察力,或者对于那些运行旧版本 Linux 内核或需要非 Linux 系统的应用程序。

【译】eBPF 概述:第 4 部分:在嵌入式系统运行

1. 前言在本系列的第 1 部分和第 2 部分,我们介绍了 eBPF 虚拟机内部工作原理,在第 3 部分我们研究了基于底层虚拟机机制之上开发和使用 eBPF 程序的主流方式。在这一部分中,我们将从另外一个视角来分析项目,尝试解决嵌入式 Linux 系统所面临的一些独特的问题:如需要非常小的自定义操作系统镜像,不能容纳完整的 BCC LLVM 工具链/python 安装,或试图避免同时维护主机的交叉编译(本地)工具链和交叉编译的目标编译器工具链,以及其相关的构建逻辑,即使在使用像 OpenEmbedded/Yocto 这样的高级构建系统时也很重要。2. 关于可移植性在第 3 部分研究的运行 eBPF/BCC 程序的主流方式中,可移植性并不是像在嵌入式设备上面临的问题那么大:eBPF 程序是在被加载的同一台机器上编译的,使用已经运行的内核,而且头文件很容易通过发行包管理器获得。嵌入式系统通常运行不同的 Linux 发行版和不同的处理器架构,与开发人员的计算机相比,有时具有重度修改或上游分歧的内核,在构建配置上也有很大的差异,或还可能使用了只有二进制的模块。eBPF 虚拟机的字节码是通用的(并未与特定机器相关),所以一旦编译好 eBPF 字节码,将其从 x86_64 移动到 ARM 设备上并不会引起太多问题。当字节码探测内核函数和数据结构时,问题就开始了,这些函数和数据结构可能与目标设备的内核不同或者会不存在,所以至少目标设备的内核头文件必须存在于构建 eBPF 程序字节码的主机上。新的功能或 eBPF 指令也可能被添加到以后的内核中,这可以使 eBPF 字节码向前兼容,但不能在内核版本之间向后兼容(参见内核版本与 eBPF 功能)。建议将 eBPF 程序附加到稳定的内核 ABI 上,如跟踪点 tracepoint,这可以缓解常见的可移植性。最近一个重要的工作已经开始,通过在 LLVM 生成的 eBPF 对象代码中嵌入数据类型信息,通过增加 BTF(BTF 类型格式)数据,以增加 eBPF 程序的可移植性(CO-RE 一次编译,到处运行)。更多信息见这里的补丁和文章。这很重要,因为 BTF 涉及到 eBPF 软件技术栈的所有部分(内核虚拟机和验证器、clang/LLVM 编译器、BCC 等),但这种方式可带来很大的便利,允许重复使用现有的 BCC 工具,而不需要特别的 eBPF 交叉编译和在嵌入式设备上安装 LLVM 或运行 BPFd。截至目前,CO-RE BTF 工作仍处于早期开发阶段,还需要付出相当多的工作才能可用【译者注:当前在高版本内核已经可以使用或者编译内核时启用了 BTF 编译选项】。也许我们会在其完全可用后再发表一篇博文。3. BPFdBPFd(项目地址https://github.com/joelagnel/bpfd)更像是一个为 Android 设备开发的概念验证,后被放弃,转而通过 adeb 包运行一个完整的设备上的 BCC 工具链【译者注:BCC 在 adeb 的编译文档参见这里】。如果一个设备足够强大,可以运行 Android 和 Java,那么它也可能可以安装 BCC/LLVM/python。尽管这个实现有些不完整(通信是通过 Android USB 调试桥或作为一个本地进程完成的,而不是通过一个通用的传输层),但这个设计很有趣,有足够时间和资源的人可以把它拿起来合并,继续搁置的 PR 工作。简而言之,BPFd 是一个运行在嵌入式设备上的守护程序,作为本地内核/libbpf 的一个远程过程调用(RPC)接口。Python 在主机上运行,调用 BCC 来编译/部署 eBPF 字节码,并通过 BPFd 创建/读取 map。BPFd 的主要优点是,所有的 BCC 基础设施和脚本都可以工作,而不需要在目标设备上安装 BCC、LLVM 或 python,BPFd 二进制文件只有 100kb 左右的大小,并依赖 libc。4. Plyply 项目实现了一种与 BPFtrace 非常相似的高级领域特定语言(受到 AWK 和 C 的启发),其明确的目的是将运行时的依赖性降到最低。它只依赖于一个现代的 libc(不一定是 GNU 的 libc)和 shell(与 sh 兼容)。Ply 本身实现了一个 eBPF 编译器,需要根据目标设备的内核头文件进行构建,然后作为一个单一的二进制库和 shell 包装器部署到目标设备上。为了更好解释 ply,我们把第 3 部分中的 BPFtrace 例子和与 ply 实现进行对比:BPFtrace:要运行该例子,你需要数百 MB 的 LLVM/clang、libelf 和其他依赖项:bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}'ply:你只需要一个 ~50kb 的二进制文件,它产生的结果是相同的,语法几乎相同:ply 'tracepoint:raw_syscalls/sys_enter {@[pid, comm] = count();}'Ply 仍在大量开发中(最近的 v2.0 版本是完全重写的)【译者注:当前最新版本为 2.1.1,最近一次代码提交是 8 个月前,活跃度一般】,除了一些示例之外,该语言还不不稳定或缺乏文档,它不如完整的 BCC 强大,也没有 BPFtrace 丰富的功能特性,但它对于通过 ssh 或串行控制台快速调试远程嵌入式设备仍然非常有用。5. GobpfGobpf 及其合并的子项目(goebpf, gobpf-elf-loader),是 IOVisor 项目的一部分,为 BCC 提供 Golang 语言绑定。eBPF 的内核逻辑仍然用 "限制性 C" 编写,并由 LLVM 编译,只有标准的 python/lua 用户空间脚本被 Go 取代。这个项目对嵌入式设备的意义在于它的 eBPF elf 加载模块,其可以被交叉编译并在嵌入式设备上独立运行,以加载 eBPF 程序至内核并与与之交互。值得注意的是,go 加载器可以被写成通用的(我们很快就会看到),因此它可以加载和运行任何 eBPF 字节码,并在本地重新用于多个不同的跟踪会话。使用 gobpf 很痛苦的,主要是因为缺乏文档。目前最好的 "文档" 是 tcptracer source 的源代码,它相当复杂(他们使用 kprobes 而不依赖于特定的内核版本!),但从它可以学到很多。Gobpf 本身也是一项正在进行的工作:虽然 elf 加载器相当完整,并支持加载带有套接字、(k|u)probes、tracepoints、perf 事件等加载的 eBPF ELF 对象,但 bcc go 绑定模块还不容易支持所有这些功能。例如,尽管你可以写一个 socket_ilter ebpf 程序,将其编译并加载到内核中,但你仍然不能像 BCC 的 python 那样从 go 用户空间轻松地与 eBPF 进行交互,BCC 的 API 更加成熟和用户友好。无论如何,gobpf 仍然比其他具有类似目标的项目处于更好的状态。让我们研究一个简单的例子来说明 gobpf 如何工作的。首先,我们将在本地 x86_64 机器上运行它,然后交叉编译并在 32 位 ARMv7 板上运行它,比如流行的 Beaglebone 或 Raspberry Pi。我们的文件目录结构如下:$ find . -type f ./src/open-example.go ./src/open-example.c ./Makefileopen-example.go:这是建立在 gobpf/elf 之上的 eBPF ELF 加载器。它把编译好的 "限制性 C" ELF 对象作为参数,加载到内核并运行,直到加载器进程被杀死,这时内核会自动卸载 eBPF 逻辑【译者注:通常情况是这样的,也有场景加载器退出,ebpf 程序继续运行的】。我们有意保持加载器的简单性和通用性(它加载在对象文件中发现的任何探针),因此加载器可以被重复使用。更复杂的逻辑可以通过使用 gobpf 绑定 模块添加到这里。package main import ( "fmt" "os" "os/signal" "github.com/iovisor/gobpf/elf" func main() {mod := elf.NewModule(os.Args[1]) err := mod.Load(nil); if err != nil {fmt.Fprintf(os.Stderr, "Error loading'%s'ebpf object: %v\n", os.Args[1], err)os.Exit(1) err = mod.EnableKprobes(0) if err != nil {fmt.Fprintf(os.Stderr, "Error loading kprobes: %v\n", err) os.Exit(1) sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, os.Kill) // ... }open-example.c:这是上述加载器加载至内核的 "限制性 C" 源代码。它挂载在 do_sys_open 函数,并根据 ftrace format 将进程命令、PID、CPU、打开文件名和时间戳打印到跟踪环形缓冲区,(详见 "输出格式" 一节)。打开的文件名作为 do_sys_open call 的第二个参数传递,可以从代表函数入口的 CPU 寄存器的上下文结构中访问。#include <uapi/linux/bpf.h> #include <uapi/linux/ptrace.h> #include <bpf/bpf_helpers.h> SEC("kprobe/do_sys_open") int kprobe__do_sys_open(struct pt_regs *ctx) {char file_name[256]; bpf_probe_read(file_name, sizeof(file_name), PT_REGS_PARM2(ctx)); char fmt[] = "file %s\n"; bpf_trace_printk(fmt, sizeof(fmt), &file_name); return 0; char _license[] SEC("license") = "GPL"; __u32 _version SEC("version") = 0xFFFFFFFE;在上面的代码中,我们定义了特定的 "SEC" 区域,这样 gobpf 加载器就可获取到哪里查找或加载内容的信息。在我们的例子中,区域为 kprobe、license 和 version。特殊的 0xFFFFFFFE 值告诉加载器,这个 eBPF 程序与任何内核版本都是兼容的,因为打开系统调用而破坏用户空间的机会接近于 0。Makefile:这是上述两个文件的构建逻辑。注意我们是如何在 include 路径中加入 "arch/x86/..." 的;在 ARM 上它将是 "arch/arm/..."。SHELL=/bin/bash -o pipefail LINUX_SRC_ROOT="/home/adi/workspace/linux" FILENAME="open-example" ebpf-build: clean go-build clang \ -D__KERNEL__ -fno-stack-protector -Wno-int-conversion \ -O2 -emit-llvm -c "src/${FILENAME}.c" \ -I ${LINUX_SRC_ROOT}/include \ -I ${LINUX_SRC_ROOT}/tools/testing/selftests \ -I ${LINUX_SRC_ROOT}/arch/x86/include \ -o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" go-build: go build -o ${FILENAME} src/${FILENAME}.go clean: rm -f ${FILENAME}*运行上述 makefile 在当前目录下产生两个新文件:open-example:这是编译后的 src/*.go 加载器。它只依赖于 libc 并且可以被复用来加载多个 eBPF ELF 文件运行多个跟踪。open-example.o:这是编译后的 eBPF 字节码,将在内核中加载。“open-example" 和 "open-example.o" ELF 二进制文件可以进一步合并成一个;加载器可以包括 eBPF 二进制文件作为资产,也可以像 tcptracer 那样在其源代码中直接存储为字节数。然而,这超出了本文的范围。运行例子显示以下输出(见 ftrace 文档 中的 "输出格式" 部分)。# (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe electron-17494 [007] ...3 163158.937350: 0: file /proc/self/maps systemd-1 [005] ...3 163160.120796: 0: file /proc/29261/cgroup emacs-596 [006] ...3 163163.501746: 0: file /home/adi/ (...)沿用我们在本系列的第 3 部分中定义的术语,我们的 eBPF 程序有以下部分组成:后端:是 open-example.o ELF 对象。它将数据写入内核跟踪环形缓冲区。加载器:这是编译过的 open-example 二进制文件,包含 gobpf/elf 加载器模块。只要它运行,数据就会被添加到跟踪缓冲区中。前端:这就是 cat /sys/kernel/debug/tracing/trace_pipe。非常 UNIX 风格。数据结构:内核跟踪环形缓冲区。现在将我们的例子交叉编译为 32 位 ARMv7。 基于你的 ARM 设备运行的内核版本:内核版本>=5.2:只需改变 makefile,就可以交叉编译与上述相同的源代码。内核版本<5.2:除了使用新的 makefile 外,还需要将 PT_REGS_PARM* 宏从 这个 patch 复制到 "受限制 C" 代码。新的 makefile 告诉 LLVM/Clang,eBPF 字节码以 ARMv7 设备为目标,使用 32 位 eBPF 虚拟机子寄存器地址模式,以便虚拟机可以正确访问本地处理器提供的 32 位寻址内存(还记得第 2 部分中介绍的所有 eBPF 虚拟机寄存器默认为 64 位宽),设置适当的包含路径,然后指示 Go 编译器使用正确的交叉编译设置。在运行这个 makefile 之前,需要一个预先存在的交叉编译器工具链,它被指向 CC 变量。SHELL=/bin/bash -o pipefail LINUX_SRC_ROOT="/home/adi/workspace/linux" FILENAME="open-example" ebpf-build: clean go-build clang \ --target=armv7a-linux-gnueabihf \ -D__KERNEL__ -fno-stack-protector -Wno-int-conversion \ -O2 -emit-llvm -c "src/${FILENAME}.c" \ -I ${LINUX_SRC_ROOT}/include \ -I ${LINUX_SRC_ROOT}/tools/testing/selftests \ -I ${LINUX_SRC_ROOT}/arch/arm/include \ -o - | llc -march=bpf -filetype=obj -o "${FILENAME}.o" go-build: GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \ go build -o ${FILENAME} src/${FILENAME}.go clean: rm -f ${FILENAME}*运行新的 makefile,并验证产生的二进制文件已经被正确地交叉编译:[adi@iwork]$ file open-example* open-example: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter (...), stripped open-example.o: ELF 64-bit LSB relocatable, *unknown arch 0xf7* version 1 (SYSV), not stripped然后将加载器和字节码复制到设备上,与在 x86_64 主机上使用上述相同的命令来运行。记住,只要修改和重新编译 C eBPF 代码,加载器就可以重复使用,用于运行不同的跟踪。[root@ionelpi adi]# (./open-example open-example.o &) && cat /sys/kernel/debug/tracing/trace_pipe ls-380 [001] d..2 203.410986: 0: file /etc/ld-musl-armhf.path ls-380 [001] d..2 203.411064: 0: file /usr/lib/libcap.so.2 ls-380 [001] d..2 203.411922: 0: file / zcat-397 [002] d..2 432.676010: 0: file /etc/ld-musl-armhf.path zcat-397 [002] d..2 432.676237: 0: file /usr/lib/libtinfo.so.5 zcat-397 [002] d..2 432.679431: 0: file /usr/bin/zcat gzip-397 [002] d..2 432.693428: 0: file /proc/ gzip-397 [002] d..2 432.693633: 0: file config.gz由于加载器和字节码加起来只有 2M 大小,这是一个在嵌入式设备上运行 eBPF 的相当好的方法,而不需要完全安装 BCC/LLVM。6. 总结在本系列的第 4 部分,我们研究了可以用于在小型嵌入式设备上运行 eBPF 程序的相关项目。不幸的是,当前使用这些项目还是比较很困难的:它们有的被遗弃或缺乏人力,在早期开发时一切都在变化,或缺乏基本的文档,需要用户深入到源代码中并自己想办法解决。正如我们所看到的,gobpf 项目作为 BCC/python 的替代品是最有活力的,而 ply 也是一个有前途的 BPFtrace 替代品,其占用空间最小。随着更多的工作投入到这些项目中以降低使用者的门槛,eBPF 的强大功能可以用于资源受限的嵌入式设备,而无需移植/安装整个 BCC/LLVM/python/Hover 技术栈。

【译】eBPF 概述:第 3 部分:软件开发生态

1. 前言在本系列的第 1 部分和第 2 部分中,我们对 eBPF 虚拟机进行了简洁的深入研究。阅读上述部分并不是理解第 3 部分的必修课,尽管很好地掌握了低级别的基础知识确实有助于更好地理解高级别的工具。为了理解这些工具是如何工作的,我们先定义一下 eBPF 程序的高层次组件:后端:这是在内核中加载和运行的 eBPF 字节码。它将数据写入内核 map 和环形缓冲区的数据结构中。加载器:它将字节码后端加载到内核中。通常情况下,当加载器进程终止时,字节码会被内核自动卸载。前端:从数据结构中读取数据(由后端写入)并将其显示给用户。数据结构:这些是后端和前端之间的通信手段。它们是由内核管理的 map 和环形缓冲区,可以通过文件描述符访问,并需要在后端被加载之前创建。它们会持续存在,直到没有更多的后端或前端进行读写操作。在第 1 部分和第 2 部分研究的 sock_example.c 中,所有的组件都被放置在一个 C 文件中,所有的动作都由用户进程完成。第 40-45 行创建 map数据结构。第 47-61 行定义后端。第 63-76 行在内核中加载后端第 78-91 行是前端,负责将从 map 文件描述符中读取的数据打印给用户。eBPF 程序可以更加复杂:多个后端可以由一个(或单独的多个!)加载器进程加载,写入多个数据结构,然后由多个前端进程读取,所有这些都可以发生在一个跨越多个进程的用户 eBPF 应用程序中。2. 层级 1:容易编写的后端:LLVM eBPF 编译器我们在前面的文章中看到,在内核中编写原始的 eBPF 字节码是不仅困难而且低效,这非常像用处理器的汇编语言编写程序,所以很自然地开发了一个能够将 LLVM 中间表示编译成 eBPF 程序的模块,并从 2015 年的 v3.7 开始发布(GCC 到现在为止仍然不支持 eBPF)。这使得多种高级语言如 C、Go 或 Rust 的子集可以被编译到 eBPF。最成熟和最流行的是基于 C 语言编写的方式,因为内核也是用 C 写的,这样就更容易复用现有的内核头文件。LLVM 将 "受限制的 C" 语言(记住,没有无界循环,最大 4096 条指令等等,见第 1 部分开始)编译成 ELF 对象文件,其中包含特殊区块(section),并可基于 bpf()系统调用,使用 libbpf 等库加载到内核中。这种设计有效地将后端定义从加载器和前端中分离出来,因为 eBPF 字节码包含在 ELF 文件中。内核还在 samples/bpf/ 下提供了使用这种模式的例子:*_kern.c 文件被编译为 *_kern.o(后端代码),被 *_user.c(装载器和前端)加载。将本系列第 1 和第 2 部分的 sock_exapmle.c 原始字节码 转换为 "受限的 C" 代码“ sockex1_kern.c,这比原始字节码更容易理解和修改。#include <uapi/linux/bpf.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include "bpf_helpers.h" struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(u32), .value_size = sizeof(long), .max_entries = 256, SEC("socket1") int bpf_prog1(struct __sk_buff *skb) {int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; value = bpf_map_lookup_elem(&my_map, &index); if (value) __sync_fetch_and_add(value, skb->len); return 0; char _license[] SEC("license") = "GPL";产生的 eBPF ELF 对象 sockex1_kern.o,包含了分离的后端和数据结构定义。加载器和前端sockex1_user.c,用于解析 ELF 文件、创建所需的 map 和加载字节码中内核函数 bpf_prog1(),然后前端像以前一样继续运行。引入这个 "受限的 C" 抽象层所做的权衡是使 eBPF后端代码更容易用高级语言编写,代价是增加加载器的复杂性(现在需要解析 ELF 对象),而前端大部分不受影响。3. 层级 2:自动化后端/加载器/前端的交互:BPF 编译器集合(BCC)并不是每个人手头都有内核源码,特别是在生产中,而且一般来说,将基于 eBPF 工具与特定的内核源码版本捆绑在一起并不是一个好主意。设计和实现 eBPF 程序的后端,前端,加载器和数据结构之间的相互作用可能是非常复杂,这也比较容易出错和耗时(特别是在 C 语言中),这被认为是一种危险的低级语言。除了这些风险之外,开发人员还经常为常见问题重新造轮子,会造成无尽的设计变化和实现。为了减轻这些痛苦,社区创建了 BCC 项目:其为编写、加载和运行 eBPF 程序提供了一个易于使用的框架,除了上面举例的 "限制性 C" 之外,还可以通过编写简单的 python 或 lua 脚本来实现。BCC 项目有两个部分。编译器集合(BCC 本身):这是用于编写 BCC 工具的框架,也是我们文章的重点。请继续阅读。BCC-tools:这是一个不断增长的基于 eBPF 且经过测试的程序集,提供了使用的例子和手册。更多信息见本教程。BCC 的安装包很大:它依赖于 LLVM/clang 将 "受限的 C"、python/lua 等编译成 eBPF,它还包含像 libbcc(用 C++ 编写)、libbpf 等库实现【译者注:原文 python/lua 顺序有错,另外 libcc 是 BCC 项目,libbpf 目前已经是内核代码一部分】。部分内核代码的也被复制到 BCC 代码中,所以它不需要基于完整的内核源(只需要头文件)进行构建。它可以很容易地占用数百 MB 的空间,这对于小型嵌入式设备来说不友好,我们希望这些设备也可以从 eBPF 的力量中受益。探索嵌入式设备由于大小限制问题的解决方案,将是我们在第 4 部分的重点。eBPF 程序组件在 BCC 组织方式如下:后端和数据结构:用 "限制性 C" 编写。可以在单独的文件中,或直接作为多行字符串存储在加载器/前端的脚本中,以方便使用。参见:语言参考。【译者注:在 BCC 实现中,后端代码采用面向对象的做法,真正生成字节码的时候,BCC 会进行一次预处理,转换成真正的 C 语言代码方式,这也包括 map 等数据结构的定义方面】。加载器和前端:可用非常简单的高级 python/lua 脚本编写。参见:语言参考。因为 BCC 的主要目的是简化 eBPF 程序的编写,因此它尽可能地标准化和自动化:在后台完全自动化地通过 LLVM 编译 "受限的 C"后端,并产生一个标准的 ELF 对象格式类型,这种方式允许加载器对所有 BCC 程序只实现一次,并将其减少到最小的 API(2 行 python)。它还将数据结构的 API 标准化,以便于通过前端访问。简而言之,它将开发者的注意力集中在编写前端上,而不必担心较低层次的细节问题。为了最好地说明它是如何工作的,我们来看一个简单的具体例子,它是对前面文章中的 sock_example.c 的重新实现。该程序统计回环接口上收到了 TCP、UDP 和 ICMP 数据包的数量。与此前直接用 C 语言编写的方式不同,用 BCC 实现具有以下优势:忘掉原始字节码:你可以用更方便的 "限制性 C" 编写所有后端。不需要维护任何 LLVM 的 "限制性 C" 构建逻辑。代码被 BCC 在脚本执行时直接编译和加载。没有危险的 C 代码:对于编写前端和加载器来说,Python 是一种更安全的语言,不会出现像空解引用(null dereferences)的错误。代码更简洁,你可以专注于应用程序的逻辑,而不是具体的机器问题。脚本可以被复制并在任何地方运行(假设已经安装了 BCC),它不会被束缚在内核的源代码目录中。等等。在上面的例子中,我们使用了 BPF.SOCKET_FILTER 程序类型,其结果是我们挂载的 C 函数得到一个网络数据包缓冲区作为 context 上下文参数【译者注:本例中为 struct _sk_buff *skb】。我们还可以使用 BPF.KPROBE 程序类型来探测任意的内核函数。我们继续优化,不再使用与上面相同的接口,而是使用一个特殊的 kprobe_* 函数名称前缀,以描述一个更高级别的 BCC API。这个例子来自于 bcc/examples/tracing/bitehist.py。它通过挂载在 blk_account_io_completion() 内核函数来打印一个 I/O 块大小的直方图。请注意:eBPF 的加载是根据 kprobe__blk_account_io_completion() 函数的名称自动发生的(加载器隐含实现)! 【译者注:kprobe__ 前缀会被 BCC 编译代码过程中自动识别并转换成对应的附加函数调用】从用 libbpf 在 C 语言中编写和加载字节码以来,我们已经走了很远。4. 层级 3:Python 太低级了:BPFftrace在某些用例中,BCC 仍然过于底层,例如在事件响应中检查系统时,时间至关重要,需要快速做出决定,而编写 python/"限制性 C" 会花费太多时间,因此 BPFtrace 建立在 BCC 之上,通过特定领域语言(受 AWK 和 C 启发)提供更高级别的抽象。根据声明帖,该语言类似于 DTrace 语言实现,也被称为 DTrace 2.0,并提供了良好的介绍和例子。BPFtrace 在一个强大而安全(但与 BCC 相比仍有局限性)的语言中抽象出如此多的逻辑,是非常让人惊奇的。这个单行 shell 程序统计了每个用户进程系统调用的次数(访问内置变量、map 函数 和count()文档获取更多信息)。bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}'BPFtrace 在某些方面仍然是一个正在进行的工作。例如,目前还没有简单的方法来定义和运行一个套接字过滤器来实现像我们之前所列举的 sock_example 这样的工具。它可能通过在 BPFtrace 中用 kprobe:netif_receive_skb 钩子完成,但这种情况下 BCC 仍然是一个更好的套接字过滤工具。在任何情况下(即使在目前的状态下),BPFTrace 对于在寻求 BCC 的全部功能之前的快速分析/调试仍然非常有用。5. 层级 4:云环境中的 eBPF:IOVisorIOVisor 是 Linux 基金会的一个合作项目,基于本系列文章中介绍的 eBPF 虚拟机和工具。它使用了一些非常高层次的热门概念,如 "通用输入/输出",专注于向云/数据中心开发人员和用户提供 eBPF 技术。内核 eBPF 虚拟机成为 "IO Visor 运行时引擎"编译器后端成为 "IO Visor 编译器后端"一般的 eBPF 程序被重新命名为 "IO 模块"实现包过滤器的特定 eBPF 程序成为 "IO 数据平面模块/组件"等等。考虑到原来的名字(扩展的伯克利包过滤器),并没有代表什么意义,也许所有这些重命名都是受欢迎和有价值的,特别是如果它能使更多的行业利用 eBPF 的力量。IOVisor 项目创建了 Hover 框架,也被称为 "IO 模块管理器",它是一个管理 eBPF 程序(或 IO 模块)的用户空间后台服务程序,能够将 IO 模块推送和拉取到云端,这类似于 Docker daemon 发布/获取镜像的方式。它提供了一个 CLI,Web-REST 接口,也有一个花哨的 Web UI。Hover 的重要部分是用 Go 编写的,因此,除了正常的 BCC 依赖性外,它还依赖于 Go 的安装,这使得它体积变得很大,这并不适合我们最终在第 4 部分中的提及的小型嵌入式设备。6. 总结在这一部分,我们研究了建立在 eBPF 虚拟机之上的用户空间生态系统,以提高开发人员的工作效率和简化 eBPF 程序部署。这些工具使得使用 eBPF 非常容易,用户只需 "apt-get install bpftrace" 就可以运行单行程序,或者使用 Hover 守护程序将 eBPF 程序(IO 模块)部署到 1000 台机器上。然而,所有这些工具,尽管它们给开发者和用户提供了所有的力量,但却需要很大的磁盘空间,甚至可能无法在 32 位 ARM 系统上运行,这使得它们不是很适合小型嵌入式设备,所以这就是为什么在第 4 部分我们将探索其他项目,试图缓解运行针对嵌入式设备生态系统的 eBPF 程序。

【译】eBPF 概述:第 2 部分:机器和字节码

1. 前言我们在第 1 篇文章中 介绍了 eBPF 虚拟机,包括其有意的设计限制以及如何从用户空间进程中进行交互。如果你还没有读过这篇文章,建议你在继续之前读一下,因为没有适当的介绍,直接开始接触机器和字节码的细节是比较困难的。如果有疑问,请看第 1 部分 开头的流程图。本系列的第 2 部分对第 1 部分中研究的 eBPF 虚拟机和程序进行了更深入的探讨。掌握这些低层次的知识并不是强制性的,但可以为本系列的其他部分打下非常有用的基础,我们将在这些机制的基础上研究更高层次的工具。2. 虚拟机eBPF 是一个 RISC 寄存器机,共有11 个 64 位寄存器,一个程序计数器和 512 字节的固定大小的栈。9 个寄存器是通用读写的,1 个是只读栈指针,程序计数器是隐式的,也就是说,我们只能跳转到它的某个偏移量。VM 寄存器总是 64 位宽(即使在 32 位 ARM 处理器内核中运行!),如果最重要的 32 位被清零,则支持 32 位子寄存器寻址 - 这在第 4 部分交叉编译和在嵌入式设备上运行 eBPF 程序时非常有用。这些寄存器是:r0:存储返回值,包括函数调用和当前程序退出代码r1-r5:作为函数调用参数使用,在程序启动时,r1 包含 "上下文" 参数指针r6-r9:这些在内核函数调用之间被保留下来r10:每个 eBPF 程序 512 字节栈的只读指针在加载时提供的 eBPF程序类型决定了哪些内核函数的子集可以被调用,以及在程序启动时通过 r1 提供的 "上下文" 参数。存储在 r0 中的程序退出值的含义也由程序类型决定。每个函数调用在寄存器 r1-r5 中最多可以有 5 个参数;这适用于 ebpf 到 ebpf 的调用和内核函数调用。寄存器 r1-r5 只能存储数字或指向栈的指针(作为函数的参数),不能直接指向任意的内存。所有的内存访问必须在 eBPF 程序中使用之前首先将数据加载到 eBPF 栈。这一限制有助于 eBPF 验证器,它简化了内存模型,使其更容易进行内核检查。BPF 可访问的内核 "帮助(helper)" 函数是由内核通过类似于定义 syscalls 的 API 定义的(不能通过模块扩展),定义使用 BPF_CALL_* 宏。bpf.h试图为所有 BPF 可访问的内核辅助函数提供参考。例如,bpf_trace_printk的定义使用了 BPF_CALL_5 和 5 对类型 / 参数名称。定义 参数数据类型 是非常重要的,因为在每次 eBPF 程序加载时,eBPF 验证器会确保寄存器的数据类型与被调用者的参数类型相符。eBPF 指令也是固定大小的 64 位编码,目前大约有 100 条指令,被分组为8 类。该虚拟机支持从通用内存(map、栈、如数据包缓冲区等的 "上下文",)进行 1-8 字节的加载 / 存储,前 / 后(非)条件跳转、算术 / 逻辑操作和函数调用。操作码格式格式深入研究的文档,请参考 Cilium 项目指令集文档。IOVisor 项目也维护了一个有用的指令规格。在本系列第 1 部分研究的例子中,我们使用了部分有用的 内核宏,使用以下结构 创建了一个 eBPF 字节码指令数组(所有指令都是这样编码的):struct bpf_insn { __u8 code; /* opcode */ __u8 dst_reg:4; /* dest register */ __u8 src_reg:4; /* source register */ __s16 off; /* signed offset */ __s32 imm; /* signed immediate constant */ msb lsb +------------------------+----------------+----+----+--------+ |immediate |offset |src |dst |opcode | +------------------------+----------------+----+----+--------+让我们看看 BPF_JMP_IMM 指令,它编码了一个针对立即值的条件跳转。下面的宏注释对指令的逻辑应该是不言自明的。操作码编码了指令类别 BPF_JMP,操作(通过 BPF_OP 位域以确保核心性)和一个标志,表示它是对即期 / 常量值的操作,BPF_K。#define BPF_OP(code) ((code) & 0xf0) #define BPF_K 0x00 /* Conditional jumps against immediates, if (dst_reg 'op' imm32) goto pc + off16 */ #define BPF_JMP_IMM(OP, DST, IMM, OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP | BPF_OP(OP) | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = OFF, \ .imm = IMM })如果我们去计算该指令的值,或者拆解一个包含 BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2)的 eBPF 字节码,我们会发现它是 0x020015。这个特定的字节码非常频繁地被用来测试存储在 r0 中的函数调用的返回值;如果 r0 == 0,它就会跳过接下来的 2 条指令。3. 重新认识字节码现在我们已经有了必要的知识来完全理解本系列第 1 部分中 eBPF 例子中使用的字节码,现在我们将一步一步地进行详解。记住,sock_example.c是一个简单的用户空间程序,使用 eBPF 来统计回环接口上收到多少个 TCP、UDP 和 ICMP 协议包。在更高层次上,代码所做的是从接收到的数据包中读取协议号,然后把它推到 eBPF 栈中,作为 map_lookup_elem 调用的索引,从而得到各自协议的数据包计数。map_lookup_elem 函数在 r0 接收一个索引(或键)指针,在 r1 接收一个 map 文件描述符。如果查找调用成功,r0 将包含一个指向存储在协议索引的 map 值的指针。然后我们原子式地增加 map 值并退出。BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),当一个 eBPF 程序启动时,r1 中的地址指向 context 上下文(当前情况下为数据包缓冲区)。r1 将在函数调用时用于参数,所以我们也将其存储在 r6 中作为备份。BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */),这条指令从 context 上下文缓冲区的偏移量向 r0 加载一个字节(BPF_B),当前情况下是网络数据包缓冲区,所以我们从一个 iphdr 结构 中提供协议字节的偏移量,以加载到 r0。BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */将包含先前读取的协议的字(BPF_W)加载到栈上(由 r10 指出,从偏移量 -4 字节开始)。BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */将栈地址指针移至 r2 并减去 4,所以现在 r2 指向协议值,作为下一个 map 键查找的参数。BPF_LD_MAP_FD(BPF_REG_1, map_fd),将本地进程中的文件描述符引用包含协议包计数的 map 加载到 r1。BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),执行 map 查找调用,将栈中由 r2 指向的协议值作为 key。结果存储在 r0 中:一个指向由 key 索引的值的指针地址。BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),还记得 0x020015 吗?这和第一节的字节码是一样的。如果 map 查找没有成功,r0 == 0,所以我们跳过下面两条指令。BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */递增 r0 所指向的地址的 map 值。BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ BPF_EXIT_INSN(),将 eBPF 的 retcode 设置为 0 并退出。尽管这个 sock_example 逻辑是非常简单(它只是在一个映射中增加一些数字),但在原始字节码中实现或理解它也是很难做到的。更加复杂的任务在像这样的汇编程序中完成会变得非常困难。展望未来,我们将准备使用更高级别的语言和工具来实现更强大的 eBPF 用例,而不费吹灰之力。4. 总结在这一部分中,我们仔细观察了 eBPF 虚拟机的寄存器和指令集,了解了 eBPF 可访问的内核函数是如何从字节码中调用的,以及它们是如何被核心内核通过类似 syscall 的特殊目的 API 定义的。我们也完全理解了第 1 部分例子中使用的字节码。还有一些未探索的领域,如创建多个 eBPF 程序函数或链式 eBPF 程序以绕过 Linux 发行版的 4096 条指令限制。也许我们会在以后的文章中探讨这些。现在,主要的问题是编写原始字节码是很困难的,这非常像编写汇编代码,而且编写效果不高。在第 3 部分中,我们将开始研究使用高级语言编译成 eBPF 字节码,到此为止我们已经了解了虚拟机工作的底层基础知识。

【译】eBPF 概述:第 1 部分:介绍

1. 前言有兴趣了解更多关于 eBPF 技术的底层细节?那么请继续移步,我们将深入研究 eBPF 的底层细节,从其虚拟机机制和工具,到在远程资源受限的嵌入式设备上运行跟踪。注意:本系列博客文章将集中在 eBPF 技术,因此对于我们来讲,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/缩写已经没有太大的意义,因为这个项目的发展远远超出了它最初的范围。BPF 和 eBPF 在该系列中会交替使用。第 1 部分和第 2 部分 为新人或那些希望通过深入了解 eBPF 技术栈的底层技术来进一步了解 eBPF 技术的人提供了深入介绍。第 3 部分是对用户空间工具的概述,旨在提高生产力,建立在第 1 部分和第 2 部分中介绍的底层虚拟机机制之上。第 4 部分侧重于在资源有限的嵌入式系统上运行 eBPF 程序,在嵌入式系统中完整的工具链技术栈(BCC/LLVM/python 等)是不可行的。我们将使用占用资源较小的嵌入式工具在 32 位 ARM 上交叉编译和运行 eBPF 程序。只对该部分感兴趣的读者可选择跳过其他部分。第 5 部分是关于用户空间追踪。到目前为止,我们的努力都集中在内核追踪上,所以是时候我们关注一下用户进程了。如有疑问时,可使用该流程图:2. eBPF 是什么?eBPF 是一个基于寄存器的虚拟机,使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 "BPF 程序",并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(LTTng 或 SystemTap),而且几乎所有的 Linux 发行版都默认启用。熟悉 DTrace 的读者可能会发现 DTrace/BPFtrace 对比非常有用。在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 eBPF 参考中找到。基于设计,eBPF 虚拟机和其程序有意地设计为不是图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、"主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。译者注: BPF_MAXINSNS 这个限制已经被放宽至 100 万条指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特权执行的 BPF 程序这个限制仍然会保留。历史上,eBPF (cBPF) 虚拟机只在内核中可用,用于过滤网络数据包,与用户空间程序没有交互,因此被称为 "伯克利数据包过滤器"(译者注:早期的 BPF 实现被称为经典 cBPF)。从内核 v3.18(2014 年)开始,该虚拟机也通过 bpf() syscall 和uapi/linux/bpf.h 暴露在用户空间,这导致其指令集在当时被冻结,成为公共 ABI,尽管后来仍然可以(并且已经)添加新指令。因为内核内的 eBPF 实现是根据 GPLv2 授权的,它不能轻易地被非 GPL 用户重新分发,所以也有一个替代的 Apache 授权的用户空间 eBPF 虚拟机实现,称为 "uBPF"。撇开法律条文不谈,基于用户空间的实现对于追踪那些需要避免内核-用户空间上下文切换成本的性能关键型应用很有用。3. eBPF 是怎么工作的?eBPF 程序在事件触发时由内核运行,所以可以被看作是一种函数挂钩或事件驱动的编程形式。从用户空间运行按需 eBPF 程序的价值较小,因为所有的按需用户调用已经通过正常的非 VM 内核 API 调用("syscalls")来处理,这里 VM 字节码带来的价值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等产生。这允许在内核和用户进程的指令中钩住(hook)和检查任何函数的内存、拦截文件操作、检查特定的网络数据包等等。一个比较好的参考是 Linux 内核版本对应的 BPF 功能。如前所述,事件触发了附加的 eBPF 程序的执行,后续可以将信息保存至 map 和环形缓冲区(ringbuffer)或调用一些特定 API 定义的内核函数的子集。一个 eBPF 程序可以链接到多个事件,不同的 eBPF 程序也可以访问相同的 map 以共享数据。一个被称为 "program array" 的特殊读/写 map 存储了对通过 bpf() 系统调用加载的其他 eBPF 程序的引用,在该 map 中成功的查找则会触发一个跳转,而且并不返回到原来的 eBPF 程序。这种 eBPF 嵌套也有限制,以避免无限的递归循环。运行 eBPF 程序的步骤:用户空间将字节码和程序类型一起发送到内核,程序类型决定了可以访问的内核区域(译者注:主要是 BPF 辅助函数的各种子集)。内核在字节码上运行验证器,以确保程序可以安全运行(kernel/bpf/verifier.c)。内核将字节码编译为本地代码,并将其插入(或附加到)指定的代码位置。(译者注:如果启用了 JIT 功能,字节码编译为本地代码)。插入的代码将数据写入环形缓冲区或通用键值 map。用户空间从共享 map 或环形缓冲区中读取结果值。map 和环形缓冲区结构是由内核管理的(就像管道和 FIFO 一样),独立于挂载的 eBPF 或访问它们的用户程序。对 map 和环形缓冲区结构的访问是异步的,通过文件描述符和引用计数实现,可确保只要有至少一个程序还在访问,结构就能够存在。加载的 JIT 后代码通常在加载其的用户进程终止时被删除,尽管在某些情况下,它仍然可以在加载进程的生命期之后继续存在。为了方便编写 eBPF 程序和避免进行原始的 bpf()系统调用,内核提供了方便的 libbpf 库,包含系统调用函数包装器,如bpf_load_program 和结构定义(如 bpf_map),在 LGPL 2.1 和 BSD 2-Clause 下双重许可,可以静态链接或作为 DSO。内核代码也提供了一些使用 libbpf 简洁的例子,位于目录 samples/bpf/ 中。4. 样例学习内核开发者非常可怜,因为内核是一个独立的项目,因而没有用户空间诸如 Glibc、LLVM、JavaScript 和 WebAssembly 诸如此类的好东西! - 这就是为什么内核中 eBPF 例子中会包含原始字节码或通过 libbpf 加载预组装的字节码文件。我们可以在 sock_example.c 中看到这一点,这是一个简单的用户空间程序,使用 eBPF 来计算环回接口上统计接收到 TCP、UDP 和 ICMP 协议包的数量。我们跳过微不足道的的 main 和 open_raw_sock 函数,而专注于神奇的代码 test_sock。static int test_sock(void) int sock = -1, map_fd, prog_fd, i, key; long long value = 0, tcp_cnt, udp_cnt, icmp_cnt; map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256, 0); if (map_fd < 0) {printf("failed to create map'%s'\n", strerror(errno)); goto cleanup; struct bpf_insn prog[] = {BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ BPF_LD_MAP_FD(BPF_REG_1, map_fd), BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ BPF_EXIT_INSN(),}; size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn); prog_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog, insns_cnt, "GPL", 0, bpf_log_buf, BPF_LOG_BUF_SIZE); if (prog_fd < 0) {printf("failed to load prog'%s'\n", strerror(errno)); goto cleanup; sock = open_raw_sock("lo"); if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {printf("setsockopt %s\n", strerror(errno)); goto cleanup; }首先,通过 libbpf API 创建一个 BPF map,该行为就像一个最大 256 个元素的固定大小的数组。按 IPROTO_* 定义的键索引网络协议(2 字节的 word),值代表各自的数据包计数(4 字节大小)。除了数组,eBPF 映射还实现了其他数据结构类型,如栈或队列。接下来,eBPF 的字节码指令数组使用方便的内核宏进行定义。在这里,我们不会讨论字节码的细节(这将在第 2 部分描述机器后进行)。更高的层次上,字节码从数据包缓冲区中读取协议字,在 map 中查找,并增加特定的数据包计数。然后 BPF 字节码被加载到内核中,并通过 libbpf 的 bpf_load_program 返回 fd 引用来验证正确/安全。调用指定了 eBPF 是什么程序类型,这决定了它可以访问哪些内核子集。因为样例是一个 SOCKET_FILTER 类型,因此提供了一个指向当前网络包的参数。最后,eBPF 的字节码通过套接字层被附加到一个特定的原始套接字上,之后在原始套接字上接受到的每一个数据包运行 eBPF 字节码,无论协议如何。剩余的工作就是让用户进程开始轮询共享 map 的数据。for (i = 0; i < 10; i++) { key = IPPROTO_TCP; assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); key = IPPROTO_UDP; assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0); key = IPPROTO_ICMP; assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0); printf("TCP %lld UDP %lld ICMP %lld packets\n", tcp_cnt, udp_cnt, icmp_cnt); sleep(1); }5. 总结第 1 部分介绍了 eBPF 的基础知识,我们通过如何加载字节码和与 eBPF 虚拟机通信的例子进行了讲述。由于篇幅限制,编译和运行例子作为留给读者的练习。我们也有意不去分析具体的 eBPF 字节码指令,因为这将是第 2 部分的重点。在我们研究的例子中,用户空间通过 libbpf 直接用 C 语言从内核虚拟机中读取 eBPF map 值(使用 10 次 1 秒的睡眠!),这很笨重,而且容易出错,而且很快就会变得很复杂,所以在第 3 部分,我们将研究更高级别的工具,通过脚本或特定领域的语言自动与虚拟机交互。

VirtualBox VM 空间瘦身记(vmdk)

本文地址:https://www.ebpf.top/post/shrink_vbox_vmdk_size在使用 VirtualBox( VMDK 模式)管理虚拟机的时候,我们经常会遇到一些编译安装场景(比如编译 Linux 内核),会导致磁盘空间急剧膨胀,但是在编译完成后即使我们删除了相关的文件,在 VM 虚拟机占用主机的空间却并没有减少,这时候为了腾出磁盘空间或者更方便与他人分享,我们需要给 VM 的磁盘进行瘦身操作。1.1 虚拟磁盘格式介绍VirtualBox 主要支持下列虚拟磁盘格式为 VMDK 和 VDI:VMDK(Virtual Machine Disk) 最初是由 VMware 为其产品研发的格式。该格式技术设计文档最初是闭源的,而现在已经开源,在 VirtualBox 里完全可用。这种格式有个功能是:把一个虚拟机的镜像分割成多个 2GB 大小的文件。如果你要把虚拟机镜像放在不支持大文件的文件系统(例如 FAT32)上,那么这个功能就非常有用。在其他的虚拟磁盘格式里,能做到同样功能的只有 Parallels 的 HDD。VDI(Virtual Disk Image) 格式是 VirtualBox 新建虚拟机时默认选用的格式。也是 VirtualBox 的自有开放格式。VirtualBox 支持的虚拟磁盘格式还有 VHDX 和 HDD 等多种格式,详细信息请参考 VirtualBox 简体中文 。1.2 用零字节填充空闲空间VirtualBox 只有在空间被设置为零的情况下才知道这是磁盘中真正的空闲空间,这与我们在一般机器上通过标准的 rm 命令删除即可释放空间有很大不同。为了实现这个效果,我们需要登录到 VM 主机中登录到虚拟机中,使用零字节空间填充掉空闲空间,然后再把填充的文件进行删除,即可达到效果。$ cat /dev/zero > zero.fill; sync; sleep 1; sync; rm -f zero.fill cat: write error: No space left on device在命令执行完成后,会出先一个 “cat: write error: No space left on device” 的错误,这个错误恰恰表明我们使用零字节填充了所有的空闲空间。至此,我们已经在 VM 虚拟机中成功地将空闲的空间进行了零字节填充,是时候进行真正的 “ 减肥 ” 操作了。1.3 定位 VM 虚拟磁盘文件在 VirtualBox 运行的主界面上,我们可以通过在虚拟机上点击右键,在弹出的菜单上选择 “Setting“ 选项,会弹出本虚拟相关的设置,切换到 ”Storage“ 选项卡。 图 1-1 进入 VM 的设置页面在 ”Storage“ 选项卡的主界面中我们可以看到 VM 挂载的虚拟磁盘,点击虚拟磁盘选项,在右侧的 ”Attributes“ 信息栏中就可以在 ”Location“ 项中查询到选择虚拟磁盘所在的目录和文件名。目录默认保存位置为 ~/VirtualBox VMs/ 目录下以 VM 名称命名的子目录下,如本例中的 ~/VirtualBox VMs/ubuntu_21_04_default_1632463892989_42055,其中 ubuntu_21_04_default_1632463892989_42055 为 VM 主机名。 图 1-2 进入 VM 的设置页面中的存储项详情确定 VM 的虚拟磁盘所在目录后,我们通过终端进入到对应的目录,进行查看:$ cd ~/VirtualBox\ VMs/ubuntu_21_04_default_1632463892989_42055/ $ ls -lh -rw------- 1 dwh0403 staff 35G Sep 28 13:37 ubuntu-hirsute-21.04-cloudimg.vmdk ...这里我们可以看到该该虚拟磁盘占用了 35G 的磁盘大小。我们可以通过 vboxmanage showhdinfo 命令查看 vmdk 文件的详情(如果后续需要继续使用 vmdk 格式需要):$ vboxmanage showhdinfo ubuntu-hirsute-21.04-cloudimg.vmdk UUID: 6a00f1e1-a53f-4a48-9f41-4f2a96248286 Parent UUID: base State: created Type: normal (base) Location: /Users/dwh0403/VirtualBox VMs/ubuntu_21_04_default_1632463892989_42055/ubuntu-hirsute-21.04-cloudimg.vmdk Storage format: VMDK Format variant: dynamic default Capacity: 40960 MBytes Size on disk: 35717 MBytes Encryption: disabled为了压缩虚拟磁盘的空间,我们需要将 vmdk 格式转换成 vdi 格式。如果本机安装了 Vmware 产品,可以直接使用其提供的工具直接进行瘦身,参见 Vmware 磁盘管理样例 。$ vboxmanage clonehd --format vdi ubuntu-hirsute-21.04-cloudimg.vmdk ubuntu-hirsute-21.04-cloudimg.vdi 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Clone medium created in format 'vdi'. UUID: 46de9fce-0055-472b-aee2-128509e3685 $ ls -hl -rw------- 1 dwh0403 staff 11G Sep 28 13:41 ubuntu-hirsute-21.04-cloudimg.vdi $ vboxmanage modifyhd ubuntu-hirsute-21.04-cloudimg.vdi --compact 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%待转换完成后,我们可以在当前目录进行查看可以发现 vdi 的文件大小已经降低至 11G(原始 vmdk 文件为 35G 大小),表明在转换过程中已经完成了磁盘空间的缩容。1.4 将 VDI 格式的磁盘挂载(方案一,验证,推荐)在转换 vdi 格式后,已经完成了空间的调整,如果我们并去强烈使用 vmdk 格式,我们可以直接将原来的 vmdk 格式虚拟磁盘从 VM 中卸载,然后将 vdi 格式的磁盘挂载即可。同时记得删除 vmdk 格式的虚拟磁盘。在保持 VM 虚拟机关闭的情况下,进入到 VM 的存储设置页面,步骤与图 1-2 一致。首先,在移除老的 vmdk 格式的虚拟磁盘上点击右键,在右键菜单属性中选择 ”Remove Attachment“:然后鼠标选择磁盘控制器,选择添加磁盘按钮:在弹出的添加磁盘文件的窗口中选择 ”Add“ 按钮,进入到选择文件窗口,选择我们新的 vdi 格式文件即可。然后将 VM 虚拟机启动验证,如果一切顺利则完成了整个瘦身过程。这里推荐使用 vdi 格式的虚拟磁盘格式,后续在磁盘空间吃紧的情况还可以使用下述命令调整大小:$ VBoxManage modifyhd xxx.vdi --resize the_new_size1.5 使用 VMDK 格式的磁盘挂载(方案二,未验证)如果由于特殊原因必须使用 vmdk 格式的虚拟磁盘,我们需要将瘦身后的 vdi 格式文件重新转换为 vmdk 格式:$ VBoxManage clonehd ubuntu-hirsute-21.04-cloudimg.vdi ubuntu-hirsute-21.04-cloudimg_new.vmdk --format vmdk这里可以选择如上述方案相同的方式,通过去除虚拟磁盘再添加新的磁盘,如果使用原有的文件名字覆盖的话,由于转换过程中生成了新的 UUID,则会导致 VirtualBox 不能够识别新的虚拟磁盘,这里需要重新设置 UUID。$ vboxmanage internalcommands sethduuid ./ubuntu-hirsute-21.04-cloudimg <原 UUID 在此>1.5.1 错误解决:$ VBoxManage clonehd ubuntu_21_04_default_1632463892989_42055/ubuntu-hirsute-21.04-cloudimg.vdi ubuntu-hirsute-21.04-cloudimg.vmdk --format vmdk VBoxManage: error: UUID {6438d068-ae7b-467d-ab30-6e1228c30bd9} of the medium '/Users/dwh0403/VirtualBox VMs/ubuntu_21_04_default_1632463892989_42055/ubuntu-hirsute-21.04-cloudimg.vdi' does not match the value {46de9fce-0055-472b-aee2-128509e3685d} stored in the media registry ('/Users/dwh0403/Library/VirtualBox/VirtualBox.xml') VBoxManage: error: Details: code NS_ERROR_FAILURE (0x80004005), component MediumWrap, interface IMedium, callee nsISupports VBoxManage: error: Context: "CloneTo(pDstMedium, ComSafeArrayAsInParam(l_variants), NULL, pProgress.asOutParam())" at line 1068 of file VBoxManageDisk.cpp如果有上述报错,建议修改 vmdk 生成的文件名重试。1.6 总结最后,我们可以已经成功完成了 VM 虚拟空间的瘦身,这对于我们在某些场景下进行功能测试还是非常有帮助。1.7 参考How to compact VirtualBox’s VDMK file sizeVirtualBox ( 简体中文 )VM VirtualBox® User Manual

深入浅出 BPF TCP 拥塞算法实现原理

本文地址:https://www.ebpf.top/post/ebpf_struct_ops1. 前言eBPF 的飞轮仍然在快速转动,自从 Linux 内核 5.6 版本支持 eBPF 程序修改 TCP 拥塞算法能力,可通过在用户态修改内核中拥塞函数结构指针实现;在 5.13 版本中该功能又被进一步优化,增加了该类程序类型直接调用部分内核代码的能力,这避免了在 eBPF 程序中需要重复实现内核中使用的 TCP 拥塞算法相关的函数。这两个功能的实现,为 Linux 从宏内核向智能化的微内核提供的演进,虽然当前只是聚焦在 TCP 拥塞算法的控制,但是这两个功能的实现却具有非常好的想象空间。这是因为 Linux 内核中的诸多功能都是基于结构体指针的方式,当我们具有在用户编写的 eBPF 程序完成内核结构体中函数的重定向,则可以实现内核的灵活扩展和功能的增强,再配合内核函数直接的调用能力,等同于为普通用户提供了定制内核的能力。尽管这只是 eBPF 一小步,后续却可能会成为内核生态的一大步。本文先聚焦在 5.6 版本为 TCP 拥塞算法定制而提供的 STRUCT_OPS 的能力,对于该类型 eBPF 程序调用 Linux 内核函数的能力,我们会在下一篇进行详细介绍。2. eBPF 赋能 TCP 拥塞控制算法为了支持通过 eBPF 程序可以修改 TCP 拥塞控制算法的能力,来自于 Facebook 的工程师 Martin KaFai Lau 于 2020-01-08 号提交了一个有 11 个小 Patch 组成的 提交 。实现为 eBPF 增加了 BPF_MAP_TYPE_STRUCT_OPS 新的 map 结构类型和 BPF_PROG_TYPE_STRUCT_OPS 的程序类型,当前阶段只支持对于内核中 TCP 拥塞结构 tcp_congestion_ops 的修改。 图 1 整体实现的相关结构和代码片段首先我们从如何使用样例程序入手(完整代码实现参见 这里 ),这里我们省略与功能介绍不相干的内容:SEC("struct_ops/dctcp_init") void BPF_PROG(dctcp_init, struct sock *sk) const struct tcp_sock *tp = tcp_sk(sk); struct dctcp *ca = inet_csk_ca(sk); ca->prior_rcv_nxt = tp->rcv_nxt; ca->dctcp_alpha = min(dctcp_alpha_on_init, DCTCP_MAX_ALPHA); ca->loss_cwnd = 0; ca->ce_state = 0; dctcp_reset(tp, ca); SEC("struct_ops/dctcp_ssthresh") __u32 BPF_PROG(dctcp_ssthresh, struct sock *sk) struct dctcp *ca = inet_csk_ca(sk); struct tcp_sock *tp = tcp_sk(sk); ca->loss_cwnd = tp->snd_cwnd; return max(tp->snd_cwnd - ((tp->snd_cwnd * ca->dctcp_alpha) >> 11U), 2U); // .... SEC(".struct_ops") struct tcp_congestion_ops dctcp_nouse = { .init = (void *)dctcp_init, .set_state = (void *)dctcp_state, .flags = TCP_CONG_NEEDS_ECN, .name = "bpf_dctcp_nouse", SEC(".struct_ops") struct tcp_congestion_ops dctcp = { // bpf 程序定义的结构与内核中使用的结构不一定相同 // 可为必要字段的组合 .init = (void *)dctcp_init, .in_ack_event = (void *)dctcp_update_alpha, .cwnd_event = (void *)dctcp_cwnd_event, .ssthresh = (void *)dctcp_ssthresh, .cong_avoid = (void *)tcp_reno_cong_avoid, .undo_cwnd = (void *)dctcp_cwnd_undo, .set_state = (void *)dctcp_state, .flags = TCP_CONG_NEEDS_ECN, .name = "bpf_dctcp", };这里注意到两点:tcp_congestion_ops 结构体并非内核头文件里的对应结构体,它只包含了内核对应结构体里 TCP CC 算法用到的字段,它是内核对应同名结构体的子集。有些结构体(如 tcp_sock)会看到 preserve_access_index 属性表示 eBPF 字节码在载入的时候,会对这个结构体里的字段进行重定向,满足当前内核版本的同名结构体字段的偏移。其中需要注意的是在 BPF 程序中定义的 tcp_congestion_ops 结构(也被称为 bpf-prg btf 类型),该类型可以与内核中定义的结构体完全一致(被称为 btf_vmlinux btf 类型),也可为内核结构中的部分必要字段,结构体定义的顺序可以不需内核中的结构体一致,但是名字,类型或者函数声明必须一致(比如参数和返回值)。因此可能需要从 bpf-prg btf 类型到 btf_vmlinux btf 类型的一个翻译过程,这个转换过程使用到的主要是 BTF 技术,目前主要是通过成员名称、btf 类型和大小等信息进行查找匹配,如果不匹配 libbpf 则会返回错误。整个转换过程与 Go 语言类型中的反射机制类似,主要实现在函数 bpf_map__init_kern_struct_ops 中(见原理章节详细介绍)。在 eBPF 程序中增加 section 名字声明为 .struct_ops,用于 BPF 实现中识别要实现的 struct_ops 结构,例如当前实现的 tcp_congestion_ops 结构。在 SEC(".struct_ops") 下支持同时定义多个 struct_ops 结构。每个 struct_ops 都被定义为 SEC(".struct_ops") 下的一个全局变量。libbpf 为每个变量创建了一个 map,map 的名字为定义变量的名字,本例中为 bpf_dctcp_nouse 和 dctcp。用户态完整代码参见 这里 ,生成的脚手架相关代码参见 这里 ,与 dctcp 相关的核心程序代码如下:static void test_dctcp(void) struct bpf_dctcp *dctcp_skel; struct bpf_link *link; // 脚手架生成的函数 dctcp_skel = bpf_dctcp__open_and_load(); if (CHECK(!dctcp_skel, "bpf_dctcp__open_and_load", "failed\n")) return; // bpf_map__attach_struct_ops 增加了注册一个 struct_ops map 到内核子系统 // 这里为我们上面定义的 struct tcp_congestion_ops dctcp 变量 link = bpf_map__attach_struct_ops(dctcp_skel->maps.dctcp); if (CHECK(IS_ERR(link), "bpf_map__attach_struct_ops", "err:%ld\n", PTR_ERR(link))) { bpf_dctcp__destroy(dctcp_skel); return; do_test("bpf_dctcp"); # 销毁相关的数据结构 bpf_link__destroy(link); bpf_dctcp__destroy(dctcp_skel); }详细流程解释如下:在 bpf_object__open 阶段,libbpf 将寻找 SEC(".struct_ops") 部分,并找出 struct_ops 所实现的 btf 类型。 需要注意的是,这里的 btf-type 指的是 bpf_prog.o 的 btf 中的一个类型。 "struct bpf_map" 像其他 map 类型一样, 通过 bpf_object__add_map() 进行添加。 然后 libbpf 会收集(通过 SHT_REL)bpf progs 的位置(使用 SEC("struct_ops/xyz") 定义的函数),这些位置是 func ptrs 所指向的地方。 在 open 阶段并不需要 btf_vmlinux。在 bpf_object__load 阶段,map 结构中的字段(赖于 btf_vmlinux) 通过 bpf_map__init_kern_struct_ops() 初始化。在加载阶段,libbpf 还会设置 prog->type、prog->attach_btf_id 和 prog->expected_attach_type 属性。 因此,程序的属性并不依赖于它的 section 名称。目前,bpf_prog btf-type ==> btf_vmlinux btf-type 匹配过程很简单:成员名匹配 + btf-kind 匹配 + 大小匹配。如果这些匹配条件失败,libbpf 将拒绝。目前的目标支持是 "struct tcp_congestion_ops",其中它的大部分成员都是函数指针。bpf_prog 的 btf-type 的成员排序可以不同于 btf_vmlinux 的 btf-type。然后,所有 obj->maps 像往常一样被创建(在 bpf_object__create_maps())。一旦 map 被创建,并且 prog 的属性都被设置好了,libbpf 就会继续执行。libbpf 将继续加载所有的程序。bpf_map__attach_struct_ops() 是用来注册一个 struct_ops map 到内核子系统中。关于支持 TCP 拥塞控制算法的完整 PR 代码参见 这里 。3. 脚手架代码相关实现关于生成脚手架的样例过程如下:(脚手架的提交 commit 参见 这里 ,可以在 这里 搜索相关关键词查看)。$ cd tools/bpf/runqslower && make V=1 # 整个过程如下 $ .output/sbin/bpftool btf dump file /sys/kernel/btf/vmlinux format c > .output/vmlinux.h clang -g -O2 -target bpf -I.output -I.output -I/home/vagrant/linux-5.8/tools/lib -I/home/vagrant/linux-5.8/tools/include/uapi \ -c runqslower.bpf.c -o .output/runqslower.bpf.o && \ $ llvm-strip -g .output/runqslower.bpf.o $ .output/sbin/bpftool gen skeleton .output/runqslower.bpf.o > .output/runqslower.skel.h $ cc -g -Wall -I.output -I.output -I/home/vagrant/linux-5.8/tools/lib -I/home/vagrant/linux-5.8/tools/include/uapi -c runqslower.c -o .output/runqslower.o $ cc -g -Wall .output/runqslower.o .output/libbpf.a -lelf -lz -o .output/runqslower4. bpf struct_ops 底层实现原理在上述的过程中对于用户态代码与内核中的主要实现流程已经给与了说明,如果你对内核底层实现原理不感兴趣,可以跳过该部分。4.1 内核中的 ops 结构(bpf_tcp_ca.c)如图 1 所示,为了实现该功能,需要在内核代码中提供基础能力支撑,内核中结构对应的操作对象结构(ops 结构)为 bpf_tcp_congestion_ops,定义在 /net/ipv4/bpf_tcp_ca.c 文件中,实现参见 这里 :/* Avoid sparse warning. It is only used in bpf_struct_ops.c. */ extern struct bpf_struct_ops bpf_tcp_congestion_ops; struct bpf_struct_ops bpf_tcp_congestion_ops = { .verifier_ops = &bpf_tcp_ca_verifier_ops, .reg = bpf_tcp_ca_reg, .unreg = bpf_tcp_ca_unreg, .check_member = bpf_tcp_ca_check_member, .init_member = bpf_tcp_ca_init_member, .init = bpf_tcp_ca_init, .name = "tcp_congestion_ops", };bpf_tcp_congestion_ops 结构中的各个函数说明如下:init() 函数将被首先调用,以进行任何需要的全局设置;init_member() 则验证该结构中任何字段的确切值。特别是,init_member() 可以验证非函数字段(例如,标志字段);check_member() 确定目标结构的特定成员是否允许在 BPF 中实现;reg() 函数在检查通过后实际注册了替换结构;在拥塞控制的情况下,它将把 tcp_congestion_ops 结构(带有用于函数指针的适当的 BPF 蹦床(trampolines ))安装在网络堆栈将使用它的地方;unreg() 撤销注册;verifier_ops 结构有一些函数,用于验证各个替换函数是否可以安全执行;其中 verfier_ops 结构主要用于验证器(verfier)的判断,其中定义的函数如下:static const struct bpf_verifier_ops bpf_tcp_ca_verifier_ops = { .get_func_proto = bpf_tcp_ca_get_func_proto,// 验证器使用的函数原型,用于验证是否允许在 eBPF 程序中的 // BPF_CALL 内核内的辅助函数,并在验证后调整 BPF_CALL 指令中的 imm32 域。 .is_valid_access = bpf_tcp_ca_is_valid_access, // 是否是合法的访问 .btf_struct_access = bpf_tcp_ca_btf_struct_access, // 用于判断 btf 中结构体是否可以被访问 };最后,在 kernel/bpf/bpf_struct_ops_types.h 中添加一行:BPF_STRUCT_OPS_TYPE(tcp_congestion_ops)4.2 内核 ops 对象结构定义和管理(bpf_struct_ops.c)在 bpf_struct_ops.c 文件中,通过包含 "bpf_struct_ops_types.h" 文件 4 次,并分别设置 BPF_STRUCT_OPS_TYPE 宏,实现了 map 中 value 值结构的定义和内核定义 ops 对象数组的管理功能,同时也包括对应数据结构 BTF 中的定义。/* bpf_struct_ops_##_name (e.g. bpf_struct_ops_tcp_congestion_ops) is * the map's value exposed to the userspace and its btf-type-id is * stored at the map->btf_vmlinux_value_type_id. #define BPF_STRUCT_OPS_TYPE(_name) \ extern struct bpf_struct_ops bpf_##_name; \ struct bpf_struct_ops_##_name { \ BPF_STRUCT_OPS_COMMON_VALUE; \ struct _name data ____cacheline_aligned_in_smp; \ #include "bpf_struct_ops_types.h" // ① 用于生成 bpf_struct_ops_tcp_congestion_ops 结构 #undef BPF_STRUCT_OPS_TYPE enum { #define BPF_STRUCT_OPS_TYPE(_name) BPF_STRUCT_OPS_TYPE_##_name, #include "bpf_struct_ops_types.h" // ② 生成一个 enum 成员 #undef BPF_STRUCT_OPS_TYPE __NR_BPF_STRUCT_OPS_TYPE, static struct bpf_struct_ops * const bpf_struct_ops[] = { #define BPF_STRUCT_OPS_TYPE(_name) \ [BPF_STRUCT_OPS_TYPE_##_name] = &bpf_##_name, #include "bpf_struct_ops_types.h" // ③ 生成一个数组中的成员 [BPF_STRUCT_OPS_TYPE_tcp_congestion_ops] // = &bpf_tcp_congestion_ops #undef BPF_STRUCT_OPS_TYPE void bpf_struct_ops_init(struct btf *btf, struct bpf_verifier_log *log) /* Ensure BTF type is emitted for "struct bpf_struct_ops_##_name" */ #define BPF_STRUCT_OPS_TYPE(_name) BTF_TYPE_EMIT(struct bpf_struct_ops_##_name); #include "bpf_struct_ops_types.h" // ④ BTF_TYPE_EMIT(struct bpf_struct_ops_tcp_congestion_ops btf 注册 #undef BPF_STRUCT_OPS_TYPE // ... }编译完整展开后相关的结构:extern struct bpf_struct_ops bpf_tcp_congestion_ops; struct bpf_struct_ops_tcp_congestion_ops { // ① 作为 map 类型的 value 对象存储 refcount_t refcnt; enum bpf_struct_ops_state state struct tcp_congestion_ops data ____cacheline_aligned_in_smp; // 内核中的 tcp_congestion_ops 对象 enum { BPF_STRUCT_OPS_TYPE_tcp_congestion_ops // ② 序号声明 __NR_BPF_STRUCT_OPS_TYPE, static struct bpf_struct_ops * const bpf_struct_ops[] = { // ③ 作为数组变量 // 其中 bpf_tcp_congestion_ops 即为 /net/ipv4/bpf_tcp_ca.c 文件中定义的变量(包含了各种操作的函数指针) [BPF_STRUCT_OPS_TYPE_tcp_congestion_ops] = &bpf_tcp_congestion_ops, void bpf_struct_ops_init(struct btf *btf, struct bpf_verifier_log *log) // #define BTF_TYPE_EMIT(type) ((void)(type *)0) ((void)(struct bpf_struct_ops_tcp_congestion_ops *)0); // ④ BTF 类型注册 // ... }至此内核完成了 ops 结构的类型的生成、注册和 ops 对象数组的管理。4.3 map 中内核结构值初始化该过程涉及将 bpf 程序中定义变量初始化 kernl 内核变量,该过程在 libbpf 库中的 bpf_map__init_kern_struct_ops 函数中实现。 函数原型为:/* Init the map's fields that depend on kern_btf */ static int bpf_map__init_kern_struct_ops(struct bpf_map *map, const struct btf *btf, const struct btf *kern_btf)使用 bpf 程序结构初始化 map 结构变量的主要流程如下:bpf 程序加载过程中会识别出来定义的 BPF_MAP_TYPE_STRUCT_OPS map 对象;获取到 struct ops 定义的变量类型(如 struct tcp_congestion_ops dctcp)中的 tcp_congestion_ops 类型,使用获取到 tname/type/type_id 设置到 map 结构中的 st_ops 对象中;通过上一步骤设置的 tname 属性在内核的 btf 信息表中查找内核中 tcp_congestion_ops 类型的 type_id 和 type 等信息,同时也获取到 map 对象中 value 值类型 bpf_struct_ops_tcp_congestion_ops 的 vtype_id 和 vtype 类型;至此已经拿到了 bpf 程序中定义的变量及 bpf_prog btf-type tcp_congestion_ops, 内核中定义的类型 tcp_congestion_ops 以及 map 值类型的 bpf_struct_ops_tcp_congestion_ops 等信息;接下来的事情就是通过特定的 btf 信息规则(名称、调用参数、返回类型等)将 bpf_prog btf-type 变量初始化到 bpf_struct_ops_tcp_congestion_ops 变量中,将内核中的变量初始化以后,放入到 st_ops->kern_vdata 结构中(bpf_map__attach_struct_ops() 函数会使用 st_ops->kern_vdata 更新 map 的值,map 的 key 固定为 0 值(表示第一个位置);然后设置 map 结构中的 btf_vmlinux_value_type_id 为 vtype_id 供后续检查和使用, map->btf_vmlinux_value_type_id = kern_vtype_id;5. 总结从表面上看,拥塞控制是 BPF 的一项重要的新功能,但是从底层的实现我们可以看到,这个功能的实现远比该功能更加通用,相信在不久的将来还有会更加丰富的实现,在软件中定义内核功能的实现会带给我们不一样的体验。具体来说,该基础功能可以用来让一个 BPF 程序取代内核中的任何使用函数指针的 " 操作结构 ",而且内核代码的很大一部分是通过至少一个这样的结构调用的。如果我们可以替换全部或部分 security_hook_heads 结构,我们就可以以任意的方式修改安全策略,例如类似于 KRSI 的建议。替换一个 file_operations 结构可以重新连接内核的 I/O 子系统的任何部分。现在还没有人提出要做这些事情,但是这种能力肯定会吸引感兴趣的用户。有一天,几乎所有的内核功能都可以被用户空间的 BPF 代码钩住或替换。在这样的世界里,用户将有很大的权力来改变他们系统的运行方式,但是我们认为的 "Linux 内核 " 将变得更加无定形,因为诸多功能可能会取决于哪些代码从用户空间加载。6. 参考资料Kernel operations structures in BPFIntroduce BPF STRUCT_OPS用 eBPF 写 TCP 拥塞控制算法

Typora 标题的自动编号

Typora 标题的自动编号1. 介绍工欲善其事必先利其器,最近虽然重度使用 Typora 工具,但是突然发现很多功能还未能够实现自动化,比如标题自动编号的功能。我们期望在输入标题的时候能够自动生成 1. 1.1 1.1.1 这样的序号用于展示,方便协作。实际上在 Typora 中可以通过添加样式的方式来实现,也就是说在实际的 Markdown 文档中未包含序号,而是工具在渲染的时候根据定义的 CSS 展示的时候,列出对应的标题号。2. 实现可以在样式中目录中添加一个 base.user.css 或者 [theme].user.css 的文件来实现,文件都位于样式的目录中。如果不知道样式目录,可以通过以下菜单获取:【Typora】-> 【Perferences】 -> 【Appearance】 中的 Themes 区域块中的 ”Open Theme Folder“ 定位到。实现了标题自动编号、目录自动编号、大纲自动编号的 CSS 文件参见这里。或者从参考资料中获取原始格式文件。将 base.user.css 放入到样式目录后,我们再次运行 Typora 就可以看到完整的相关实现。3. 参考资料Auto Numbering for Headings

VSCode 翻译插件一览表

1. Google Translate首推的还是 Google 翻译,插件为 Google Translate,安装如下图:扩展安装完成后,需要设置 googleTranslateExt.languages 变量,常用设置的值如下:名称ISO-639-1 编码Chinese (Simplified)zh-CN (BCP-47)Chinese (Traditional)zh-TW (BCP-47)Englishen完整的编码格式可以参考 这里。Google Translate 的使用需要能够合理上网,否则将不能正常使用。另外如果将选择翻译的文本替代选择的文本(即覆盖模式)需要设置变量:googleTranslateExt.replaceText 为 true。如果该值为 false 则会对翻译的文本通过底部信息框提示。选中文字快捷键为 Ctrl+Shift+t 。2. Yao-TranslateYao-Translate 是底层基于有道翻译实现的。安装完成后就可以直接使用:按 Cmd+Shift+T 或 Ctrl+Shift+T 对选中的文本内容快速翻译按 Cmd+Shift+R 或 Ctrl+Shift+R 对选中的文本内容快速翻译并替换成翻译结果3. Comment Translate许多优秀的项目,都有丰富的注释,使用者可以快速理解代码意图。但是如果使用者并不熟习注释的语言,会带来理解困难。本插件使用 Google Translate API 翻译 VSCode 的编程语言的注释。4. Python SDK这里与 VSCode 无关,DeepL 和 Google 都提供免费 50 万字符/每月的翻译,日常翻译的话量还是够用,这两者可以使用相关的 Python SDK 调用,利于扩展。Google 需要注册一个翻译项目,而 DeepL 需要注册为免费用户(但是当前没有针对中国开放,需要新建一个美国或其他国家的虚拟 Visa 卡才可以使用 ):Google Python SDK,SDK 文档也可以参见 这里。产品详细文档参见 这里。DeepL Python SDK,SDK 说明文档参见 这里。Google 国内翻译地址: https://translate.google.cn/参考文档翻译经验分享(Markdown)Ptyhon 如何免费调用 Google 翻译 API如何使用Python调用Google翻译API,并实现剪贴板自动翻译使用Python中的Google Translate API进行文本翻译

“XXXXX” is damaged and can’t be opened. You should move it to the Trash 解决方案

1. 打开 AnyWhere 选项MacOS 早期版本只要打开 System Preferences -> Security & Privacy 并勾选 AnyWhere 就可以了。如果新的版本 AnyWhere 没有出现,则需要执行以下命令$ sudo spctl --master-disable命令执行完成,一般都可以看到 AnyWhere 选项了。2. 使用 xattr 命令行使用 xattr 命令调整 app 属性,比如 “Sublime\ Text”$ sudo xattr -cr /Applications/Sublime\ Text.app参考:https://blog.csdn.net/zilaike/article/details/80777426https://osxdaily.com/2019/02/13/fix-app-damaged-cant-be-opened-trash-error-mac/

深入浅出 eBPF 安全项目 Tracee

原文地址: https://www.ebpf.top/post/tracee_intro/1. Tracee 介绍1.1 Tracee 介绍Tracee 是一个用 于 Linux 的运行时安全和取证工具。它使用 Linux eBPF 技术在运行时跟踪系统和应用程序,并分析收集的事件以检测可疑的行为模式。Tracee 以 Docker 镜像的形式交付,监控操作系统并根据预定义的行为模式集检测可疑行为。完整文档参见:https://aquasecurity.github.io/tracee/dev/。Tracee 由以下子项目组成:Trace-eBPF - 使用 eBPF 进行 Linux 追踪和取证,BPF 底层代码实现参见 tracee.bpf.c;Trace-Rules - 运行时安全检测引擎,在真实的使用场景中通过管道的方式从 Trace-eBPF 中接受数据,具体运行命令 如下:$TRACEE_EBPF_EXE --output=format:gob --security-alerts | $TRACEE_RULES_EXE --input-tracee=file:stdin --input-tracee=format:gob $@libbpfgo 基于 Linux libbpf 的 Go 的 eBPF 库,Tracee 程序通过 cgo 访问 libbpf C 语言库;Tracee 运行系统最低内核版本要求 >= 4.18,可以根据是否开启 CO-RE 进行 BPF 底层代码编译。运行 Tracee 需要足够的权限才能运行,测试可以直接使用 root 用户运行或者在 Docker 模型下使用 --privileged 模式运行。Tracee 在最近的版本中增加了一个非常有意思的功能抓取 --capture,可以将读写文件、内存文件、网络数据包等进行抓取并保存,该功能主要是用于取证相关功能。1.2 Tracee 与 Falco 的区别看到 Tracee 这款基于 eBPF 技术的安全产品,很自然想到的对应产品是 Falco,如果你对 Falco 不了解,那么可以参见 这篇文章。 Tracee 与 Falco 还是有诸多类似的功能,只是从实现和架构上看, Tracee 更加直接和简单,也没有特别复杂的规则引擎,作者给出的与 Falco 定位不同如下,更加详细的可参见 这里:Falco 是一个规则引擎,基于 sysdig 的开放源代码。它从 sysdig 获取原始事件,并与 yaml 文件中 falco 语言定义的规则相匹配。相比之下,Tracee 从 eBPF 中追踪事件,但不执行基于这些事件的规则。我们编写 Tracee 时考虑到了以下几点:Tracee 从一开始就被设计成一个基于 eBPF 的轻量级事件追踪器。Tracee 建立在 bcc 的基础上,并没有重写低级别的 BPF 接口。Tracee 被设计成易于扩展,例如,在 tracee 中添加对新的系统调用的支持就像添加两行代码一样简单,在这里你可以描述系统调用的名称和参数类型。其他事件也被支持,比如内部内核函数。我们现在已经支持 cap_capable,我们正在增加对 security_bprm_check lsm 钩子的支持。由于 lsm 安全钩子是安全的战略要点,我们计划在不久的将来增加更多这样的钩子。其实,从使用的场景上来说 Tracee 与 Falco 不是非 A 即 B 的功能,在 Tracee 也可以与 FalcoSideKick 进行集成,作为一个事件输入源使用。从下面两者架构图的对比,我们也可以略微熟悉一二, Tracee 更加直接和简洁,规则引擎的维护也不是重点,而且规则引擎恰恰是 Falco 的重点。Falco 的架构图如下:而 Tracee 的架构图如下:2. Tracee 的工作原理Tracee 中的 tracee-ebpf 模块的核心能力包括: 事件跟踪(trace)、抓取(capture)和输出(output)三个能力。tracee-ebpf 的核心能力在于底层 eBPF 程序抓取事件的能力,tracee-ebpf 默认实现了诸多的事件抓取功能,可以通过 trace -l 参看到底层支持的函数全集( 0.6.1 版本大概 390 个函数,格式如下:$ sudo docker run --name tracee-only --rm --privileged --pid=host -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -v /boot/:/boot tracee -l System Calls: Sets: Arguments: ____________ ____ _________ read [syscalls fs fs_read_write] (int fd, void* buf, size_t count) write [syscalls fs fs_read_write] (int fd, void* buf, size_t count) open [default syscalls fs fs_file_ops] (const char* pathname, int flags, mode_t mode) openat [default syscalls fs fs_file_ops] (int dirfd, const char* pathname, int flags, mode_t mode) ...第一列为系统调用函数名字;第二列为该函数归属为的子类(注可归属多个,比如 read 函数,归属于 syscalls/fs/fs_read_write 3 个子类,除了 fs 外,net 集合中也包含了许多的跟踪函数);第三列为该函数的原型,可以使用参数中的字段进行过滤,支持特定的运算,比如 == != 等常见的逻辑操作符,对于字符串也支持通配符操作;这里简单介绍两个样例,更加详细的可以使用 tracee --trace help 命令查看。--trace s=fs --trace e!=open,openat 跟踪 fs 集合中的所有事件,但是不包括 open,openat 两个函数;--trace openat.pathname!=/tmp/1,/bin/ls 这里表示不跟踪 openat 事件中,pathname 为 /tmp/1,/bin/ls 的事件,注意这里的 openat.pathname 为跟踪函数名与函数参数的组合;以上跟踪事件的过滤条件通过接口设置进内核中对应的 map 结构中,在完成过滤和事件跟踪以后,通过 perf_event 的方式上报到用户空间程序中,可以保存到奥文件后续进行处理,或者直接通过管道发送至 tracee-rule 进行解析和进行更高级别的上报,详细参见上一章节的架构图。3. Tracee 功能测试3.1 功能测试测试前需要保证内核版本及相关条件满足最小要求:内核 >= 4.18,可选项启用了 BTF,BTF 启用可以通过 /boot/config* 文件检查 CONFIG_DEBUG_INFO_BTF 是否启用;(grep CONFIG_DEBUG_INFO_BTF /boot/config-xx-yyy)Linux 内核头文件已经安装,Ubuntu/Debian/Arch/Manjaro 中为 linux-headers 包,CentOS/Fedora 中为 kernel-headers 和 kernel-devel 两个包;如果内核启用了 BTF 功能,可以直接使用官方提供的镜像进行测试:$ sudo docker run --name tracee --rm --privileged -it aquasec/tracee:latest trace如果系统未启用 BTF 功能,则需要加载内核 /lib/modules/ 和 /usr/src 目录,并运行以下命令:$ sudo docker run --name tracee --rm --privileged --pid=host -v /boot:/boot:ro -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -e TINI_SUBREAPER=true aquasec/tracee Loaded signature(s): [TRC-1 TRC-2 TRC-3 TRC-4 TRC-5 TRC-6 TRC-7]挂载 /boot 目录方便读取 /boot/config* 等相关文件,-e TINI_SUBREAPER=true 是为了让 tini 作为父进程进行子进程回收的能力。在运行以后我们可以发现最后有一系列签名输出 TRC-1 TRC-2 TRC-3 TRC-4 TRC-5 TRC-6 TRC-7,这些签名代表了对应检测的选项。我们可以使用 --list 选项进行查看,结果如下:# docker run --name tracee --rm --privileged --pid=host -v /boot:/boot:ro -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -e TINI_SUBREAPER=true aquasec/tracee --list Loaded signature(s): [TRC-1 TRC-2 TRC-3 TRC-4 TRC-5 TRC-6 TRC-7] ID NAME VERSION DESCRIPTION TRC-1 Standard Input/Output Over Socket 0.1.0 Redirection of process's standard input/output to socket TRC-2 Anti-Debugging 0.1.0 Process uses anti-debugging technique to block debugger TRC-3 Code injection 0.1.0 Possible code injection into another process TRC-4 Dynamic Code Loading 0.1.0 Writing to executable allocated memory region TRC-5 Fileless Execution 0.1.0 Executing a process from memory, without a file in the disk TRC-6 kernel module loading 0.1.0 Attempt to load a kernel module detection TRC-7 LD_PRELOAD 0.1.0 Usage of LD_PRELOAD to allow hooks on process使用 elfexec 进行测试:# From https://github.com/abbat/elfexec/releases $ wget https://github.com/abbat/elfexec/releases/download/v0.3/elfexec.x64.glibc.xz $ chmod u+x elfexec.x64.glib && mv ./elfexec.x64.glibc ./elfexec $ echo 'IyEvYmluL3NoCmVjaG8gIkhlbGxvISIK' | base64 -d|./elfexec Hello! $ echo 'IyEvYmluL3NoCmVjaG8gIkhlbGxvISIK' | base64 -d #!/bin/sh echo "Hello!"上述命令就是将一个输出 echo hello 的脚本重定向到 elfexec 进行执行, 在上述命令运行后,输出以下信息:# docker run --name tracee --rm --privileged --pid=host -v /boot:/boot:ro -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -e TINI_SUBREAPER=true aquasec/tracee Loaded signature(s): [TRC-1 TRC-2 TRC-3 TRC-4 TRC-5 TRC-6 TRC-7] *** Detection *** Time: 2021-09-10T09:10:25Z Signature ID: TRC-5 Signature: Fileless Execution Data: map[] Command: elfexec Hostname: VM-0-14-ubuntu这里我们看到测试触发的签名为 TRC-5, 详细情况为 ”Fileless Execution“,命令为 ”elfexec“。4. 源码编译 eBPF 程序4.1 镜像方式编译Tracee 支持我们自己基于系统编译 eBPF 程序,然后将编译后的 eBPF 字节码传递至 Docker 镜像进行运行。推荐 eBPF 程序编译通过 Docker 镜像进行,如果使用本机环境编译需要安装并保证 GNU Make >= 4.3 - clang >= 11。推荐编译和运行基于 Ubuntu 系列的系统(Ubuntu 20.04),猜测 Tracee 的主要测试环境应该是在 Ubuntu 系列中,CentOS 系列测试偏少。推荐 Ubuntu 20.04 版本,在 CentOS 5.4 内核中编译遇到不少问题,主要是环境差异导致,包括内核编译目录软连接,dockerfile 等问题,参见我提交的 pr$ git clone --recursive https://github.com/aquasecurity/tracee.git $ make bpf DOCKER=1 # --just-print 只是打印,编译完成后可以在 dist 目录中看到编译好的字节码程序 $ ls -hl dist/ total 7.8M -rw-r--r-- 1 root root 3.1M Sep 10 17:22 tracee.bpf.5_4_132-1_el7_elrepo_x86_64.v0_6_1-1-gce65764.o -rw-r--r-- 1 root root 4.7M Sep 10 17:22 tracee.bpf.core.o # 可以通过 TRACEE_BPF_FILE 环境变量指定我们需要加载的 eBPF 程序,这里使用目录 /tmp/tracee $ sudo docker run --name tracee --rm --privileged --pid=host -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -e TRACEE_BPF_FILE=/tmp/tracee/tracee.bpf.core.o aquasec/tracee如果编译 tracee-ebpf 我们也会发现,底层还是会依赖动态库,只是因为 tracee-ebpf 底层使用 cgo 机制使用 libbpf 库依赖的结果。$ file tracee-ebpf/dist/tracee-ebpf| tr , '\n' tracee-ebpf/dist/tracee-ebpf: ELF 64-bit LSB executable x86-64 version 1 (SYSV) dynamically linked (uses shared libs) for GNU/Linux 3.2.0 BuildID[sha1]=5bd7dbfd0475f015e268e321476dfc928d06d950 not stripped $ # ldd tracee-ebpf/dist/tracee-ebpf tracee-ebpf/dist/tracee-ebpf: /lib64/libc.so.6: version `GLIBC_2.22' not found (required by tracee-ebpf/dist/tracee-ebpf) linux-vdso.so.1 => (0x00007ffe559d7000) libelf.so.1 => /lib64/libelf.so.1 (0x00007f5d0c884000) libz.so.1 => /lib64/libz.so.1 (0x00007f5d0c66e000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f5d0c452000) libc.so.6 => /lib64/libc.so.6 (0x00007f5d0c084000) /lib64/ld-linux-x86-64.so.2 (0x00007f5d0ca9c000)单独验证 tracee-ebpf 可以使用以下命令$ sudo docker run --name tracee-only --rm --privileged --pid=host -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -v /boot/:/boot tracee --trace event=execve --output table-verbose --debug|more4.2 编译错误处理4.2.1 "bind": invalid mount path: './' mount path must be absolutetracee-ebpf 使用 Docker-builder 路径加载报错如下:Step 3/3 : WORKDIR /tracee ---> Using cache ---> 5e8a792b8117 Successfully built 5e8a792b8117 Successfully tagged tracee-builder:latest docker run --rm -v /root/tracee/tracee-ebpf:./ -v /root/tracee/tracee-ebpf:/tracee/tracee-ebpf -w /tracee/tracee-ebpf --entrypoint make tracee-builder KERN_BLD_PATH=/usr/src/kernels/5.4.132-1.el7.elrepo.x86_64 KERN_SRC_PATH=build dist/tracee-ebpf VERSION=v0.6.1-1-gce65764 docker: Error response from daemon: invalid volume specification: '/root/tracee/tracee-ebpf:./': invalid mount config for type "bind": invalid mount path: './' mount path must be absolute. See 'docker run --help'. make: *** [dist/tracee-ebpf] Error 125修改方式+DOCKER_BUILDER_KERN_BLD ?= ``(if ``(shell readlink -f ``(KERN_BLD_PATH)),``(shell readlink -f ``(KERN_BLD_PATH)),``(KERN_BLD_PATH)) +DOCKER_BUILDER_KERN_SRC ?= ``(if ``(shell readlink -f ``(KERN_SRC_PATH)),``(shell readlink -f ``(KERN_SRC_PATH)),``(KERN_SRC_PATH))主要差异点为系统不同,软链接的方式不同导致CentOS# ls -hl /lib/modules/5.4.132-1.el7.elrepo.x86_64/source lrwxrwxrwx 1 root root 5 Jul 22 15:12 /lib/modules/5.4.132-1.el7.elrepo.x86_64/source -> buildUbuntu# ls -hl /lib/modules/5.4.0-42-generic/build lrwxrwxrwx 1 root root 39 Jul 10 2020 /lib/modules/5.4.0-42-generic/build -> /usr/src/linux-headers-5.4.0-42-generic4.2.2 单独生成 tracee-ebpf 镜像时,make: uname: Operation not permitted 等问题$ make docker docker build --build-arg VERSION=v0.6.1-1-gce65764 -t tracee:latest . Sending build context to Docker daemon 2.116GB Step 1/16 : ARG BASE=fat Step 2/16 : FROM golang:1.16-alpine as builder ARG BASE=fat FROM golang:1.16-alpine as builder RUN apk --no-cache update && apk --no-cache add git clang llvm make gcc libc6-compat coreutils linux-headers musl-dev elfutils-dev libelf-static zlib-static WORKDIR /tracee // ... make: uname: Operation not permitted make: find: Operation not permitted make: uname: Operation not permitted make: /bin/sh: Operation not permitted mkdir -p dist make: mkdir: Operation not permitted make: *** [Makefile:51: dist] Error 127 The command '/bin/sh -c make build VERSION=$VERSION' returned a non-zero code: 2 make: *** [docker] Error 2该问题是 builder 基础镜像 golang:1.16-alpine 版本升级版本(alpine3.14 以后)导致的,明确指定为 golang:1.16-alpine3.13 即可:-FROM golang:1.16-alpine as builder +FROM golang:1.16-alpine3.13 as builder4.2.3 failed to add kprobe 'p:kprobes/psecurity_file_open security_file_open': -17异常退出后,再次运行可能会导致 ailed to create kprobe event: -17 的错误,错误的原因是使用了传统的 kprobe 方式,写入到 /sys/kernel/debug/tracing/kprobe_events 中,但是退出的时候未能够正常清理。# docker run --name tracee --rm --privileged -v /lib/modules/:/lib/modules/:ro -v /usr/src:/usr/src:ro -v /tmp/tracee:/tmp/tracee -it aquasec/tracee:latest 2021/09/07 02:46:41 [INFO] : Enabled Outputs : 2021/09/07 02:46:41 [INFO] : Falco Sidekick is up and listening on port 2801 failed to add kprobe 'p:kprobes/psecurity_file_open security_file_open': -17 failed to create kprobe event: -17如果出现错误可以通过 /sys/kernel/debug/tracing/kprobe_events 文件进行查看:$ cat /sys/kernel/debug/tracing/kprobe_events p:kprobes/psecurity_mmap_addr security_mmap_addr p:kprobes/psecurity_file_mprotect security_file_mprotect p:kprobes/psecurity_bprm_check security_bprm_check p:kprobes/pcap_capable cap_capable p:kprobes/psecurity_inode_unlink security_inode_unlink p:kprobes/psecurity_file_open security_file_open修复 sudo bash -c "echo""> /sys/kernel/debug/tracing/kprobe_events",参见 issue 447 和 639。4.2.4 ‘err’ may be used uninitialized in this function编译 libbpf 的时候可能报错,需要修改 Makefile 文件中的 CFLAGS ?= -g -O2 -Werror -Wall,删除 -Werror 即可。btf_dump.c: In function ‘btf_dump_dump_type_data.isra.24’: btf_dump.c:2266:5: error: ‘err’ may be used uninitialized in this function [-Werror=maybe-uninitialized] if (err < 0) cc1: all warnings being treated as errors4.2.5 Go 拉取包超时// ... GOOS=linux GOARCH=amd64 CC=clang CGO_CFLAGS="-I /tracee/tracee-ebpf/dist/libbpf/usr/include" CGO_LDFLAGS="/tracee/tracee-ebpf/dist/libbpf/libbpf.a" go build -tags netgo -v -o dist/tracee-ebpf \ -ldflags "-w -extldflags \"\"-X main.version=v0.6.1-1-gce65764" go: github.com/aquasecurity/libbpfgo@v0.2.1-libbpf-0.4.0: Get "https://proxy.golang.org/github.com/aquasecurity/libbpfgo/@v/v0.2.1-libbpf-0.4.0.mod": dial tcp 142.251.42.241:443: i/o timeout make: *** [Makefile:59: dist/tracee-ebpf] Error 1 make: *** [dist/tracee-ebpf] Error 2添加代理执行 GOPROXY=https://goproxy.cn 即可。$ docker run --rm -v /usr/src/kernels:/usr/src/kernels/ -v /root/tracee/tracee-ebpf:/tracee/tracee-ebpf -w /tracee/tracee-ebpf --entrypoint make tracee-builder DOCKER_BUILDER_KERN_SRC=/usr/src/kernels/5.4.132-1.el7.elrepo.x86_64 KERN_SRC_PATH=/lib/modules/5.4.132-1.el7.elrepo.x86_64/source KERN_BLD_PATH=/usr/src/kernels/5.4.132-1.el7.elrepo.x86_64 KERN_SRC_PATH=/usr/src/kernels/5.4.132-1.el7.elrepo.x86_64 dist/tracee-ebpf VERSION=v0.6.1-1-gce65764 GOPROXY=https://goproxy.cn5. 参考Tracee: Tracing Containers with eBPFTracee:如何使用 eBPF 来追踪容器和系统事件

【译】eBPF 和 Go 经验初探

原文地址:https://networkop.co.uk/post/2021-03-ebpf-intro/首发地址: 【译】eBPF 和 Go 经验初探本站相关文档:使用 Go 语言管理和分发 ebpf 程序1. 前言eBPF 的生态欣欣向荣,无论是 eBPF 本身及其各种应用(包括 XDP) 方面都有大量的学习资源。但当涉及到选择库和工具来与 eBPF 进行交互时,会让人有所困惑。在选择时,你必须在基于 Python 的 BCC 框架、基于 C 的 libbpf 和一系列基于 Go 的 Dropbox、Cilium、Aqua 和 Calico 等库中选择。另一个经常被忽视的重要领域是 eBPF 代码的 "生产化",即从手动编写的样例到生产级应用(例如 Cilium)。在本篇文章中,我将记录相关的经验,特别是在网络(XDP)应用程序场景中,使用 Go 编写的用户空间控制程序。2. 选择 eBPF 库在大多数情况下,eBPF 库主要协助实现两个功能:将 eBPF 程序和 Map 载入内核并执行重定位,通过其文件描述符将 eBPF 程序与正确的 Map 进行关联。与 eBPF Map 交互,允许对存储在 Map 中的键/值对进行标准的 CRUD 操作。部分库也可以帮助你将 eBPF 程序附加到一个特定的钩子,尽管对于网络场景下,这可能很容易采用现有的 netlink API 库完成。当涉及到 eBPF 库的选择时,我并不是唯一感到困惑的人(见[1], [2])。事实是每个库都有各自的范围和限制。Calico 在用 bpftool 和 iproute2 实现的 CLI 命令基础上实现了一个 Go 包装器。Aqua 实现了对 libbpf C 库的 Go 包装器。Dropbox 支持一小部分程序,但有一个非常干净和方便的用户API。IO Visor 的 gobpf 是 BCC 框架的 Go 语言绑定,它更注重于跟踪和性能分析。Cilium 和 Cloudflare 维护一个 纯 Go 语言编写的库 (以下简称 "libbpf-go"),它将所有 eBPF 系统调用抽象在一个本地 Go 接口后面。基于我的网络特定用例,我最终选择了 libbpf-go,因为其被 Cilium 和 Cloudflare 使用,并且有一个活跃的社区,尽管我也非常喜欢简单易用的 Dropbox 库,并且也可以使用它。为了熟悉开发过程,我决定实现一个 XDP 交叉连接的应用,它在网络拓扑模拟方面有一个非常小众但重要的用例。我们的目标是要有一个应用程序来观察一个配置文件,并确保本地接口根据该文件的 YAML 规范进行互连。下面是对 xdp-xconnect 工作高层次概述。下面的章节将逐步描述应用的构建和交付过程,更多的是关注集成,而不是实际的代码。xdp-xconnect的完整代码在Github上可用。3. 步骤1 - 编写 eBPF 代码通常情况下,这将是任何 "eBPF 入门" 文章的主要部分,然而这一次它并不是重点。我并不认为自己可以帮助别人学习如何编写eBPF,然而,我可以参考一些非常好的资源。通用的 eBPF 理论在网站 ebpf.io 和 Cilium 的 eBPF 和 XDP 参考指南中有大量的细节。对 eBPF 和 XDP 进行实践的最好地方是 xdp-tutorial。这是一个了不起的资源,即使你最终选择不完成作业,也绝对值得阅读。Cilium 的源代码和其在 [1] 和 [2] 的分析。我的 eBPF 程序非常简单,它包括对 eBPF 帮助函数的一次调用,可根据传入接口的索引将所有数据包从一个接口重定向到另一个。#include <linux/bpf.h> #include <bpf/bpf_helpers.h> SEC("xdp") int xdp_xconnect(struct xdp_md *ctx) return bpf_redirect_map(&xconnect_map, ctx->ingress_ifindex, 0); }为了编译上述程序,我们需要为所有包含的头文件提供包含路径。最简单的方法是在 linux/tools/lib/bpf/ 下复制所有文件,然而,这将包括很多不必要的文件。因此,另一种方法是创建一个依赖性列表。$ clang -MD -MF xconnect.d -target bpf -I ~/linux/tools/lib/bpf -c xconnect.c现在我们可以只对 xconnect.d 中指定的少量文件进行本地拷贝,并使用以下命令为本地 CPU 架构编译 eBPF 代码。$ clang -target bpf -Wall -O2 -emit-llvm -g -Iinclude -c xconnect.c -o - | \ llc -march=bpf -mcpu=probe -filetype=obj -o xconnect.oThe resulting ELF file is what we’d need to provide to our Go library in the next step.编译生成的 ELF 文件就是我们在下一步需要提供给 Go 库的程序。4. 步骤 2 - 编写 Go 代码编译好的 eBPF 程序和 Map 可以通过 libbpf-go 加载,这只需几个指令。通过添加带有 ebpf 标签的结构,我们可以自动进行重定位程序,并且知道何处发现 Map。spec, err := ebpf.LoadCollectionSpec("ebpf/xconnect.o") if err != nil { panic(err) var objs struct { XCProg *ebpf.Program `ebpf:"xdp_xconnect"` XCMap *ebpf.Map `ebpf:"xconnect_map"` if err := spec.LoadAndAssign(&objs, nil); err != nil { panic(err) defer objs.XCProg.Close() defer objs.XCMap.Close()ebpf.Map 类型有一组方法,可对加载的 Map 内容进行标准的 CRUD 操作:err = objs.XCMap.Put(uint32(0), uint32(10)) var v0 uint32 err = objs.XCMap.Lookup(uint32(0), &v0) err = objs.XCMap.Delete(uint32(0))唯一没有被 libbpf-go 包含的步骤是将程序附加到网络钩子上。然而,这可以通过任何现有的 netlink 库轻松实现,例如vishvananda/netlink,通过将网络连接与加载程序的文件描述符联系起来:link, err := netlink.LinkByName("eth0") err = netlink.LinkSetXdpFdWithFlags(*link, c.objs.XCProg.FD(), 2)请注意,我使用 SKB_MODE XDP 标志来绕过退出的 veth 驱动程序 caveat 。尽管本地 XDP 模式比任何其他 eBPF 钩子快得多,但 SKB_MODE 可能没有那么快,因为数据包头必须由网络栈预先解析(见视频)。5. 步骤 3 - 代码分发在这一点上,如果不是因为一个问题 -- eBPF 代码可移植性,一切都应该已经准备好打包和发布应用。历史上,这个过程涉及将 eBPF 源代码复制到目标平台,拉取所需的内核头文件,并为特定的内核版本进行编译。这个问题对于追踪/监控/跟踪的用例尤其明显,因为这些用例可能需要访问几乎所有的内核数据结构,所以唯一的解决办法是引入中介层(见 CO-RE)。另一方面,网络用例依赖于一个相对较小且稳定的内核类型子集,所以它们不会像跟踪和性能分析程序那样遇到同样的问题。根据我目前看到的情况,两种最常见的代码打包方法是:将 eBPF 代码与所需的内核头文件放在一起,假设它们与底层内核相匹配(见Cilium)。分发 eBPF 代码并在目标平台上拉取内核头文件。在这两种情况下,eBPF 代码仍然需要在目标平台上编译,这是一个额外的步骤,需要在用户空间应用程序启动之前进行。然而,还有一个选择,那就是预先将 eBPF 代码编译成 ELF 格式文件,最终只分发 ELF 文件。这正是 bpf2go 可以做到的,它可以将编译后的代码嵌入到 Go 包中。其依靠 go generate 注解指令产生一个新的文件,其中包含编译好的 eBPF 和 libbpf-go 脚手架代码,唯一的要求是 //go:generate 指令。一旦生成,我们的 eBPF 程序只需几行就可以被加载(注意没有任何参数)。specs, err := newXdpSpecs() objs, err := specs.Load(nil)这种方法明显的优点是,我们不再需要在目标机器上编译,可以在一个软件包或 Go 二进制文件中同时运送 eBPF 和用户空间 Go 代码。这很好,因为它允许我们不仅将应用程序作为二进制文件使用,还可以将其导入任何第三方 Go 应用程序中(见使用实例)。6. 阅读和有趣的参考资料通用理论:https://github.com/xdp-project/xdp-tutorialhttps://docs.cilium.io/en/stable/bpf/https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/BCC 和 libbpf:https://facebookmicrosites.github.io/bpf/blog/2020/02/20/bcc-to-libbpf-howto-guide.htmlhttps://nakryiko.com/posts/libbpf-bootstrap/https://pingcap.com/blog/why-we-switched-from-bcc-to-libbpf-for-linux-bpf-performance-analysishttps://facebookmicrosites.github.io/bpf/blog/eBPF/XDP 性能:https://www.netronome.com/blog/bpf-ebpf-xdp-and-bpfilter-what-are-these-things-and-what-do-they-mean-enterprise/Linus Kernel 代码风格:https://www.kernel.org/doc/html/v5.9/process/coding-style.htmllibbpf-go 样例程序:https://github.com/takehaya/goxdp-templatehttps://github.com/hrntknr/nfNathttps://github.com/takehaya/Vinberohttps://github.com/tcfw/vpchttps://github.com/florianl/tc-skeletonhttps://github.com/cloudflare/rakelimithttps://github.com/b3a-dev/ebpf-geoip-demobpf2go:https://github.com/lmb/ship-bpf-with-gohttps://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2goXDP 样例程序:https://github.com/cpmarvin/lnetd-ctlhttps://gitlab.com/mwiget/crpd-l2tpv3-xdp

浅析手机抓包方法实践(zt)

原文:http://drops.wooyun.org/tips/124670x00 摘要在移动逆向分析以及 App 开发的时候,总会需要对其网络行为进行监控测试,本文总结一些抓包思路,并对其使用方法进行实践笔者认为在抓包界,Wireshark 应该算是综合排名第一的工具(其实 Wireshark 自带的命令行工具 tshark 更牛逼)本文总结记录了 5 种抓包方式,掌握其一即可进行实践,欢迎大家一起交流分享0x01 基于 Wireshark实验步骤:1.1 在电脑主机上使用猎豹 Wifi之类的工具,开启热点,将所要测试的手机连接该热点,记录其IP地址1.2 使用 Wireshark 对以上 IP 地址进行捕获Capture——Options1.3 总结该方法简单粗暴高效,可以将捕获的数据包随时保存下来,便于后续分析或者进行 PCAP 可视化分析。关于命令行工具 tshark 在此不做赘述,感兴趣的读者自行研究。0x02 基于 tcpdump实验环境:下载安装 Genymotion 安卓虚拟机,在该模拟器环境种进行实践操作(基于实体手机亦然,前提是手机必须得 ROOT)笔者仅在 Android 系统下测试,未在 iOS 系统下实验实验步骤:2.1 说明模拟器中自带的 tcpdump 工具,位于: /system/xbin/ 目录下2.2 数据包捕获可以通过 adb shell 命令在 CMD 模式下连接模拟器,su 到 root 模式进行抓包tcpdump -vv -s 0 -i eth1 -w /sdcard/capture.pcap参数说明:-vv:获取详细的包信息(注意是两个 v 不是 w)-s 0:不限数据包的长度,如果不加则只获取包头-w xxx.pcap:捕获数据包名称以及存储位置(本例中保存在 sdcard 路径下,数据包名为 capture.pcap)-i eth1:捕获制定的网卡(在 genymotion 虚拟机中,使用 busybox ifconfig 命令可以查看相关信息,一般 genymotion 的 ip 地址都为 10.xx.xx.x)如果你想指定捕获的数据包长度,可以使用 -c 参数(例如 -c 128)捕获结束,直接按 Ctrl + C 即可2.3 数据分析将捕获到的数据包拖到本地使用 Wireshark 进行查看:adb pull /sdcard/capture.pcap C:\tmpTIPS:将数据包文件 push 到手机上命令为adb push C:\tmp\capture.pcap /sdcard/0x03 基于 Fiddler 4实验步骤:3.1 下载 FIddler 4点击下载 Fiddler 43.2 设置 Fiddler 4打开Fiddler,Tools-> Fiddler Options (配置完成记得重启 Fiddler)3.3 设置手机代理首先,获取安装 Fiddler 4 的 PC 对应的 IP 地址(ipconfig):确保手机和 PC 是连接在同一个局域网中!!!下面对手机进行设置(笔者使用小米测试机):点击手机中“设置”——Wi-Fi——选择已经连接的wifi——代理设置改为手动下载 Fiddler 的安全证书使用手机浏览器访问:http://10.2.145.187:8888,点击"FiddlerRoot certificate",然后安装证书即可。至此,已经全部设置完毕。3.4 数据包捕获重新打开 Fiddler 4,然后打开手机中的浏览器,访问任意网址,Fiddler 抓包信息如下:Enjoy!0x04 基于 Charles实验环境:win7 + Charles v3.11一般使用 Charles 都是基于 MAC OS ,笔者在 mac 平台以及 windows 平台均试验过,操作过程和思路基本一致,因此,本文以 win7 为测试环境实验步骤:4.1 捕获 http 数据包手机设置代理:打开 Charles 即可捕获数据包(Proxy —— Proxy Settings):4.2 捕获 https 数据包手机端安装证书:Android 手机或者 iPhone 均可直接访问 http://www.charlesproxy.com/ssl.zip ,然后根据图示点击证书安装设置 Charles:选择 Proxy —— SSL Proxying Settings —— Locations —— Add在弹出的表单中填写 Host 域名(也就是你想要抓包链接的主机名),以及对应的 Port 端口(此处相当于过滤作用)当然,你可以采用更加粗暴的方式:使用通配符,例如你想要捕获所有的 https 包,这里也可以直接都为空,表示捕获所有的主机和端口;或者都分别填“*”星号,匹配所有的字符,捕获所有的 https。0x05 基于 Burpsuite实验步骤:5.1 捕获 http 数据包PC 端 Burpsuite 设置:手机端代理设置方法同以上 3.3 4.1打开 Burpsuite 即可捕获 http 数据包:5.2 捕获 https 数据包手机端设置好代理之后,使用浏览器访问:http://burp/此处存在一个问题:下载的证书是 der 格式的,我们手机端安装的是 crt 格式的,需要使用 firefox 浏览器转一下格式:可以首先在 Brupsuite 中导出 der 格式证书,然后导入火狐浏览器,然后从火狐浏览器导出证书格式为 crt打开火狐浏览器:工具——选项——高级——证书——查看证书成功捕获 https 数据包0x06 总结当我们停止捕获数据包时,将Fiddler 或 Charles 关闭,此时手机端是无法正常访问网络的,因为设置了代理,这时候需要将代理关闭,即可正常浏览网页对于大多数走代理的应用可以选择 Fiddler 或 Charles,无需 root,一次配置,终身使用;对于不走代理的 App 可以利用 tcpdump 捕包,然后使用 Wireshark 查看;最简单便捷的便是第一种方法「0x01. 基于 Wireshark」以上所有工具各有优劣,读者可以根据工作环境,按需使用,个人觉得一般情况下使用 Wireshark + Fiddler 或者 Wireshark + Charles 即可完成各平台的抓包分析任务以上工具中只有BurpSuite可以对抓包过程进行交互式操作;Wireshark支持的协议最多,也更底层,功能强大,但过于沉重对于本文涉及的相关工具的安装、设置、破解、详细使用,不在本文讨论范围之内(Charles 免费版其实还比较厚道,如果重度需要,建议购买正版),本文旨在浅析捕获移动终端数据包的方法和思路0x07 参考文献抓包工具Fidder详解Mac上的抓包工具Charles网络抓包工具Charles的介绍与使用charles使用教程指南Android安全测试之BurpSuite抓包Android利用tcpdump和wireshark抓取网络数据包

Debian8修改启动默认运行级别

Two things you need to know:1) Systemd boots towards the target given by "default.target". This is typically a symbolic link to the actual target file.2) Systemd keeps it's targets in /lib/systemd/system and /etc/systemd/system. A file in /etc/systemd/system takes precedence over those shipped with the OS in /lib/systemd/system -- the intent is that /etc/systemd is used by systems administrators and /lib/systemd is used by distributions.Debian as-shipped boots towards the graphical target. You can see this yourself:$ ls -l /etc/systemd/system/default.target ... No such file or directory $ ls -l /lib/systemd/system/default.target ... /lib/systemd/system/default.target -> graphical.targetSo to boot towards the multiuser target all you need do is to put in own target:$ cd /etc/systemd/system/ $ sudo ln -s /lib/systemd/system/multi-user.target default.target

Linux sysinfo获取系统相关信息

Linux中,可以用sysinfo来获取系统相关信息。#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <linux/unistd.h> /* for _syscallX macros/related stuff */ #include <linux/kernel.h> /* for struct sysinfo */ //_syscall1(int, sysinfo, struct sysinfo *, info); /* Note: if you copy directly from the nroff source, remember to * REMOVE the extra backslashes in the printf statement. */ main(void) struct sysinfo s_info; int error; error = sysinfo(&s_info); printf("code error = %d\n", error); printf("Uptime = %lds\nLoad: 1 min %lu / 5 min %lu / 15 min %lu\n" "RAM: total %lu / free %lu / shared %lu\n" "Memory in buffers = %lu\nSwap: total %lu / free %lu\n" "Number of processes = %d\n", s_info.uptime, s_info.loads[0], s_info.loads[1], s_info.loads[2], s_info.totalram, s_info.freeram, s_info.sharedram, s_info.bufferram, s_info.totalswap, s_info.freeswap, s_info.procs); exit(EXIT_SUCCESS); }code error = 0Uptime = 47428sLoad: 1 min 192 / 5 min 2272 / 15 min 2976RAM: total 1027239936 / free 249516032 / shared 0Memory in buffers = 0Swap: total 2147479552 / free 2092707840Number of processes = 391

C/C++代码覆盖工具gcov与lcov入门

C/C++代码覆盖工具gcov与lcov入门gcov是一个可用于C/C++的代码覆盖工具,是gcc的内建工具。下面介绍一下如何利用gcov来收集代码覆盖信息。想要用gcov收集代码覆盖信息,需要在gcc编译代码的时候加上这2个选项 “-fprofile-arcs -ftest-coverage”,把这个简单的程序编译一下gcc -fprofile-arcs -ftest-coverage hello.c -o hello编译后会得到一个可执行文件hello和hello.gcno文件,当用gcc编译文件的时候,如果带有“-ftest-coverage”参数,就会生成这个.gcno文件,它包含了程序块和行号等信息接下来可以运行这个hello的程序./hello 5./hello 12运行结束以后会生成一个hello.gcda文件,如果一个可执行文件带有“-fprofile-arcs”参数编译出来,并且运行过至少一次,就会生成。这个文件包含了程序基本块跳转的信息。接下来可以用gcov生成代码覆盖信息:gcov hello.c运行结束以后会生成2个文件hello.c.gcov和myfunc.c.gcov。打开看里面的信息:-: 0:Source:myfunc.c-: 0:Graph:hello.gcno-: 0:Data:hello.gcda-: 0:Runs:1-: 0:Programs:1-: 1:#include-: 2:-: 3:void test(int count)1: 4:{-: 5: int i;10: 6: for (i = 1; i < count; i++)-: 7: {9: 8: if (i % 3 == 0)3: 9: printf (“%d is divisible by 3 \n”, i);9: 10: if (i % 11 == 0)#####: 11: printf (“%d is divisible by 11 \n”, i);9: 12: if (i % 13 == 0)#####: 13: printf (“%d is divisible by 13 \n”, i);-: 14: }1: 15:}被标记为#####的代码行就是没有被执行过的,代码覆盖的信息是正确的,但是让人去读这些文字,实在是一个杯具。不用担心,有另外一个工具叫lcov,可以用程序解析这些晦涩的字符,最终输出成html格式的报告,很好吧!lcov -d . -t ‘Hello test’ -o ‘hello_test.info’ -b . -c指定lcov在当前目录“.”去找代码覆盖的信息,输出为’hello_test.info’ ,这个hello_test.info是一个中间结果,需要把它用genhtml来处理一下,genhtml是lcov里面的一个工具。genhtml -o result hello_test.info指定输出目录是 result。一个完整的html报告就生成了,做一个连接,把这个目录连到随便一个web server的目录下,就可以看报告了。gcov和lcov基本上能满足测试过程中收集代码覆盖率信息的需求,不过有个遗憾就是gcov不能收集.so文件的代码覆盖信息。No related posts.发布于2010年09月12日作者magus分类白盒测试、软件测试标签gcov、lcov、代码覆盖、白盒测试、软件测试 1.risewind说道:2015年10月11日 16:20现在已经可以收集so的覆盖率信息了。回复2. moon说道:2015年08月21日 11:11gcov是非常方便,如果能统计一行的部分执行就好了。现在还没看到有这样的功能。比如if(a==0){a=1;}else{a=2;}如果只执行到a==0的条件,没执行到else部分,能表示出来部分执行就好了。

variable-precision SWAR算法介绍

BITCOUNT命令是统计一个位数组中非0进制位的数量,数学上称作:"Hanmming Weight"目前效率最好的为variable-precision SWAR算法,可以常数时间内计算出多个字节的非0数目,算法设计的非常精巧,值得学习。int swar(uint32_t i) // (A) i = ( i & 0x55555555) + ((i >> 1) & 0x55555555); // (B) i = (i & 0x33333333) + ((i >> 2) & 0x33333333); // (C) i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F); // (D) i = (i * 0x01010101) >> 24); return i; }原理解释:(A) 0x55555555  二进制为:  0101 0101 0101 0101  0101 0101 0101 0101, 奇位为1, 偶数为0  如果按照i的二进制表示  b31 b30.......  b7 b6 b5 b4 b3 b2 b1 b0         i & 0x55555555  则取出全部的奇数位:         0  b30 ...... 0  b6 0 b4 0 b2 0 b0     (i >> 1) & 0x55555555 则取出偶数位:        0 b31        0  b7  0 b5 0 b3 0 b1   两者相加:                                        + ------------------------------------------                                                                     0  (b30+b31)     .....         0   (b6+b7)   0   (b4+b5)   0   (b2+b3)    0   (b0+b1)原理就是按照二进制2位一个分割,计算该两位的1的数目 (B) 将 (A)步骤按照二进制2位划分的1的数目按照4个bit位进行累加(C) 将  (B)步骤中1的数目按照8个bit位进行累加(D)  (C)步骤中已经计算出了8bit划分的2进制的数目       如     byte3  byte2 byte1  byte0      y  =    y3      y2      y1      y0       那么 y * 0x01010101 则实现了 将 y0 y1 y2位和y3位置的累加 则y的值为:                       byte3            byte2        byte1    byte0    yn  =     y3+y2+y1+y0        x2             x1         x0    将yn >> 24位 则得到了  y3+y2+y1+y0 的效果。

redis性能测试tcp socket and unix domain

UNIX Domain Socket IPCsocket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。 初步测试可以得到以下结论:SET和GET操作提升  60%左右,具体可以参见: redis_benchmark_diff.txt

redis async client 与自有框架集成

hiredis的异步接口已经支持ae libuv libev 和 libevent集成,具体头文件可以参见redis/deps/hiredis/adapters,样例参见redis/deps/hiredis/examples.完整样例参见: https://github.com/DavadDi/study_example/tree/master/async_redis_client 参照hireids的异步接口和libevent的集成可以很容易和其他网络框架集成,例如asio或者ace等。 以下样例为自己编写reactor框架的集成方式,支持自动重练和asyncRedisContext对象的创建和释放,重练使用退步算法,最大连接时间间隔为32秒。 使用方式:  将redis_client.hpp 放到 hiredis的adapter目录即可。#ifndef redis_client_h #define redis_client_h #include "reactor/define.hpp" #include "reactor/event_handler.hpp" #include "hiredis.h" #include "async.h" using namespace reactor; static void redisReactorAddRead(void *arg); static void redisReactorDelRead(void *arg); static void redisReactorAddWrite(void *arg); static void redisReactorDelWrite(void *arg); static void redisReactorCleanup(void *arg); void connectCallBack(const redisAsyncContext *c, int status); void disconnectCallBack(const redisAsyncContext *c, int status); void get_call_fun(redisAsyncContext *c, void *r, void *arg) redisReply *reply = (redisReply *)r; std::string *key_str = (std::string *)arg; if (reply == NULL) delete key_str; return; LOG_INFO("[%s] -> %s\n", key_str->c_str(), reply->str); delete key_str; /* Disconnect after receiving the reply to GET */ // redisAsyncDisconnect(c); // ------------------------------------------------------------------- // !!NOTE, if obj conneted to server faild and unregister from epoll, // prog exit, this object my leak memory // ------------------------------------------------------------------- class CRedisClient : public CEventHandler public: // enum {MAX_BUF_SIZE = 4096}; typedef CEventHandler super; CRedisClient(const char *srv_ip, uint16_t srv_port, reactor::CReactor *reactor) : super(reactor) m_srv_ip_str = srv_ip; m_srv_port = srv_port; int connect() LOG_DEBUG("Enter CRedisClient connect()"); m_client_status = CONNECT_STATUS::CLIENT_CONNECTING; clear_redis_context(); m_redis_context = redisAsyncConnect(m_srv_ip_str.c_str(), m_srv_port); if (m_redis_context == nullptr) return -1; if (m_redis_context->err) LOG_INFO("Connect to %s:%d Error: %s", m_srv_ip_str.c_str(), m_srv_port, m_redis_context->errstr); return -1; if (m_timer_id == 0) m_timer_id = this->reactor()->regist_timer(this, m_timeout_value); // only one time LOG_DEBUG("Client regist timer to reactor id %d, timeout %d", m_timer_id, m_timeout_value); this->attach(); return 0; virtual ~CRedisClient() // maybe should not free redis context in deconstuct!! m_delete_redis_context = true; clear_redis_context(); virtual int open(void *data = nullptr) m_client_status = CONNECT_STATUS::CLIENT_CONNECTED; m_delete_redis_context = false; if (m_timer_id == 0) m_timer_id = this->reactor()->regist_timer(this, m_timeout_value); // only one time LOG_DEBUG("Client regist timer to reactor id %d, timeout %d", m_timer_id, m_timeout_value); LOG_INFO("Connect to RedisServer %s:%d succeed!!", m_srv_ip_str.c_str(), m_srv_port); return 0; virtual int handle_input(socket_t socket) redisAsyncHandleRead(m_redis_context); return 0; virtual int handle_output(socket_t socket) redisAsyncHandleWrite(m_redis_context); return 0; virtual int handle_timeout(uint32_t tm, void *data = nullptr) // LOG_DEBUG("Enter into timeout function...."); if (m_client_status == CONNECT_STATUS::CLIENT_CONNECTED) /* just for test */ std::string key = std::to_string(tm); LOG_DEBUG("Set key %s", key.c_str()); redisAsyncCommand(m_redis_context, NULL, NULL, "SET %s %s",key.c_str(), "aaa"); redisAsyncCommand(m_redis_context, get_call_fun, (char*)new string(key), "GET %s", key.c_str()); static uint32_t last_tm = 0; if ((tm - last_tm) >= m_timeout_interval) //reconnect LOG_DEBUG("Start reconnect now ..."); this->connect(); m_timeout_interval = m_timeout_interval * 2; if (m_timeout_interval > 32) m_timeout_interval = 1; last_tm = tm; return 0; virtual int handle_close(socket_t socket = INVALID_SOCKET, uint32_t mask = 0) LOG_DEBUG("Enter into handle_close()"); m_client_status = CONNECT_STATUS::CLIENT_UNCONNECTED; // epoll call delete this handler if (mask & RE_MASK_DEL) LOG_DEBUG("Call RE_MASK_DEL now"); if (this->m_timer_id && (this->reactor() != nullptr)) this->reactor()->unregist_timer(this->m_timer_id); this->m_timer_id = 0; delete this; return 0; this->reactor()->del_event(this,0); return 0; void clear_redis_context() if (m_delete_redis_context && m_redis_context != nullptr) LOG_DEBUG("Call redisAsynFree() now"); redisAsyncFree(m_redis_context); m_redis_context = nullptr; int attach() LOG_DEBUG("Enter attatch function... "); redisContext *context = &(m_redis_context->c); if (m_redis_context->ev.data != NULL) return -1; // set callback function redisAsyncSetConnectCallback(m_redis_context,connectCallBack); redisAsyncSetDisconnectCallback(m_redis_context,disconnectCallBack); this->set_handle(context->fd); // set handler m_redis_context->ev.addRead = redisReactorAddRead; m_redis_context->ev.delRead = redisReactorDelRead; m_redis_context->ev.addWrite = redisReactorAddWrite; m_redis_context->ev.delWrite = redisReactorDelWrite; m_redis_context->ev.cleanup = redisReactorCleanup; m_redis_context->ev.data = this; LOG_DEBUG("ac->ev.data %p", m_redis_context->ev.data); this->add_read(); this->add_write(); return 0; void add_read() LOG_TRACE_METHOD(__func__); if( (this->m_current_event_mask & reactor::EVENT_READ) > 0) LOG_DEBUG("EV_READ(0x%0x) already in event_mask 0x%x", reactor::EVENT_READ, this->m_current_event_mask); return; this->reactor()->add_event(this, reactor::EVENT_READ); void del_read() LOG_TRACE_METHOD(__func__); this->reactor()->mod_event(this, this->m_current_event_mask&(~reactor::EVENT_READ)); void add_write() LOG_TRACE_METHOD(__func__); this->schedule_write(); void del_write() LOG_TRACE_METHOD(__func__); this->cancel_schedule_write(); void clean_up() LOG_TRACE_METHOD(__func__); // note!!! // connenct not succeed. we can free redis context. ] // But if connect succeed and borken, we don't connect protected: std::string m_srv_ip_str; uint16_t m_srv_port; CONNECT_STATUS m_client_status = CONNECT_STATUS::CLIENT_UNCONNECTED; int m_timer_id = 0; uint32_t m_timeout_value = 1; uint32_t m_timeout_interval = 1; bool m_delete_redis_context = true; redisAsyncContext *m_redis_context = nullptr; static void redisReactorAddRead(void *arg) LOG_DEBUG("Enter redisReactorAddRead() arg %p", arg); CRedisClient *event_handler = (CRedisClient *)arg; event_handler->add_read(); static void redisReactorDelRead(void *arg) CRedisClient *event_handler = (CRedisClient *)arg; event_handler->del_read(); static void redisReactorAddWrite(void *arg) CRedisClient *event_handler = (CRedisClient *)arg; event_handler->add_write(); static void redisReactorDelWrite(void *arg) CRedisClient *event_handler = (CRedisClient *)arg; event_handler->del_write(); static void redisReactorCleanup(void *arg) CRedisClient *event_handler = (CRedisClient *)arg; event_handler->clean_up(); void connectCallBack(const redisAsyncContext *ac, int status) if (status != REDIS_OK) LOG_ERROR("connectCallBack() Error: %s", ac->errstr); return; CRedisClient *event_handler = (CRedisClient *)ac->ev.data; event_handler->open(); LOG_INFO("RedisClient Connected..."); void disconnectCallBack(const redisAsyncContext *ac, int status) CRedisClient *event_handler = (CRedisClient *)ac->ev.data; event_handler->handle_close(0,0); if (status != REDIS_OK) LOG_INFO("disconnectCallBack()!! Error: %s", ac->errstr); return; LOG_INFO("RedisClient Disconnected..."); #endif /* redis_client_h */使用的程序样例:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> //#include <hiredis.h> //#include <async.h> #include <adapters/redis_client.hpp> //#include "redis_client.hpp" #include <signal.h> static void signal_handler(int sig) if (sig == SIGINT) reactor::CReactor::instance()->end_event_loop(); void get_call_fun(redisAsyncContext *c, void *r, void *arg) redisReply *reply = (redisReply *)r; std::string *key_str = (std::string *)arg; if (reply == NULL) delete key_str; return; LOG_INFO("[%s] -> %s\n", key_str->c_str(), reply->str); delete key_str; // Disconnect after receiving the reply to GET // redisAsyncDisconnect(c); int main (int argc, char **argv) signal(SIGPIPE, SIG_IGN); signal(SIGINT, signal_handler); CLoggerMgr logger("reactor.prop"); reactor::CReactor *rt = reactor::CReactor::instance(); CRedisClient *redis_client = new CRedisClient("127.0.0.1", 6379, rt); redis_client->connect(); rt->run_event_loop(); return 0;

服务发现:Zookeeper vs etcd vs Consul

【编者的话】本文对比了Zookeeper、etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考。如果使用预定义的端口,服务越多,发生冲突的可能性越大,毕竟,不可能有两个服务监听同一个端口。管理一个拥挤的比方说被几百个服务所使用的所有端口的列表,本身就是一个挑战,添加到该列表后,这些服务需要的数据库和数量会日益增多。因此我们应该部署无需指定端口的服务,并且让Docker为我们分配一个随机的端口。唯一的问题是我们需要发现端口号,并且让别人知道。当我们开始在一个分布式系统上部署服务到其中一台服务器上时,事情会变得更加复杂,我们可以选择预先定义哪台服务器运行哪个服务的方式,但这会导致很多问题。我们应该尽我们所能尽量利用服务器资源,但是如果预先定义每个服务的部署位置,那么要实现尽量利用服务器资源是几乎不可能的。另一个问题是服务的自动伸缩将会非常困难,更不用说自动恢复了,比方说服务器故障。另一方面,如果我们将服务部署到某台只有最少数量的容器在运行的服务器上,我们需要添加IP地址到数据列表中,这些数据需要可以被发现并存储在某处。当我们需要存储和发现一些与正在工作的服务相关的信息时,还有很多其他的例子。为了能够定位服务,我们需要至少接下来的两个有用的步骤。服务注册——该步骤存储的信息至少包括正在运行的服务的主机和端口信息服务发现——该步骤允许其他用户可以发现在服务注册阶段存储的信息。除了上述的步骤,我们还需要考虑其他方面。如果一个服务停止工作并部署/注册了一个新的服务实例,那么该服务是否应该注销呢?当有相同服务的多个副本时咋办?我们该如何做负载均衡呢?如果一个服务器宕机了咋办?所有这些问题都与注册和发现阶段紧密关联。现在,我们限定只在服务发现的范围里(常见的名字,围绕上述步骤)以及用于服务发现任务的工具,它们中的大多数采用了高可用的分布式键/值存储。服务发现工具服务发现工具的主要目标是用来服务查找和相互对话,为此该工具需要知道每个服务,这不是一个新概念,在Docker之前就已经存在很多类似的工具了,然而,容器带给了这些工具一个全新水平的需求。服务发现背后的基本思想是对于服务的每一个新实例(或应用程序),能够识别当前环境和存储相关信息。存储的注册表信息本身通常采用键/值对的格式,由于服务发现经常用于分布式系统,所以要求这些信息可伸缩、支持容错和分布式集群中的所有节点。这种存储的主要用途是给所有感兴趣的各方提供最起码诸如服务IP地址和端口这样的信息,用于它们之间的相互通讯,这些数据还经常扩展到其它类型的信息服务发现工具倾向于提供某种形式的API,用于服务自身的注册以及服务信息的查找。比方说我们有两个服务,一个是提供方,另一个是第一个服务的消费者,一旦部署了服务提供方,就需要在服务发现注册表中存储其信息。接着,当消费者试图访问服务提供者时,它首先查询服务注册表,使用获取到的IP地址和端口来调用服务提供者。为了与注册表中的服务提供方的具体实现解耦,我们常常采用某种代理服务。这样消费者总是向固定IP地址的代理请求信息,代理再依次使用服务发现来查找服务提供方信息并重定向请求,在本文中我们稍后通过反向代理来实现。现在重要的是要理解基于三种角色(服务消费者、提供者和代理)的服务发现流程。服务发现工具要查找的是数据,至少我们应该能够找出服务在哪里?服务是否健康和可用?配置是什么样的?既然我们正在多台服务器上构建一个分布式系统,那么该工具需要足够健壮,保证其中一个节点的宕机不会危及数据,同时,每个节点应该有完全相同的数据副本,进一步地,我们希望能够以任何顺序启动服务、杀死服务或者替换服务的新版本,我们还应该能够重新配置服务并且查看到数据相应的变化。让我们看一下一些常用的选项来完成我们上面设定的目标。手动配置大多数服务仍然是需要手动管理的,我们预先决定在何处部署服务、如何配置和希望不管什么原因,服务都将继续正常工作,直到天荒地老。这样的目标不是可以轻易达到的。部署第二个服务实例意味着我们需要启动全程的手动处理,我们需要引入一台新的服务器,或者找出哪一台服务器资源利用率较低,然后创建一个新的配置集并启动服务。情况或许会变得越来越复杂,比方说,硬件故障导致的手动管理下的反应时间变得很慢。可见性是另外一个痛点,我们知道什么是静态配置,毕竟是我们预先准备好的,然而,大多数的服务有很多动态生成的信息,这些信息不是轻易可见的,也没有一个单独的地方供我们在需要时参考这些数据。反应时间会不可避免的变慢,鉴于存在许多需要手动处理的移动组件,故障恢复和监控也会变得非常难以管理。尽管在过去或者当服务/服务器数量很少的时候有借口不做这项工作,随着服务发现工具的出现,这个借口已经不存在了。ZookeeperZookeeper是这种类型的项目中历史最悠久的之一,它起源于Hadoop,帮助在Hadoop集群中维护各种组件。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。其数据存储的格式类似于文件系统,如果运行在一个服务器集群中,Zookeper将跨所有节点共享配置状态,每个集群选举一个领袖,客户端可以连接到任何一台服务器获取数据。Zookeeper的主要优势是其成熟、健壮以及丰富的特性,然而,它也有自己的缺点,其中采用Java开发以及复杂性是罪魁祸首。尽管Java在许多方面非常伟大,然后对于这种类型的工作还是太沉重了,Zookeeper使用Java以及相当数量的依赖使其对于资源竞争非常饥渴。因为上述的这些问题,Zookeeper变得非常复杂,维护它需要比我们期望从这种类型的应用程序中获得的收益更多的知识。这部分地是由于丰富的特性反而将其从优势转变为累赘。应用程序的特性功能越多,就会有越大的可能性不需要这些特性,因此,我们最终将会为这些不需要的特性付出复杂度方面的代价。Zookeeper为其他项目相当大的改进铺平了道路,“大数据玩家“在使用它,因为没有更好的选择。今天,Zookeeper已经老态龙钟了,我们有了更好的选择。etcdetcd是一个采用HTTP协议的健/值对存储系统,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。其很容易部署、安装和使用,提供了可靠的数据持久化特性。它是安全的并且文档也十分齐全。etcd比Zookeeper是比更好的选择,因为它很简单,然而,它需要搭配一些第三方工具才可以提供服务发现功能。现在,我们有一个地方来存储服务相关信息,我们还需要一个工具可以自动发送信息给etcd。但在这之后,为什么我们还需要手动把数据发送给etcd呢?即使我们希望手动将信息发送给etcd,我们通常情况下也不会知道是什么信息。记住这一点,服务可能会被部署到一台运行最少数量容器的服务器上,并且随机分配一个端口。理想情况下,这个工具应该监视所有节点上的Docker容器,并且每当有新容器运行或者现有的一个容器停止的时候更新etcd,其中的一个可以帮助我们达成目标的工具就是Registrator。RegistratorRegistrator通过检查容器在线或者停止运行状态自动注册和去注册服务,它目前支持etcd、Consul和SkyDNS 2。Registrator与etcd是一个简单但是功能强大的组合,可以运行很多先进的技术。每当我们打开一个容器,所有数据将被存储在etcd并传播到集群中的所有节点。我们将决定什么信息是我们的。上述的拼图游戏还缺少一块,我们需要一种方法来创建配置文件,与数据都存储在etcd,通过运行一些命令来创建这些配置文件。ConfdConfd是一个轻量级的配置管理工具,常见的用法是通过使用存储在etcd、consul和其他一些数据登记处的数据保持配置文件的最新状态,它也可以用来在配置文件改变时重新加载应用程序。换句话说,我们可以用存储在etcd(或者其他注册中心)的信息来重新配置所有服务。对于etcd、Registrator和Confd组合的最后的思考当etcd、Registrator和Confd结合时,可以获得一个简单而强大的方法来自动化操作我们所有的服务发现和需要的配置。这个组合还展示了“小”工具正确组合的有效性,这三个小东西可以如我们所愿正好完成我们需要达到的目标,若范围稍微小一些,我们将无法完成我们面前的目标,而另一方面如果他们设计时考虑到更大的范围,我们将引入不必要的复杂性和服务器资源开销。在我们做出最后的判决之前,让我们看看另一个有相同目标的工具组合,毕竟,我们不应该满足于一些没有可替代方案的选择。ConsulConsul是强一致性的数据存储,使用gossip形成动态集群。它提供分级键/值存储方式,不仅可以存储数据,而且可以用于注册器件事各种任务,从发送数据改变通知到运行健康检查和自定义命令,具体如何取决于它们的输出。与Zookeeper和etcd不一样,Consul内嵌实现了服务发现系统,所以这样就不需要构建自己的系统或使用第三方系统。这一发现系统除了上述提到的特性之外,还包括节点健康检查和运行在其上的服务。Zookeeper和etcd只提供原始的键/值队存储,要求应用程序开发人员构建他们自己的系统提供服务发现功能。而Consul提供了一个内置的服务发现的框架。客户只需要注册服务并通过DNS或HTTP接口执行服务发现。其他两个工具需要一个亲手制作的解决方案或借助于第三方工具。Consul为多种数据中心提供了开箱即用的原生支持,其中的gossip系统不仅可以工作在同一集群内部的各个节点,而且还可以跨数据中心工作。Consul还有另一个不错的区别于其他工具的功能,它不仅可以用来发现已部署的服务以及其驻留的节点信息,还通过HTTP请求、TTLs(time-to-live)和自定义命令提供了易于扩展的健康检查特性。RegistratorRegistrator有两个Consul协议,其中consulkv协议产生类似于etcd协议的结果。除了通常的IP和端口存储在etcd或consulkv协议中之外,Registrator consul协议存储了更多的信息,我们可以得到服务运行节点的信息,以及服务ID和名称。我们也可以借助于一些额外的环境变量按照一定的标记存储额外的信息。Consul-templateconfd可以像和etce搭配一样用于Consul,不过Consul有自己的模板服务,其更适配Consul。通过从Consul获得的信息,Consul-template是一个非常方便的创建文件的途径,还有一个额外的好处就是在文件更新后可以运行任意命令,正如confd,Consul-template也可以使用Go模板格式。Consul健康检查、Web界面和数据中心监控集群节点和服务的健康状态与测试和部署它们一样的重要。虽然我们应该向着拥有从来没有故障的稳定的环境努力,但我们也应该承认,随时会有意想不到的故障发生,时刻准备着采取相应的措施。例如我们可以监控内存使用情况,如果达到一定的阈值,那么迁移一些服务到集群中的另外一个节点,这将是在发生“灾难”前执行的一个预防措施。另一方面,并不是所有潜在的故障都可以被及时检测到并采取措施。单个服务可能会齿白,一个完整的节点也可能由于硬件故障而停止工作。在这种情况下我们应该准备尽快行动,例如一个节点替换为一个新的并迁移失败的服务。Consul有一个简单的、优雅的但功能强大的方式进行健康检查,当健康阀值达到一定数目时,帮助用户定义应该执行的操作。如果用户Google搜索“etcd ui”或者“etec dashboard”时,用户可能看到只有几个可用的解决方案,可能会问为什么我们还没有介绍给用户,这个原因很简单,etcd只是键/值对存储,仅此而已。通过一个UI呈现数据没有太多的用处,因为我们可以很容易地通过etcdctl获得这些数据。这并不意味着etcd UI是无用的,但鉴于其有限的使用范围,它不会产生多大影响。Consu不仅仅是一个简单的键/值对存储,正如我们已经看到的,除了存储简单的键/值对,它还有一个服务的概念以及所属的数据。它还可以执行健康检查,因此成为一个好的候选dashboard,在上面可以看到我们的节点的状态和运行的服务。最后,它支持了多数据中心的概念。所有这些特性的结合让我们从不同的角度看到引入dashboard的必要性。通过Consul Web界面,用户可以查看所有的服务和节点、监控健康检查状态以及通过切换数据中心读取设置键/值对数据。对于Consul、Registrator、Template、健康检查和Web UI的最终思考Consul以及上述我们一起探讨的工具在很多情况下提供了比etcd更好的解决方案。这是从内心深处为了服务架构和发现而设计的方案,简单而强大。它提供了一个完整的同时不失简洁的解决方案,在许多情况下,这是最佳的服务发现以及满足健康检查需求的工具。结论所有这些工具都是基于相似的原则和架构,它们在节点上运行,需要仲裁来运行,并且都是强一致性的,都提供某种形式的键/值对存储。Zookeeper是其中最老态龙钟的一个,使用年限显示出了其复杂性、资源利用和尽力达成的目标,它是为了与我们评估的其他工具所处的不同时代而设计的(即使它不是老得太多)。etcd、Registrator和Confd是一个非常简单但非常强大的组合,可以解决大部分问题,如果不是全部满足服务发现需要的话。它还展示了我们可以通过组合非常简单和特定的工具来获得强大的服务发现能力,它们中的每一个都执行一个非常具体的任务,通过精心设计的API进行通讯,具备相对自治工作的能力,从架构和功能途径方面都是微服务方式。Consul的不同之处在于无需第三方工具就可以原生支持多数据中心和健康检查,这并不意味着使用第三方工具不好。实际上,在这篇博客里我们通过选择那些表现更佳同时不会引入不必要的功能的的工具,尽力组合不同的工具。使用正确的工具可以获得最好的结果。如果工具引入了工作不需要的特性,那么工作效率反而会下降,另一方面,如果工具没有提供工作所需要的特性也是没有用的。Consul很好地权衡了权重,用尽量少的东西很好的达成了目标。Consul使用gossip来传播集群信息的方式,使其比etcd更易于搭建,特别是对于大的数据中心。将存储数据作为服务的能力使其比etcd仅仅只有健/值对存储的特性更加完整、更有用(即使Consul也有该选项)。虽然我们可以在etcd中通过插入多个键来达成相同的目标,Consul的服务实现了一个更紧凑的结果,通常只需要一次查询就可以获得与服务相关的所有数据。除此之外,Registrator很好地实现了Consul的两个协议,使其合二为一,特别是添加Consul-template到了拼图中。Consul的Web UI更是锦上添花般地提供了服务和健康检查的可视化途径。我不能说Consul是一个明确的赢家,而是与etcd相比其有一个轻微的优势。服务发现作为一个概念,以及作为工具都很新,我们可以期待在这一领域会有许多的变化。秉承开放的心态,大家可以对本文的建议持保留态度,尝试不同的工具然后做出自己的结论。原文链接:Service Discovery: Zookeeper vs etcd vs Consul(翻译:胡震)​

libtool: Version mismatch error 解决

在编译一个软件的时候,在 ./configure 和 make  之后可能会出现如下错误: libtool: Version mismatch error. This is libtool 2.4.2 Debian-2.4.2-1ubuntu1, but the libtool: definition of this LT_INIT comes from libtool 2.4. libtool: You should recreate aclocal.m4 with macros from libtool 2.4.2 Debian-2.4.2-1ubuntu1 libtool: and run autoconf again. make[5]: *** 1 Error 63 解决方法很简单:运行 autoreconf -ivf 即可。 另:make clean仅仅是清除之前编译的可执行文件及配置文件。 而make distclean要清除所有生成的文件。Makefile在符合GNU Makefiel惯例的Makefile中,包含了一些基本的预先定义的操作:make根据Makefile编译源代码,连接,生成目标文件,可执行文件。make clean清除上次的make命令所产生的object文件(后缀为“.o”的文件)及可执行文件。make install将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin目录。make dist产生发布软件包文件(即distribution package)。这个命令将会将可执行文件及相关文件打包成一个tar.gz压缩的文件用来作为发布软件的软件包。它会在当前目录下生成一个名字类似“PACKAGE-VERSION.tar.gz”的文件。PACKAGE和VERSION,是我们在configure.in中定义的AM_INIT_AUTOMAKE(PACKAGE, VERSION)。make distcheck生成发布软件包并对其进行测试检查,以确定发布包的正确性。这个操作将自动把压缩包文件解开,然后执行configure命令,并且执行make,来确认编译不出现错误,最后提示你软件包已经准备好,可以发布了。make distclean类似make clean,但同时也将configure生成的文件全部删除掉,包括Makefile。

Mac无法找到摄像头问题解决

facetime显示“未检测到摄像头”之类的,重启后可能摄像头有工作正常了,摄像头不稳定重置 NVRAM后恢复正常,据说机器卡的时候,此法也可以使用。 https://support.apple.com/zh-cn/HT204063如何重置 Mac 上的 NVRAM了解有关电脑的 NVRAM 的信息以及何时及如何重置 NVRAM。 什么是 NVRAM?NVRAM 是一小部分电脑内存,全称“非易失的随机访问存储器”,用于将某些设置存储在 OS X 可快速访问的位置。存储在 NVRAM 中的设置取决于您所使用的 Mac 类型以及该 Mac 所连接的设备类型。存储在 NVRAM 中的信息包括:扬声器音量屏幕分辨率启动磁盘选择最近的内核崩溃信息(如果有)如果您遇到了有关这些功能的问题,则可能需要重置电脑上的 NVRAM。例如,如果 Mac 并非从“启动磁盘”偏好设置中指定的启动磁盘启动,或者 Mac 启动时短暂地出现了一个问号图标。重置 NVRAM关闭 Mac。在键盘上找到以下按键:Command (⌘)、Option、P 和 R。打开 Mac。听到启动声后立即按住 Command-Option-P-R 键。按住这些按键直到电脑重新启动,然后您会再次听到启动声。松开这些按键。重置 NVRAM 后,您可能需要重新配置扬声器音量、屏幕分辨率、启动磁盘选择和时区信息设置。如果在 Mac 台式电脑(如 iMac、Mac mini 或 Mac Pro)上仍然存在与这些功能相关的问题,则可能需要更换其主板电池。台式电脑上的主板电池用于在拔下 Mac 的电源插头时帮助保存 NVRAM 设置。您可以携 Mac 前往 Apple Store 零售店或 Apple 授权服务提供商处更换主板上的电池。了解详情在旧式 Mac 电脑上,类似信息存储在参数 RAM (PRAM) 中。在基于 Intel 的 Mac 上使用相同的组合键重置 NVRAM,就相当于重置 PRAM。与电源相关的设置由 Mac 上的系统管理控制器 (SMC) 控制。如果您遇到了有关电脑供电、睡眠、唤醒、为 Mac 笔记本电脑电池充电的问题或其他电源相关的症状,则可能需要改为重置 SMC。

flatbuffers 使用问题记录

1.  命名空间的问题namespace 1.0.3 版本包含文件类型前面不需要加命名空间,但是1.1.0 中包含需要在类型前加命名空间include必须放在namespace前面例如:include “aa.fbs” namespace IM.test; foo.fbc namespace foo; struct Foo { f: uint; } bar.fbc include "foo.fbc"; namespace bar; struct Bar { foo: Foo; } flatc -c bar.fbc will fail with bar.fbc:3:0: error: structs_ may contain only scalar or struct fields 修改方式: struct Bar { foo: Foo; } -> struct Bar { foo: foo.Foo; } 或者 将 struct 修改成 table struct UserInfo{ user_id:uint; name:string error: structs_ may contain only scalar or struct fields 2. struct 和 table的区别http://www.coder4.com/archives/4386基本类型: 8 bit: byte ubyte bool16 bit: short ushort32 bit: int uint float64 bit: long ulong double复杂类型:数组 (用中括号表示 [type]). 不支持嵌套数组,可以用table实现字符串 string, 支持 UTF-8 或者 7-bit ASCII. 对于其他编码可以用数组 [byte]或者[ubyte]表示。Struct 只支持基本类型或者嵌套StructTable 类似Struct,但是可以支持任何类型。3. roottype的问题及多个table的解决方式https://github.com/google/flatbuffers/issues/65  Why the need for a Root1) a commit was pushed yesterday that adds GetRootAs functions for all tables, not just the root_type.2) generally no. this is a strongly types system, meaning you need to know the kind of buffer you're dealing with. If you want to use this in a context where you want to have multiple different root types, you have these options:a) make your root type a table that contains a union of all possible sub-roots.b) prefix flatbuffers with your own file headerc) use flatbuffer's built-in file indentification feature, which hasn't been ported to Java yet. I'll get to that.3) That's a bug, the 1 should actually read: Any.Monster. I'll fix that.多个消息一个文件中,但是root_type 只能有一个,解决方式如下:namespace TestApp; union Msg {TestObj, Hello} struct KV { key: ulong; value: double; table TestObj { id:ulong; name:string; flag:ubyte = 0; list:[ulong]; kv:KV; table Hello { id:uint; name:string; table RootMsg{ any:Msg; root_type RootMsg;具体样例可以参见:https://github.com/DavadDi/study_example/tree/master/flatbuffers/multi_table4. enum不生成name的前缀flatc -c --no-prefix -b aa.fbs5. 其他问题enum的默认值只能从0开始由于table中的字段全部为可选,因此所有返回指针的地方都必须判断是否为空指针#define STR(ptr) (ptr!=nullptr)?ptr->c_str():"" std::string = STR(user_info->user_name());

protobuf 文件级别优化

package IM.BaseDefine;option java_package = "com.mogujie.tt.protobuf";option optimize_for = LITE_RUNTIME;// service id enum ServiceID{   SID_LOGIN = 0x0001; // for login   SID_BUDDY_LIST = 0x0002; // for friend list   SID_MSG = 0x0003; //   SID_GROUP = 0x0004; // for group message   SID_FILE = 0x0005;   SID_SWITCH_SERVICE = 0x0006;   SID_OTHER = 0x0007;   SID_INTERNAL = 0x0008; }  option optimize_for = LITE_RUNTIME;      optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。      SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。       CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。      LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。      SPEED和LITE_RUNTIME相比,在于调试级别上,例如 msg.SerializeToString(&str) 在SPEED模式下会利用反射机制打印出详细字段和字段值,但是LITE_RUNTIME则仅仅打印字段值组成的字符串;      因此:可以在程序调试阶段使用 SPEED模式,而上线以后使用提升性能使用 LITE_RUNTIME 模式优化。

windows 无法上网问题解决一例

dhcp获取ip地址,网卡驱动和ip地址获取正常,ping www.baidu.com可以ping通,但是打开浏览器或者qq上网不行,而且系统有提示腾讯管家出错的信息,初步怀疑360和腾讯管家打架导致,后采用以下步骤恢复。#ipconfig /release#ipconfig /renew#netsh winsock reset   介绍如下:netsh winsock reset命令,作用是重置 Winsock 目录。如果一台机器上的Winsock协议配置有问题的话将会导致网络连接等问题,就需要用netsh winsock reset命令来重置Winsock目录借以恢复网络。这个命令可以重新初始化网络环境,以解决由于软件冲突、病毒原因造成的参数错误问题。 netsh是一个能够通过命令行操作几乎所有网络相关设置的接口,比如设置IP,DNS,网卡,无线网络等,Winsock是系统内部目录,Winsock是Windows网络编程接口,winsock工作在应用层,它提供与底层传输协议无关的高层数据传输编程接口,reset是对Winsock的重置操作。当执行完winsock的命令重启计算机后,需要重新配置IP。

Google序列化库FlatBuffers 1.1发布,及与protobuf的比较

个人总结:FlatBuffer相对于Protobuffer来讲,优势如下:1. 由于省去了编解码的过程,所以从速度上快于Protobuffer,个人测试结果100w次编解码,编码上FlatBuffer 优势不明显,解码上优势明显2. FlatBuffer的格式文件定义上比Protobuffer格式更丰富3. 使用方便,直接一个头文件就能搞定,这点很赞 劣势:1. FlatBuffer的使用上不如Protobuffer方便,创建类型多了一次转换,这和FlatBuffer提升性能有关2. FlatBuffer的格式定义文件比较灵活,不如Protobuffer直观性好3. 目前项目的稳定度上略为欠缺,Github上issuse还不少 另外:1. FB中的Table中field都为optional,可以指定default value,如果not optional and  no defaults,可以使用struct2. PB中定义message的时候可以使用opitional和required 进行指定 如果对于性能没有迫切要求和通信消息量不大的情况,两者都可以选择。 FlatBuffers 1.1版本下载地址:https://github.com/google/flatbuffers/releases FlatBuffers项目主页:http://google.github.io/flatbuffers/index.html项目在GitHub上的托管地址:https://github.com/google/flatbuffers 经过几个月开发,FlatBuffers 1.1版本更新。这次的更新包含:对Java API进行了广泛的检修out-of-the-box支持C#和Go一个可选的校对器,使FlatBuffers在不可信的情况下变得实用原型解析更容易从协议缓冲区迁移字段ID可选手动分配通过对一个键字段二进制查询的字典功能bug修复和其他的改进FlatBuffers与protobuf性能比较http://blog.csdn.net/menggucaoyuan/article/details/34409433......从以上数据看出,在内存空间占用这个指标上,FlatBuffers占用的内存空间比protobuf多了两倍。序列化时二者的cpu计算时间FB比PB快了3000ms左右,反序列化时二者的cpu计算时间FB比PB快了9000ms左右。FB在计算时间上占优势,而PB则在内存空间上占优(相比FB,这也正是它计算时间比较慢的原因)。 上面的测试环境是在公司的linux server端和我自己的mac pro分别进行的。请手机端开发者自己也在手机端进行下测试, 应该能得到类似的结果。Google宣称FB适合游戏开发是有道理的,如果在乎计算时间我想它也适用于后台开发。 另外,FB大量使用了C++11的语法,其从idl生成的代码接口不如protubuf友好。不过相比使用protobuf时的一堆头文件和占18M之多的lib库,FlatBuffers仅仅一个"flatbuffers/flatbuffers.h"就足够了。一. 什么是Google FlatBuffersFlatBuffers是一个开源的、跨平台的、高效的、提供了C++/Java接口的序列化工具库。它是Google专门为游戏开发或其他性能敏感的应用程序需求而创建。尤其更适用于移动平台,这些平台上内存大小及带宽相比桌面系统都是受限的,而应用程序比如游戏又有更高的性能要求。它将序列化数据存储在缓存中,这些数据既可以存储在文件中,又可以通过网络原样传输,而不需要任何解析开销。 二. 为什么要使用Google FlatBuffers 对序列化数据的访问不需要打包和拆包——它将序列化数据存储在缓存中,这些数据既可以存储在文件中,又可以通过网络原样传输,而没有任何解析开销;内存效率和速度——访问数据时的唯一内存需求就是缓冲区,不需要额外的内存分配。 这里可查看详细的 基准测试;扩展性、灵活性——它支持的可选字段意味着不仅能获得很好的前向/后向兼容性(对于长生命周期的游戏来说尤其重要,因为不需要每个新版本都更新所有数据);最小代码依赖——仅仅需要自动生成的少量代码和一个单一的头文件依赖,很容易集成到现有系统中。再次,看基准部分细节;强类型设计——尽可能使错误出现在编译期,而不是等到运行期才手动检查和修正;使用简单——生成的C++代码提供了简单的访问和构造接口;而且如果需要,通过一个可选功能可以用来在运行时高效解析Schema和类JSON格式的文本;跨平台——支持C++11、Java,而不需要任何依赖库;在最新的gcc、clang、vs2010等编译器上工作良好; 三. 为什么不使用Protocol Buffers的,或者JSONProtocol Buffers的确和FlatBuffers比较类似,但其主要区别在于FlatBuffers在访问数据前不需要解析/拆包这一步。 而且Protocol Buffers既没有可选的文本导入/导出功能,也没有Schemas语法特性(比如union)。 JSON是非常可读的,而且当和动态类型语言(如JavaScript)一起使用时非常方便。然而在静态类型语言中序列化数据时,JSON不但具有运行效率低的明显缺点,而且会让你写更多的代码来访问数据(这个与直觉相反)。 四. 内建的数据类型8 bit: byte ubyte bool16 bit: short ushort32 bit: int uint float64 bit: long ulong doubleVector of any other type (denoted with [type]). Nesting vectors is not supported, instead you can wrap the inner vector in a table.string, which may only hold UTF-8 or 7-bit ASCII. For other text encodings or general binary data use vectors ( [byte] or [ubyte]) instead.References to other tables or structs, enums or unions. 五. 如何使用编写一个用来定义你想序列化的数据的schema文件(又称IDL),数据类型可以是各种大小的int、float,或者是string、array,或者另一对象的引用,甚至是对象集合;各个数据属性都是可选的,且可以设置默认值。使用FlatBuffer编译器flatc生成C++头文件或者Java类,生成的代码里额外提供了访问、构造序列化数据的辅助类。生成的代码仅仅依赖flatbuffers.h;请看 如何生成;使用FlatBufferBuilder类构造一个二进制buffer。你可以向这个buffer里循环添加各种对象,而且很简单,就是一个单一函数调用;保存或者发送该buffer当再次读取该buffer时,你可以得到这个buffer根对象的指针,然后就可以简单的就地读取数据内容;

设置socket接收和发送超时的一种方式

Linux环境设置Socket接收和发送超时:须如下定义:struct timeval timeout = {3,0}; //设置发送超时setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(struct timeval));//设置接收超时setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval)); 另外常用的方式是使用select函数设置fd为读时间,并设置超时时间。#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <strings.h> #include <string.h> #include <thread> #include <unistd.h> extern char *optarg; extern int optind, opterr, optopt; #include <getopt.h> #define LOG_ERROR my_printf int my_printf(char *fmt, ...) char buffer[1024]; va_list argptr; int length = 0; va_start(argptr, fmt); length = vsnprintf(buffer,1024 ,fmt, argptr); va_end(argptr); printf("%s\n", buffer); return (length + 1); int start_client(const char *host, int port, const char *local_host = NULL) int client_socket = socket(AF_INET,SOCK_STREAM,0); if( client_socket < 0) LOG_ERROR("Create socket failed, errno %d", errno); return -1; //设置一个socket地址结构server_addr,代表服务器的internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); server_addr.sin_family = AF_INET; if(inet_aton(host,&server_addr.sin_addr) == 0) LOG_ERROR("Server address inet_aton failed, errno %d!", errno); return -1; if (local_host != NULL) sockaddr_in client_addr; client_addr.sin_family = AF_INET; client_addr.sin_addr.s_addr = inet_addr(local_host); if (bind(client_socket,(struct sockaddr*)&client_addr, sizeof(client_addr)) == -1) LOG_ERROR("\nBind client failed, local_host %s, errno %d, %s\n", local_host, errno, strerror(errno)); close(client_socket); return -1; server_addr.sin_port = htons(port); socklen_t server_addr_length = sizeof(server_addr); //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接 if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0) LOG_ERROR("Connect to %s:%d failed! error %d, %s", host, port, errno, strerror(errno)); close(client_socket); return -1; // write(client_socket, "Hello Server", strlen("Hello Server")); return client_socket; int main() int sock = 0; struct timeval timeout = {3,0}; int tm = 0; int res = 0; char buf[1024]; sock = start_client("127.0.0.1", 5050); setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(struct timeval)); tm = time(0); res = read(sock, buf, 1024); fprintf(stderr, "Read timeout %d\n", time(0) - tm); return 0;

MQTT 开源代理mosquitto的网络层封装相当sucks

最近学习MQTT协议,选择了当前比较流行的MQTT Broker “mosquitto”,但是在阅读代码过程中发现其网络底层库封装的相当差劲。对于MQTT协议的变长头长度的读取上,基本上采取每次一个byte的方式进行读取判断,对于系统调用read的高代价来讲,真的是相当的浪费,也难怪其不能作为高并发的服务器进行处理。  当然mosquitto需要优化的地方还很多:1. 使用poll而不是使用epoll (可能是处于跨平台考虑,如果linux下可以使用epoll替换),同时的就是刚才提到的 byte 读取网络数据2. 订阅树的管理上,对于大量的请求断开或者重练效率比较低3. 空闲空间管理机制优化和数据包发送方式的修改4. 内存管理上malloc new 没有使用mem pool机制,在大并发情况下,内存管理容易出现问题5. 锁遍地飞,如果采用reactor_ 但是从另一个方面讲,mosquitto作为开源的实现,思路上还是比较清晰,为mqtt服务器开发提供了比较完备的参考,这也就是它的价值所在了。#ifdef WITH_BROKER int _mosquitto_packet_read(struct mosquitto_db *db, struct mosquitto *mosq) #else int _mosquitto_packet_read(struct mosquitto *mosq) #endif uint8_t byte; ssize_t read_length; int rc = 0; if(!mosq) return MOSQ_ERR_INVAL; if(mosq->sock == INVALID_SOCKET) return MOSQ_ERR_NO_CONN; if(mosq->state == mosq_cs_connect_pending){ return MOSQ_ERR_SUCCESS; /* This gets called if pselect() indicates that there is network data * available - ie. at least one byte. What we do depends on what data we * already have. * If we've not got a command, attempt to read one and save it. This should * always work because it's only a single byte. * Then try to read the remaining length. This may fail because it is may * be more than one byte - will need to save data pending next read if it * does fail. * Then try to read the remaining payload, where 'payload' here means the * combined variable header and actual payload. This is the most likely to * fail due to longer length, so save current data and current position. * After all data is read, send to _mosquitto_handle_packet() to deal with. * Finally, free the memory and reset everything to starting conditions. if(!mosq->in_packet.command){ read_length = _mosquitto_net_read(mosq, &byte, 1); if(read_length == 1){ mosq->in_packet.command = byte; #ifdef WITH_BROKER # ifdef WITH_SYS_TREE g_bytes_received++; # endif /* Clients must send CONNECT as their first command. */ if(!(mosq->bridge) && mosq->state == mosq_cs_new && (byte&0xF0) != CONNECT) return MOSQ_ERR_PROTOCOL; #endif }else{ if(read_length == 0) return MOSQ_ERR_CONN_LOST; /* EOF */ #ifdef WIN32 errno = WSAGetLastError(); #endif if(errno == EAGAIN || errno == COMPAT_EWOULDBLOCK){ return MOSQ_ERR_SUCCESS; }else{ switch(errno){ case COMPAT_ECONNRESET: return MOSQ_ERR_CONN_LOST; default: return MOSQ_ERR_ERRNO; /* remaining_count is the number of bytes that the remaining_length * parameter occupied in this incoming packet. We don't use it here as such * (it is used when allocating an outgoing packet), but we must be able to * determine whether all of the remaining_length parameter has been read. * remaining_count has three states here: * 0 means that we haven't read any remaining_length bytes * <0 means we have read some remaining_length bytes but haven't finished * >0 means we have finished reading the remaining_length bytes. if(mosq->in_packet.remaining_count <= 0){ read_length = _mosquitto_net_read(mosq, &byte, 1); if(read_length == 1){ mosq->in_packet.remaining_count--; /* Max 4 bytes length for remaining length as defined by protocol. * Anything more likely means a broken/malicious client. if(mosq->in_packet.remaining_count < -4) return MOSQ_ERR_PROTOCOL; #if defined(WITH_BROKER) && defined(WITH_SYS_TREE) g_bytes_received++; #endif mosq->in_packet.remaining_length += (byte & 127) * mosq->in_packet.remaining_mult; mosq->in_packet.remaining_mult *= 128; }else{ if(read_length == 0) return MOSQ_ERR_CONN_LOST; /* EOF */ #ifdef WIN32 errno = WSAGetLastError(); #endif if(errno == EAGAIN || errno == COMPAT_EWOULDBLOCK){ return MOSQ_ERR_SUCCESS; }else{ switch(errno){ case COMPAT_ECONNRESET: return MOSQ_ERR_CONN_LOST; default: return MOSQ_ERR_ERRNO; }while((byte & 128) != 0); /* We have finished reading remaining_length, so make remaining_count * positive. */ mosq->in_packet.remaining_count *= -1; if(mosq->in_packet.remaining_length > 0){ mosq->in_packet.payload = _mosquitto_malloc(mosq->in_packet.remaining_length*sizeof(uint8_t)); if(!mosq->in_packet.payload) return MOSQ_ERR_NOMEM; mosq->in_packet.to_process = mosq->in_packet.remaining_length; while(mosq->in_packet.to_process>0){ read_length = _mosquitto_net_read(mosq, &(mosq->in_packet.payload[mosq->in_packet.pos]), mosq->in_packet.to_process); if(read_length > 0){ #if defined(WITH_BROKER) && defined(WITH_SYS_TREE) g_bytes_received += read_length; #endif mosq->in_packet.to_process -= read_length; mosq->in_packet.pos += read_length; }else{ #ifdef WIN32 errno = WSAGetLastError(); #endif if(errno == EAGAIN || errno == COMPAT_EWOULDBLOCK){ if(mosq->in_packet.to_process > 1000){ /* Update last_msg_in time if more than 1000 bytes left to * receive. Helps when receiving large messages. * This is an arbitrary limit, but with some consideration. * If a client can't send 1000 bytes in a second it * probably shouldn't be using a 1 second keep alive. */ pthread_mutex_lock(&mosq->msgtime_mutex); mosq->last_msg_in = mosquitto_time(); pthread_mutex_unlock(&mosq->msgtime_mutex); return MOSQ_ERR_SUCCESS; }else{ switch(errno){ case COMPAT_ECONNRESET: return MOSQ_ERR_CONN_LOST; default: return MOSQ_ERR_ERRNO; /* All data for this packet is read. */ mosq->in_packet.pos = 0; #ifdef WITH_BROKER # ifdef WITH_SYS_TREE g_msgs_received++; if(((mosq->in_packet.command)&0xF5) == PUBLISH){ g_pub_msgs_received++; # endif rc = mqtt3_packet_handle(db, mosq); #else rc = _mosquitto_packet_handle(mosq); #endif /* Free data and reset values */ _mosquitto_packet_cleanup(&mosq->in_packet); pthread_mutex_lock(&mosq->msgtime_mutex); mosq->last_msg_in = mosquitto_time(); pthread_mutex_unlock(&mosq->msgtime_mutex); return rc;

利用LD_PRELOAD进行hook

原文地址:http://hbprotoss.github.io/posts/li-yong-ld_preloadjin-xing-hook.html 好久没玩hook这种猥琐的东西里,今天在Linux下体验了一把。loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。这和Windows下通过修改import table来hook API很类似。相比较之下,LD_PRELOAD更方便了,都不用自己写代码了,系统的loader会帮我们搞定。但是LD_PRELOAD有个限制:只能hook动态链接的库,对静态链接的库无效,因为静态链接的代码都写到可执行文件里了嘛,没有坑让你填。上代码先是受害者,我们的主程序main.c,通过strcmp比较字符串是否相等:#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) if( strcmp(argv[1], "test") ) printf("Incorrect password\n"); printf("Correct password\n"); return 0; }然后是用来hook的库hook.c:#include <stdio.h> #include <string.h> #include <dlfcn.h> typedef int(*STRCMP)(const char*, const char*); int strcmp(const char *s1, const char *s2) static void *handle = NULL; static STRCMP old_strcmp = NULL; if( !handle ) handle = dlopen("libc.so.6", RTLD_LAZY); old_strcmp = (STRCMP)dlsym(handle, "strcmp"); printf("hack function invoked. s1=<%s> s2=<%s>\n", s1, s2); return old_strcmp(s1, s2); }因为hook的目标是strcmp,所以typedef了一个STRCMP函数指针。由于hook的目的是要控制函数行为,所以需要从原库libc.so.6中拿到“正版”strcmp指针,保存成old_strcmp以备调用。Makefile:test: main.c hook.so gcc -o test main.c hook.so: hook.c gcc -fPIC -shared -o hook.so hook.c -ldl执行:$ LD_PRELOAD=./hook.so ./test 123 hack function invoked. s1=<123> s2=<test> Incorrect password $ LD_PRELOAD=./hook.so ./test test hack function invoked. s1=<test> s2=<test> Correct password其中有一点不理解的是,dlopen打开libc.so.6能拿到“正版”strcmp地址,打开libc.so就是hook后的地址。照理说libc.so不是libc.so.6的一个软链吗?为什么结果会不一样嘞?

ACE学习综述(1)

1. ACE学习综述1.1. ACE项目的优点可以跨平台使用,基本上可以实现一次编写,多平台运行。ACE本身不仅仅是一个简单的网络框架,对于网络框架涉及到的进程管理、线程管理等系统本身相关的内容也进行了统一的封装,甚至消息队列和内存管理等也都有统一封装。代码的质量还是比较高,能经得起长时间运行的考验。代码经过层层封装和模板通用性封装,仍然能够保持较高的性能。1.2. ACE项目的缺点ACE的前身是 《Unix网络编程》,该书页数达上千页,包括了各种网络开发的细节、移植扩展和网络开发架构模式,这就要求使用ACE开发网络的人必须具备了较丰富的网络开发经验。ACE的代码已经比较庞大,且每个版本都有新功能加入,但是相关文档更新不及时。三本开发编程的书也都定位与基本的功能,想深入了解就必须从大量代码中选择代码阅读。由于ACE定位是跨平台开发,各个开发平台上的网络实现和数据结构各不相同,因为代码中充斥了大量的宏来区分,对于阅读代码造成了一定的障碍。学术性较强,设计模式与模板使用相对密集。阅读代码前需要先学习 POSA2 的图书。ACE的类继承关系略显负责,层次较多,不利于学习和分析。1.3. Reactor使用过程中的注意事项相对时间和绝对时间使用,相对超时时间常常用于这样的情况:操作可能会在能够继续执行之前迟滞,但只会被调用一次。 绝对时间常常被用于这样的情况:操作可能会通过循环多次调用。绝对时间的使用使得无需每次循环重新计算超时值。 具体可参考 《C++ 网络编程卷二》 中文 P45,副栏6。handle_*() 函数(Reactor会忽略handle_close的返回值)的返回值,决定着Reactor后续对相关MASK的处理。 == 0 正常处理 > 0 Reactor继续为此事件检测和分配已经登记的时间。 == -1 Reactor应停止对时间处理器已经登记的MASK。 如何跟踪已经注册的事件,则可参考 《C++ 网络编程卷二》 P51, 副栏 10。对于事件处理器的资源释放集中在其 handle_close 函数中进行。如果为动态分配的对象,可以在关闭所有资源后调用 delete this。reactor->remove_handler(eh,mask),如果没有设置 ACE_Event_Handler::DONT_CALL 标志,会调用该 mask 的 handle_close 函数,因此如果在 handle_close 函数中调用 remove_handler 函数,需要添加ACE_Event_Handler::DONT_CALL 标志,避免 handle_close 的递归调用。

Wireshark lua dissector 对TCP消息包合并分析

应用程序发送的数据报都是流式的,IP不保证同一个一个应用数据包会被抓包后在同一个IP数据包中,因此对于使用自制dissector的时候需要考虑这种情况。Lua Dissector相关资料可以见:http://wiki.wireshark.org/Lua/DissectorsLua脚本书写wireshark dissector非常方便,使用Lua合并tcp数据报进行分析的样例如下,其实就是多了一个条件分支,所谓难者不会,会者不难:local slicer = Proto("slicer","Slicer") function slicer.dissector(tvb, pinfo, tree) local offset = pinfo.desegment_offset or 0 local len = get_len() -- for tests i used a constant, but can be taken from tvb while true do local nxtpdu = offset + len if nxtpdu > tvb:len() then pinfo.desegment_len = nxtpdu - tvb:len() pinfo.desegment_offset = offset return tree:add(slicer, tvb(offset, len)) offset = nxtpdu if nxtpdu == tvb:len() then return local tcp_table = DissectorTable.get("tcp.port") tcp_table:add(2506, slicer)对于Lua Dissector脚本使用方法如下:tsharktshark -X lua_script:slicer.lua -i lo0 -f "tcp port 2506" -O aa -VWiresharkOn OSXCopy slicer.lua to ~/.wiresharkAdd dofile(USER_DIR.."slicer.lua") to the end of /Applications/Wireshark.app/Contents/Resources/share/wireshark/init.lua 在wireshark的C语言版本中,有针对tcp合并报的相关函数,packet-tcp.c 具体见下:/* 2152 * Loop for dissecting PDUs within a TCP stream; assumes that a PDU 2153 * consists of a fixed-length chunk of data that contains enough information 2154 * to determine the length of the PDU, followed by rest of the PDU. 2155 * 2156 * The first three arguments are the arguments passed to the dissector 2157 * that calls this routine. 2158 * 2159 * "proto_desegment" is the dissector's flag controlling whether it should 2160 * desegment PDUs that cross TCP segment boundaries. 2161 * 2162 * "fixed_len" is the length of the fixed-length part of the PDU. 2163 * 2164 * "get_pdu_len()" is a routine called to get the length of the PDU from 2165 * the fixed-length part of the PDU; it's passed "pinfo", "tvb" and "offset". 2166 * 2167 * "dissect_pdu()" is the routine to dissect a PDU. 2168 */ 2169 void 2170 tcp_dissect_pdus(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, 2171 gboolean proto_desegment, guint fixed_len, 2172 guint (*get_pdu_len)(packet_info *, tvbuff_t *, int), 2173 dissector_t dissect_pdu) 2174 { 2175 volatile int offset = 0; 2176 int offset_before; 2177 guint length_remaining; 2178 guint plen; 2179 guint length; 2180 tvbuff_t *next_tvb; 2181 proto_item *item=NULL; 2182 void *pd_save; 2184 while (tvb_reported_length_remaining(tvb, offset) != 0) { 2185 /* 2186 * We use "tvb_ensure_length_remaining()" to make sure there actually 2187 * *is* data remaining. The protocol we're handling could conceivably 2188 * consists of a sequence of fixed-length PDUs, and therefore the 2189 * "get_pdu_len" routine might not actually fetch anything from 2190 * the tvbuff, and thus might not cause an exception to be thrown if 2191 * we've run past the end of the tvbuff. 2192 * 2193 * This means we're guaranteed that "length_remaining" is positive. 2194 */ 2195 length_remaining = tvb_ensure_length_remaining(tvb, offset); 2197 /* 2198 * Can we do reassembly? 2199 */ 2200 if (proto_desegment && pinfo->can_desegment) { 2201 /* 2202 * Yes - is the fixed-length part of the PDU split across segment 2203 * boundaries? 2204 */ 2205 if (length_remaining < fixed_len) { 2206 /* 2207 * Yes. Tell the TCP dissector where the data for this message 2208 * starts in the data it handed us and that we need "some more 2209 * data." Don't tell it exactly how many bytes we need because 2210 * if/when we ask for even more (after the header) that will 2211 * break reassembly. 2212 */ 2213 pinfo->desegment_offset = offset; 2214 pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT; 2215 return; 2216 } 2217 } 2219 /* 2220 * Get the length of the PDU. 2221 */ 2222 plen = (*get_pdu_len)(pinfo, tvb, offset); 2223 if (plen < fixed_len) { 2224 /* 2225 * Either: 2226 * 2227 * 1) the length value extracted from the fixed-length portion 2228 * doesn't include the fixed-length portion's length, and 2229 * was so large that, when the fixed-length portion's 2230 * length was added to it, the total length overflowed; 2231 * 2232 * 2) the length value extracted from the fixed-length portion 2233 * includes the fixed-length portion's length, and the value 2234 * was less than the fixed-length portion's length, i.e. it 2235 * was bogus. 2236 * 2237 * Report this as a bounds error. 2238 */ 2239 show_reported_bounds_error(tvb, pinfo, tree); 2240 return; 2241 } 2243 /* 2244 * Do not display the the PDU length if it crosses the boundary of the 2245 * packet and no more packets are available. 2246 * 2247 * XXX - we don't necessarily know whether more packets are 2248 * available; we might be doing a one-pass read through the 2249 * capture in TShark, or we might be doing a live capture in 2250 * Wireshark. 2251 */ 2252 #if 0 2253 if (length_remaining >= plen || there are more packets) 2254 { 2255 #endif 2256 /* 2257 * Display the PDU length as a field 2258 */ 2259 item=proto_tree_add_uint(pinfo->tcp_tree, hf_tcp_pdu_size, 2260 tvb, offset, plen, plen); 2261 PROTO_ITEM_SET_GENERATED(item); 2262 #if 0 2263 } else { 2264 item = proto_tree_add_text(pinfo->tcp_tree, tvb, offset, -1, 2265 "PDU Size: %u cut short at %u",plen,length_remaining); 2266 PROTO_ITEM_SET_GENERATED(item); 2267 } 2268 #endif 2271 /* give a hint to TCP where the next PDU starts 2272 * so that it can attempt to find it in case it starts 2273 * somewhere in the middle of a segment. 2274 */ 2275 if(!pinfo->fd->flags.visited && tcp_analyze_seq) { 2276 guint remaining_bytes; 2277 remaining_bytes=tvb_reported_length_remaining(tvb, offset); 2278 if(plen>remaining_bytes) { 2279 pinfo->want_pdu_tracking=2; 2280 pinfo->bytes_until_next_pdu=plen-remaining_bytes; 2281 } 2282 } 2284 /* 2285 * Can we do reassembly? 2286 */ 2287 if (proto_desegment && pinfo->can_desegment) { 2288 /* 2289 * Yes - is the PDU split across segment boundaries? 2290 */ 2291 if (length_remaining < plen) { 2292 /* 2293 * Yes. Tell the TCP dissector where the data for this message 2294 * starts in the data it handed us, and how many more bytes we 2295 * need, and return. 2296 */ 2297 pinfo->desegment_offset = offset; 2298 pinfo->desegment_len = plen - length_remaining; 2299 return; 2300 } 2301 } 2303 /* 2304 * Construct a tvbuff containing the amount of the payload we have 2305 * available. Make its reported length the amount of data in the PDU. 2306 * 2307 * XXX - if reassembly isn't enabled. the subdissector will throw a 2308 * BoundsError exception, rather than a ReportedBoundsError exception. 2309 * We really want a tvbuff where the length is "length", the reported 2310 * length is "plen", and the "if the snapshot length were infinite" 2311 * length is the minimum of the reported length of the tvbuff handed 2312 * to us and "plen", with a new type of exception thrown if the offset 2313 * is within the reported length but beyond that third length, with 2314 * that exception getting the "Unreassembled Packet" error. 2315 */ 2316 length = length_remaining; 2317 if (length > plen) 2318 length = plen; 2319 next_tvb = tvb_new_subset(tvb, offset, length, plen); 2321 /* 2322 * Dissect the PDU. 2323 * 2324 * Catch the ReportedBoundsError exception; if this particular message 2325 * happens to get a ReportedBoundsError exception, that doesn't mean 2326 * that we should stop dissecting PDUs within this frame or chunk of 2327 * reassembled data. 2328 * 2329 * If it gets a BoundsError, we can stop, as there's nothing more to 2330 * see, so we just re-throw it. 2331 */ 2332 pd_save = pinfo->private_data; 2333 TRY { 2334 (*dissect_pdu)(next_tvb, pinfo, tree); 2335 } 2336 CATCH(BoundsError) { 2337 RETHROW; 2338 } 2339 CATCH(ReportedBoundsError) { 2340 /* Restore the private_data structure in case one of the 2341 * called dissectors modified it (and, due to the exception, 2342 * was unable to restore it). 2343 */ 2344 pinfo->private_data = pd_save; 2345 show_reported_bounds_error(tvb, pinfo, tree); 2346 } 2347 ENDTRY; 2349 /* 2350 * Step to the next PDU. 2351 * Make sure we don't overflow. 2352 */ 2353 offset_before = offset; 2354 offset += plen; 2355 if (offset <= offset_before) 2356 break; 2357 } 2358 }

7-zip 压缩算法及C SDK使用

pdf版本下载:https://files.cnblogs.com/davad/7-zip_and_SDK.pdf 1. 介绍官方网址:中文:http://sparanoid.com/lab/7z/ 英文:http://www.7-zip.org/SDK下载网址:中文:http://sparanoid.com/lab/7z/ 英文:http://www.7-zip.org/sdk.htmlSDK开发支持语言:Java C/C++ C# 缺点:LZMA SDK相关文档不完整. 7-zip当前最新稳定版本为:7-Zip 9.20稳定版,最后更新时间为:2010-11-187-zip当前最新版本为:7-Zip 9.32 alpha,最后更新时间为:2013-12-01 7z 是一种全新的压缩格式,它拥有极高的压缩比。7z 格式的主要特征:l  开放的结构l  高压缩比l  强大的 AES-256 加密l  能够兼容任意压缩、转换、加密算法l  最高支持 16000000000 GB 的文件压缩l  以 Unicode 为标准的文件名l  支持固实压缩l  支持文件头压缩7z 已公开了结构编辑功能,所以它可以支持任何一种新的压缩算法。到目前为止,下列压缩算法已被整合到了 7z 中:压缩算法备注LZMA改良与优化后的   LZ77 算法LZMA2改良的   LZMA 算法PPMD基于   Dmitry Shkarin 的   PPMdH 算法BCJ32 位 x86   可执行文件转换程序BCJ232 位 x86   可执行文件转换程序BZip2标准 BWT   算法Deflate标准   LZ77-based 算法 LZMA 算法是 7z 格式的默认算法。LZMA 算法具有以下主要特征:l  高压缩比l  可变字典大小(最大 4 GB)l  压缩速度:运行于 2 GHz 的处理器可达到 1 MB/秒l  解压缩速度:运行于 2 GHz 的处理器可达到 10-20 MB/秒l  较小的解压缩内存需求(取决于字典大小)l  较小的解压缩代码:约 5 KBl  支持 Pentium 4 的超线程(Hyper-Threading)技术及多处理器 LZMA 压缩算法非常适于应用程序的内嵌。LZMA 发布于 GNU LGPL 许可协议之下,如果您想使用 LZMA 的代码,您可以通过 发送信息到 LZMA 开发部 来咨询和自定义设计代码及制定开发者的使用许可。您也可以点击此处来查看有关 LZMA SDK 的信息: LZMA SDK.7z 是 7-Zip 发布于 GNU LGPL 许可下的子程序。您可从 下载页面 下载 7-Zip 的源代码。支持 7z 压缩格式的应用程序:WinRAR、PowerArchiver、TUGZip、IZArc。 2 LZMA SDK介绍SDK下载网址:中文:http://sparanoid.com/lab/7z/ 英文:http://www.7-zip.org/sdk.htmlSDK开发支持语言:Java C/C++ C#9.20版本下载地址:http://downloads.sourceforge.net/sevenzip/lzma920.tar.bz2,新增用于安装包的精简版 SFX 自释放模块。 3. LZMA SDK代码分布下载lzma920.tar.bz2后,解压目录如下:                           LZMA SDK 包含以下内容:l  C++ source code of LZMA Encoder and Decoderl  C++ source code for .7z compression and decompression (reduced version)l  ANSI-C compatible source code for LZMA / LZMA2 / XZ compression and decompressionl  ANSI-C compatible source code for 7z decompression with examplel  C# source code for LZMA compression and decompressionl  Java source code for LZMA compression and decompressionl  lzma.exe for .lzma compression and decompressionl  7zr.exe to work with 7z archives (reduced version of 7z.exe from 7-Zip)l  ANSI-C and C++ source code in LZMA SDK is subset of source code of 7-Zip. ANSI-C LZMA 解压缩代码是从原始的 C++ 源代码转换到 C。并简化和优化了代码的大小。但它依然和 7-Zip 的 LZMA 完全兼容。 C目录:Util和相对应的文件。Util目录内容如下: 目录名                                    说明                      支持平台7z                              生成可执行程序7z          Linux/WindowsLzma                         生成可执行程序lzma        Linux/WindowsLzmaLib                  生成LZMA.dll动态库          WindowsSfxSetup                 生成可执行程序7zS2.sfx     Windows  CPP目录内容如下: 目录名                                          说明                             支持平台7z                                         生成可执行程序7z           Linux/Windows                                    Windows: CPP\7zip\UI\Client7z -> client7z.exe                                                 CPP\7zip\Bundles\Alone7z –> 7zr.exe                                                 CPP\7zip\Bundles\LzmaCon-> lzma.exe                                   Linux:    CPP\7zip\Bundles\LzmaCon -> lzma Common                       公共包含的文件             Linux/WindowsWindows                 Windows平台下包含的文件    Windows Java目录主要包含7zip.jar和使用的Java源代码 结论:对于Linux下程序集成开发采用C语言SDK更加方便。4. 使用LZMA C SDKC版本SDK已经实现了针对输入文件压缩和解压缩的功能,具体功能在:C/Util/Lzma/LzmaUtil.c中的main2函数中实现,可以从main函数中直接调用。int main2(int numArgs, const char *args[], char *rs) 实现lzma程序的main函数如下:int MY_CDECL main(int numArgs, const char *args[]) char rs[10*1024*1024] = { 0 }; // 用于中间过程的内存,原始大小为80K int res = main2(numArgs, args, rs); fputs(rs, stdout); return res; }对于lzma程序来讲,使用帮助如下:lzma <e|d> inputFile outputFile             e: encode file             d: decode file 因此如果使用文件解压缩的话,只需要将LzmaUtil.c中的main函数使用宏定义控制,将相关文件编译成动态库使用即可。 例如解压缩函数可定义如下:int decode_file(const char *in_file_name, const char* out_file_name) char buf[800]; char *argvs[4]; argvs[0] = NULL; argvs[1] = "d"; argvs[2] = in_file_name; argvs[3] = out_file_name; return main2(4, argvs, buf); }压缩函数只需要将argv[1]=”d”替换成,argv[1]=”e”即可

Prolog奇怪奇妙的思考方式

今天在《七周七语言》中接触到了prolog,发现它的编程模式和思考方式的确比较奇怪,但同时也非常奇妙,值得学习一下。1. prolog语言介绍    和SQL一样,Prolog基于数据库,但是其数据由逻辑规则和关系组成;和SQL一样,Prolog包含两个部分:一部分用于描述数据,而另一部分则用于查询数据。在Prolog中,数据以逻辑规则的形式存在,下面是基本构建单元。 事实:事实是关于真实世界的基本断言。(Babe是一头猪,猪喜欢泥巴。)    规则:规则是关于真实世界中一些事实的推论。(如果一个动物是猪,那么它喜欢泥巴。) 查询:查询是关于真实世界的一个问题。(Babe喜欢泥巴吗?)    事实和规则被放入一个知识库(knowledge base)。Prolog编译器将这个知识库编译成一种适于高效查询的形式。2. 语言编译器gprolog 本文使用的编译器为:gprolog,下载地址 ftp://gprolog.univ-paris1.fr/pub/gprolog/gprolog-1.4.4.tar.gz,     使用开源软件 configure && make && make install即可3. 谁是谁的爸爸? 样例来源:http://fengdidi.github.io/blog/2011/11/16/di-2zhang-shui-shi-shui-de-ba-ba/     推荐阅读:http://fengdidi.github.io/blog/archives/    假设我们有这样一个家谱图:               我们现在的任务是将这个家谱图写成程序代码的形式。请打开你最喜欢的文本编辑器,输入以下代码。       father.pl:male(di). male(jianbo). female(xin). female(yuan). female(yuqing). father(jianbo,di). father(di,yuqing). mother(xin,di). mother(yuan,yuqing). grandfather(X,Y):-father(X,Z),father(Z,Y). grandmother(X,Y):-mother(X,Z),father(Z,Y).

花生壳6.5工程版原理简析

花生壳最近推出了6.5工程版本,主要功能为:1. 无需公网IP 2. 无需路由端口映射,其实就是简化了在路由器上设置端口映射的操作步骤,有点类似于TeamView的味道了。具体地址见:http://www.oray.com/peanuthull/download_ddns_6.5.php相关简单教程见:http://service.oray.com/question/1360.html 本着好奇的心理,通过网络抓包等方式进行了一个简单了解,仅是针对原理分析,具体细节可能不准确,如下:1. 相关客户端程序的网络连接情况如下: 2. 域名动态请求简易流程         本文只是个人一点浅薄分析,如有遗漏或者错误欢迎补充。

数据备份和恢复方案(1)

数据备份和恢复图书:《Backup & Recovery: Inexpensive Backup Solutions forOpen Systems》       1. 开源的网络备份软件bacula  官方网址:http://www.bacula.org/ 目前最新版本为5.2.13,下载地址: http://sourceforge.net/projects/bacula/files/bacula/5.2.13/     类似的商业备份软件:legato ,ARCserveIT, Arkeia, 或者 PerfectBackup+  Bacula是一款能够由管理员控制数据备份,恢复,完整性验证,并跨不同种网络进行数据传递的软件。它基于Client/Server模式的备份软件,容易使用,效率高,提供许多高级存储管理功能,使得它能够很容易的发现并恢复丢失的或已经损坏的文件。采用的模块化设计使得bacula可以满足从单独的备份恢复管理系统升级到由上百台计算机组成的大型备份网络。  支持的多种备份方式:  完全备份  差异备份  增量备份  支持多种恢复方式  恢复某个目录到指定位置,恢复时会自动恢复其父目录树结构。  恢复某个文件到指定位置,恢复时会自动恢复其父目录树结构。  恢复某个时间之后的备份到指定位置,恢复时会自动恢复其父目录树结构。  恢复所有备份的数据到指定位置,恢复时会自动恢复其父目录树结构。  恢复的文件和目录都保持原有的权限和属主,访问时间等。  恢复winNT/winXp/win2K时,会保存原属主和权限。      支持广泛的备份的介质支持把备份写到硬盘文件中;也支持写到磁带中;支持写到dvd光盘中;支持光纤存储阵列。  支持多种操作系统Linux(We have successfully compiled and used Bacula on RH8.0/RH9/RHEL 3.0/FC3 with GCC 3.4)UnixMacWindows versions (Win98, WinMe, WinNT, WinXP, Win2000, and Windows 2003 systems)(备份win,还不支持恢复到win)  支持备份与恢复多种文件系统:   ext2, jfs, ntfs, proc, reiserfs, xfs, usbdevfs, sysfs, smbfs, iso9660. For ext3 systems, use ext2.   内部功能强大终端控制命令强大;可以配合编写shell程序完成自动话备份和恢复;自动绘制各种图形报表(备份报表,恢复报表);原备份文件丢失可以做到自动寻找可用备份文件,完成备份数据的传输;针对碎片文件(又名稀疏文件),可先进行优化然后备份,恢复后自动整理碎片文件;支持MD5和SHA1两种签名校验;支持对硬连接的备份控制;支持正则表达式,可控制是否备份某个符合条件的目录或文件;支持Unix ACL特别权限备份;定时备份,无须手动干预;支持针对整个操作系统(包括Unix,Windows,Linux)的备份;     待续

boost1.53中的lock-free

原始网站:http://www.boost.org/doc/libs/1_53_0/doc/html/lockfree.html  前言  Boost1.53版本中新增加了lock-free库,终于有这么一款官方的lock-free结构出来了。以前在做高性能服务器处理的时候自己费了不少精力去网上搜索代码测试,不仅浪费精力而且可靠性还不敢保证。当然在项目中我使用最多的还是one-one的circle-buffer的方式,其实也就是boost::lockfree::spsc_queue,lock-free结构减少了大量的系统调用,因此特定场合下提升的性能还是非常明显的。总而言之,Boost的lock-free库还是比较值得期待和学习的。  Non-blocking数据结构不再依赖于locks和mutexes去保证线程的安全。同步的操作完全在user-space完成,不需要直接和操作系统交互。不再依赖于guards,non-blocking数据结构需要atomic operations,尤其是CPU执行中不被中断。并不是所有的硬件支持相同系列的 atomic instructions,硬件不支持的将会使用gurard来进行模拟,当然这已经失去了lock-free的优势。     影响lock-free的三种情况:      Atomic Operations: 硬件提供atomic操作,这时候将会用spinlocks进行模拟,则会导致操作被block。      Memory Allocations:操作系统的内存分配不是lock-free的。因此没有办法去实现真正的dynamically-sized的数据结构。boost.lockfree使用memory pool去分配内部的nodes。如果memory pool耗尽,则新节点的内存需要向操作系统进行索取导致内存分配。但是对于boost.lockfree可以采用special call访问失败的方式来避免内存分配。见后续lock-free内存相关设置。  Exception Handling:C++ exception处理没有任何保证其real-time处理的行为。因此我们不鼓励在excetion的处理中使用lock-free代码。     Boost.lockfree实现了三种数据结构:   boost::lockfree::queue: a lock-free multi-produced/multi-consumer queue     boost::lockfree::stack: a lock-free multi-produced/multi-consumer stack     boost::lockfree::spsc_queue: a wait-free single-producer/single-consumer queue (commonly known as ringbuffer)        lock-free内存相关设置:boost::lockfree::fixed_sized, defaults to boost::lockfree::fixed_sized<false> Can be used to completely disable dynamic memory allocations during push in order to ensure lockfree behavior. If the data structure is configured as fixed-sized, the internal nodes are stored inside an array and they are addressed by array indexing. This limits the possible size of the queue to the number of elements that can be addressed by the index type (usually 2**16-2), but on platforms that lack double-width compare-and-exchange instructions, this is the best way to achieve lock-freedom.boost::lockfree::capacity, optional If this template argument is passed to the options, the size of the queue is set at compile-time. It this option implies fixed_sized<true>boost::lockfree::allocator, defaults to boost::lockfree::allocator<std::allocator<void>> Specifies the allocator that is used for the internal freelist         Example#include <boost/thread/thread.hpp> #include <boost/lockfree/queue.hpp> #include <iostream> #include <boost/atomic.hpp> boost::atomic_int producer_count(0); boost::atomic_int consumer_count(0); boost::lockfree::queue<int> queue(128); const int iterations = 10000000; const int producer_thread_count = 4; const int consumer_thread_count = 4; void producer(void) for (int i = 0; i != iterations; ++i) { int value = ++producer_count; while (!queue.push(value)) boost::atomic<bool> done (false); void consumer(void) int value; while (!done) { while (queue.pop(value)) ++consumer_count; while (queue.pop(value)) ++consumer_count; int main(int argc, char* argv[]) using namespace std; cout << "boost::lockfree::queue is "; if (!queue.is_lock_free()) cout << "not "; cout << "lockfree" << endl; boost::thread_group producer_threads, consumer_threads; for (int i = 0; i != producer_thread_count; ++i) producer_threads.create_thread(producer); for (int i = 0; i != consumer_thread_count; ++i) consumer_threads.create_thread(consumer); producer_threads.join_all(); done = true; consumer_threads.join_all(); cout << "produced " << producer_count << " objects." << endl; cout << "consumed " << consumer_count << " objects." << endl; }#include <boost/thread/thread.hpp> #include <boost/lockfree/stack.hpp> #include <iostream> #include <boost/atomic.hpp> boost::atomic_int producer_count(0); boost::atomic_int consumer_count(0); boost::lockfree::stack<int> stack(128); const int iterations = 1000000; const int producer_thread_count = 4; const int consumer_thread_count = 4; void producer(void) for (int i = 0; i != iterations; ++i) { int value = ++producer_count; while (!stack.push(value)) boost::atomic<bool> done (false); void consumer(void) int value; while (!done) { while (stack.pop(value)) ++consumer_count; while (stack.pop(value)) ++consumer_count; int main(int argc, char* argv[]) using namespace std; cout << "boost::lockfree::stack is "; if (!stack.is_lock_free()) cout << "not "; cout << "lockfree" << endl; boost::thread_group producer_threads, consumer_threads; for (int i = 0; i != producer_thread_count; ++i) producer_threads.create_thread(producer); for (int i = 0; i != consumer_thread_count; ++i) consumer_threads.create_thread(consumer); producer_threads.join_all(); done = true; consumer_threads.join_all(); cout << "produced " << producer_count << " objects." << endl; cout << "consumed " << consumer_count << " objects." << endl; }#include <boost/thread/thread.hpp>#include <boost/lockfree/spsc_queue.hpp> #include <iostream>#include <boost/atomic.hpp>int producer_count = 0; boost::atomic_int consumer_count (0); boost::lockfree::spsc_queue<int, boost::lockfree::capacity<1024> > spsc_queue; const int iterations = 10000000;void producer(void){ for (int i = 0; i != iterations; ++i) { int value = ++producer_count; while (!spsc_queue.push(value)) ; }} boost::atomic<bool> done (false);void consumer(void){ int value; while (!done) { while (spsc_queue.pop(value)) ++consumer_count; } while (spsc_queue.pop(value)) ++consumer_count;}int main(int argc, char* argv[]){ using namespace std; cout << "boost::lockfree::queue is "; if (!spsc_queue.is_lock_free()) cout << "not "; cout << "lockfree" << endl; boost::thread producer_thread(producer); boost::thread consumer_thread(consumer); producer_thread.join(); done = true; consumer_thread.join(); cout << "produced " << producer_count << " objects." << endl; cout << "consumed " << consumer_count << " objects." << endl;}