<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; OS</title>
	<atom:link href="https://blog.gmem.cc/category/work/os/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>eBPF学习笔记</title>
		<link>https://blog.gmem.cc/ebpf</link>
		<comments>https://blog.gmem.cc/ebpf#comments</comments>
		<pubDate>Fri, 22 Jan 2021 09:29:27 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[eBPF]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37035</guid>
		<description><![CDATA[<p>简介 BPF，即Berkeley Packet Filter，是一个古老的网络封包过滤机制。它允许从用户空间注入一段简短的字节码到内核来定制封包处理逻辑。Linux从2.5开始移植了BPF，tcpdump就是基于BPF的应用。 所谓eBPF（extended BPF），则是从3.18引入的，对BPF的改造和功能增强： 使用类似于X86的体系结构，eBPF设计了一个通用的RISC指令集，支持11个64bit寄存器（32bit子寄存器）r0-r10，使用512字节的栈空间 引入了JIT编译，取代了BPF解释器。eBPF程序直接被编译为目标体系结构的机器码 和网络子系统进行了解耦。它的数据模型是通用的，eBPF程序可以挂钩到[crayon-69e2a15482d97725262514-i/]或[crayon-69e2a15482d9e587795059-i/] 使用Maps来存储全局数据，这是一种通用的键值存储。可用作不同eBPF程序、eBPF和用户空间程序的状态共享 助手函数（Helper Functions），这些函数供eBPF程序调用，可以实现封包改写、Checksum计算、封包克隆等能力 尾调用（Tail Calls），可以用于将程序控制权从一个eBPF转移给另外一个。老版本的eBPF对程序长度有4096字节的限制，通过尾调用可以规避 用于Pin对象（Maps、eBPF程序）的伪文件系统 支持将eBPF Offload给智能硬件的基础设施 以上增强，让eBPF不仅仅限于网络封包处理，当前eBPF的应用领域包括： 网络封包处理：XDP、TC、socket progs、kcm、calico、cilium等 内核跟踪和性能监控：KProbes、UProbes、TracePoints 安全领域：Secomp、landlock等。例如阻止部分类型的系统调用 <a class="read-more" href="https://blog.gmem.cc/ebpf">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ebpf">eBPF学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>BPF，即Berkeley Packet Filter，是一个古老的网络封包过滤机制。它允许从用户空间<span style="background-color: #c0c0c0;">注入一段简短的字节码到内核</span>来定制封包处理逻辑。Linux从2.5开始移植了BPF，tcpdump就是基于BPF的应用。</p>
<p>所谓eBPF（extended BPF），则是从3.18引入的，对BPF的改造和功能增强：</p>
<ol>
<li>使用类似于X86的体系结构，eBPF设计了一个通用的RISC指令集，支持11个64bit寄存器（32bit子寄存器）r0-r10，使用512字节的栈空间</li>
<li><span style="background-color: #c0c0c0;">引入了JIT编译</span>，取代了BPF解释器。eBPF程序直接被编译为目标体系结构的机器码</li>
<li>和网络子系统进行了解耦。它的数据模型是通用的，eBPF程序可以挂钩到<pre class="crayon-plain-tag">Kprobe</pre>或<pre class="crayon-plain-tag">Tracepoint</pre></li>
<li>使用Maps来存储全局数据，这是一种通用的键值存储。可用作不同eBPF程序、eBPF和用户空间程序的<span style="background-color: #c0c0c0;">状态共享</span></li>
<li>助手函数（Helper Functions），这些函数供eBPF程序调用，可以实现封包改写、Checksum计算、封包克隆等能力</li>
<li>尾调用（Tail Calls），可以用于将程序控制权从一个eBPF转移给另外一个。老版本的eBPF对程序长度有4096字节的限制，通过尾调用可以规避</li>
<li>用于Pin对象（Maps、eBPF程序）的<span style="background-color: #c0c0c0;">伪文件系统</span></li>
<li>支持将eBPF Offload给智能硬件的基础设施</li>
</ol>
<p>以上增强，让eBPF不仅仅限于网络封包处理，当前eBPF的应用领域包括：</p>
<ol>
<li>网络封包处理：XDP、TC、socket progs、kcm、calico、cilium等</li>
<li>内核跟踪和性能监控：KProbes、UProbes、TracePoints</li>
<li>安全领域：Secomp、landlock等。例如阻止部分类型的系统调用</li>
</ol>
<p>现在BPF一般都是指eBPF，而老的BPF一般称为cBPF（classic BPF）。</p>
<p>性能是eBPF的另外一个优势，由于<span style="background-color: #c0c0c0;">所有代码都在内核空间运行，避免了复制数据到用户空间、上下文切换等开销</span>。甚至编译过程都在尽可能的优化，例如助手函数会被内联到eBPF程序中，避免函数调用的开销。</p>
<p>用户提供的代码在内核中运行，安全性需要得到保证。eBPF校验器会对字节码进行各方面的检查，确保它不会导致内核崩溃或锁死。</p>
<p>eBPF具有非常好的灵活性、动态性，可以随时的注入、卸载，不需要重启内核或者中断网络连接。</p>
<p>eBPF程序可以在不同体系结构之间移植。</p>
<div class="blog_h1"><span class="graybg">eBPF基础</span></div>
<div class="blog_h2"><span class="graybg">BPF架构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/05/ebpf-arch.jpg"><img class=" wp-image-37049 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/05/ebpf-arch.jpg" alt="ebpf-arch" width="995" height="543" /></a>如上图所示，eBPF应用程序，从开发到运行的典型流程如下：</p>
<ol>
<li>
<p>利用Clang，将C语言开发的代码编译为eBPF object文件</p>
</li>
<li>
<p>在用户空间将eBPF object文件载入内核。载入前，可能对object文件进行各种修改。这一步骤，可能通过iproute2之类的BPF ELF loader完成，也可能通过自定义的控制程序完成</p>
</li>
<li>
<p>BPF Verifier在VM中进行安全性校验</p>
</li>
<li>
<p>JIT编译器将字节码编译为机器码，返回BPF程序的文件描述符</p>
</li>
<li>使用文件描述符将BPF程序挂钩到某个子系统（例如networking）的挂钩点。子系统有可能将BPF程序offload给硬件（例如智能网卡）</li>
<li>
<p>用户空间通过eBPF Map和内核空间交换数据，获知eBPF程序的执行结果</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">挂钩点</span></div>
<p>eBPF程序以事件驱动的方式执行，具体来说，就是在<span style="background-color: #c0c0c0;">内核的代码路径上，存在大量挂钩点</span>（Hook Point）。eBPF程序会注册到某些挂钩点，当内核运行到挂钩点后，就执行eBPF程序。</p>
<p>挂钩点主要包括以下几类：</p>
<ol>
<li>网络事件，例如封包到达</li>
<li>Kprobes / Uprobes</li>
<li>系统调用</li>
<li>函数的入口/退出点</li>
</ol>
<div class="blog_h2"><span class="graybg">BPF Verifier</span></div>
<p>在加载之后，BPF校验器负责验证eBPF程序是否安全，它会模拟所有的执行路径，并且：</p>
<ol>
<li>检查程序控制流，发现循环</li>
<li>检测越界的跳转、不可达指令</li>
<li>跟踪Context的访问、栈移除</li>
<li>检查unpriviledged的指针泄漏</li>
<li>检查助手函数调用参数</li>
</ol>
<div class="blog_h2"><span class="graybg">BPF JITs</span></div>
<p>在校验之后，eBPF程序被JIT编译器编译为Native代码。</p>
<div class="blog_h2"><span class="graybg">BPF Maps</span></div>
<p>键值对形式的存储，通过文件描述符来定位，值是不透明的Blob（任意数据）。用于跨越多次调用共享数据，或者与用户空间应用程序共享数据。</p>
<p>一个eBPF程序可以<span style="background-color: #c0c0c0;">直接访问最多64个Map</span>，多个eBPF程序可以共享同一Map。</p>
<div class="blog_h2"><span class="graybg">Pinning</span></div>
<p>BPF Maps和程序都是内核资源，仅能通过文件描述符访问到。文件描述符对应了内核中的匿名inodes。</p>
<p>用户空间程序可以使用大部分基于文件描述符的API，但是<span style="background-color: #c0c0c0;">文件描述符是限制在进程的生命周期内的</span>，这导致Map难以被共享。比较显著的例子是iproute2，当tc或XDP加载eBPF程序之后，自身会立刻退出。这导致无法从用户空间访问Map。</p>
<p>为了解决上面的问题，引入了一个最小化的、<span style="background-color: #c0c0c0;">内核空间中的BPF文件系统。BPF程序和Map会被pin到一个被称为object pinning的进程</span>。bpf系统调用有两个命令BPF_OBJ_PIN、BPF_OBJ_GET分别用于钉住、取回对象。</p>
<p>tc这样的工具就是利用Pinning在ingress/egress端共享Map。</p>
<div class="blog_h2"><span class="graybg">尾调用</span></div>
<p>尾调用允许一个BPF程序调用另外一个，这种调用<span style="background-color: #c0c0c0;">没有函数调用那样的开销</span>。其实现方式是long jump，重用当前stack frame。</p>
<p>注意：只用相同类型的BPF程序才能相互尾调用。</p>
<p>要使用尾调用，需要一个BPF_MAP_TYPE_PROG_ARRAY类型的Map，其内容目前必须由用户空间产生，值是需要被尾调用的BPF程序的文件描述符。通过助手函数bpf_tail_call触发尾调用，内核会将此调用内联到一个特殊的BPF指令。</p>
<div class="blog_h2"><span class="graybg">BPF-BPF调用</span></div>
<p>BPF - BPF调用是一个新添加的特性。在此特性引入之前，典型的BPF C程序需要将所有可重用的代码声明为always_inline的，这样才能确保LLVM生成的object包含所有函数。这会导致函数在每个object文件中都反复（只要它被调用超过一次）出现，增加体积。</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

// 总是内联
static __inline int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";</pre>
<p>总是需要内联的原因是BPF的Loader/Verifier/Interpreter/JITs不支持函数调用。但是从<span style="background-color: #c0c0c0;">内核4.16和LLVM 6.0开始，此限制消除</span>，BPF程序不再总是需要always_inline。上面程序的__inline可以去掉了。</p>
<p>目前x86_64/arm64的JIT编译器支持BPF to BPF调用，这是很重要的性能优化，因为它大大简化了生成的object文件的尺寸，对CPU指令缓存更加友好。</p>
<p>JIT编译器<span style="background-color: #c0c0c0;">为每个函数生成独立的映像</span>（Image），并且<span style="background-color: #c0c0c0;">在JIT的最后一个步骤中修复映像中的函数调用地址</span>。</p>
<p>到5.9为止，你不能同时使用BPF-BPF调用（BPF子程序）和尾调用。从5.10开始，可以混合使用，但是仍然存在一些限制。此外，<span style="background-color: #c0c0c0;">混合使用两者可能导致内核栈溢出</span>，原因是尾调用在跳转之前仅会unwind当前栈帧。</p>
<div class="blog_h2"><span class="graybg">Offloading</span></div>
<p>BPF网络程序，特别是tc和XDP，提供了将BPF代码offload给NIC执行的特性。这个特性需要驱动的支持。</p>
<div class="blog_h2"><span class="graybg">BPF前端工具</span></div>
<p>能够加载BPF程序的前端工具有很多，包括bcc、perf、iproute2等。内核也在tools/lib/bpf目录下提供了用户空间库，被perf用来加载BPF追踪应用程序到内核。这是一个通用的库，你也可以直接调用它。BCC是面向追踪的工具箱。内核在samples/bpf下也提供了一些BPF示例，这些示例解析Object文件，并且直接通过系统调用将其载入内核。</p>
<p>基于不同前端工具，实现BPF程序的语法、语义（例如对于段名的约定）有所不同。</p>
<div class="blog_h2"><span class="graybg">相关sysctl</span></div>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_enable</span></div>
<p>启用或禁用BPF JIT编译器：</p>
<p style="padding-left: 30px;">0 仅用，仅仅使用解释器，默认值<br />1 启用JIT编译器<br />2 启用JIT编译器并且生成debugging trace到内核日志</p>
<p>设置为2，可以使用bpf_jit_disasm处理debugging trace</p>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_harden</span></div>
<p>启用或禁用JIT加固，加固和性能是对立的，但是可以缓和JIT spraying：</p>
<p style="padding-left: 30px;">0 禁用JIT加固，默认值<br />1 对非特权用户启用<br />2 对所有用户启用</p>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_kallsyms</span></div>
<p>启用或禁用JITed的程序的内核符号导出（导出到/proc/kallsyms），这样可以和perf工具一起使用，还能够让内核对BPF程序的地址感知（用于stack unwinding）：</p>
<p style="padding-left: 30px;">0 启用<br />1 仅对特权用户启用</p>
<div class="blog_h3"><span class="graybg">/proc/sys/kernel/unprivileged_bpf_disabled</span></div>
<p>是否启用非特权的bpf系统调用。默认启用，一旦禁用，重启前无法恢复启用状态。不会影响seccomp等不使用bpf2系统调用的cBPF程序：</p>
<p style="padding-left: 30px;">0 启用<br />1 禁用</p>
<div class="blog_h2"><span class="graybg">助手函数</span></div>
<p>eBPF程序可以调用助手函数，完成各种任务，例如：</p>
<ol>
<li>在Map中搜索、更新、删除键值对</li>
<li>生成伪随机数</li>
<li>读写隧道元数据</li>
<li>尾调用 —— 将eBPF程序链在一起</li>
<li>执行套接字相关操作，例如绑定、查询Cookies、重定向封包</li>
<li>打印调试信息</li>
<li>获取系统启动到现在的时间</li>
</ol>
<p>助手函数是定义在内核中的，有一个白名单，决定哪些内核函数可以被eBPF程序调用。</p>
<p>根据eBPF的约定，<span style="background-color: #c0c0c0;">助手函数的参数数量不超过5</span>。</p>
<p>编译后，助手函数的代码是内联到eBPF程序中的，因而不存在函数调用的开销（栈帧处理开销、CPU流水线预取指令失效开销）。</p>
<p>返回int的类型的助手函数，通常操作成功返回0，否则返回负数。如果不是如此，会特别说明。</p>
<p>助手函数不可以随意调用，不同类型的eBPF程序，可以调用不同的助手函数子集。</p>
<div class="blog_h1"><span class="graybg">iproute2</span></div>
<p>iproute2提供的BPF前端，主要用来载入BPF网络程序，这些程序的类型包括XDP、tc、lwt。只要是为iproute2编写的BPF程序，共享统一的加载逻辑。</p>
<div class="blog_h2"><span class="graybg">XDP</span></div>
<div class="blog_h3"><span class="graybg">加载XDP程序</span></div>
<p>编译好的XDP类型（BPF_PROG_TYPE_XDP）的BPF程序 ，可以使用如下命令载入到支持XDP的网络设备：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp obj prog.o</pre>
<p>上述命令假设程序位于名为prog的段中。如果不使用默认段名，则需要指定sec参数：</p>
<pre class="crayon-plain-tag"># 如果程序放在foobar段
ip link set dev em1 xdp obj prog.o sec foobar</pre>
<p>如果程序没有标注段，也就是位于默认的.text段，则也可以用上面的命令加载。</p>
<p>如果已经存在挂钩到网络设备的XDP程序，默认情况下命令会报错，可以用-force参数强制替换：</p>
<pre class="crayon-plain-tag">ip -force link set dev em1 xdp obj prog.o</pre>
<p>大多数支持XDP的驱动，能够原子的替换XDP程序，而不会引起流量中断。出于性能的考虑同时只能有一个XDP程序挂钩， 可以利用前文提到的尾调用来组织多个XDP程序。</p>
<p>如果网络设备挂钩了XDP程序，则ip link命令会显示xdp标记和程序的ID。使用bpftool传入ID可以查看更多细节信息。</p>
<div class="blog_h3"><span class="graybg">卸载XDP程序</span></div>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp off</pre>
<div class="blog_h3"><span class="graybg">XDP操作模式</span></div>
<p>iproute2实现了XDP所支持的三种操作模式：</p>
<ol>
<li>xdpdrv：即native XDP，<span style="background-color: #c0c0c0;">BPF程序在驱动的接收路径的最早时刻被调用</span>。这是正常的XDP模式，上游内核的所有主要10G/40G+网络驱动（包括virtio）都实现了XDP支持，也就是可使用该模式</li>
<li>xdpoffload：由智能网卡的驱动（例如Netronome的nfp驱动）实现，将整个XDP程序offload到硬件中，网卡每接收到封包都会执行XDP程序。该模式比native XDP的性能更高，缺点是，并非所有助手函数、Map类型可用。</li>
<li>xdpgeneric：即generic XDP，作为尚不支持native XDP的驱动的试验台。挂钩点比native XDP晚很多，已经进入网络栈的主接收路径，生成了skb对象，因此性能比native XDP差很多，不会用于生产环境</li>
</ol>
<p>在切换驱动的XDP模式时，驱动通常需要重新配置它的接收/发送Rings，以保证接收到的封包线性的（linearly）存放到单个内存页中。</p>
<p>调用ip link set dev xxx xdp命令时，内核会首先尝试在native XDP模式下载入，如果驱动不支持，则自动使用generic XDP模式。要强制使用native XDP，则可以使用：</p>
<pre class="crayon-plain-tag">#                           强制使用native XDP
ip -force link set dev eth0 xdpdrv obj prog.o</pre>
<p>使用类似的方式可以强制使用xdpgeneric、xdpoffload。</p>
<p><span style="background-color: #c0c0c0;">切换操作模式目前不能原子的进行，但是在单个操作模式下替换XDP程序则可以</span>。</p>
<p>使用<pre class="crayon-plain-tag">verb</pre>选项，可以显示详尽的BPF校验日志：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp obj xdp-example.o verb</pre>
<p>除了从文件加载BPF程序，也可以直接从BPF伪文件系统中得到程序并使用：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp pinned /sys/fs/bpf/prog
#                               m:表示BPF文件系统的挂载点，默认/sys/fs/bpf/
ip link set dev eth0 xdp pinned m:prog</pre>
<div class="blog_h2"><span class="graybg">tc</span></div>
<p>对于为tc设计的BPF程序（BPF_PROG_TYPE_SCHED_CLS、BPF_PROG_TYPE_SCHED_ACT），可以使用tc命令加载并挂钩到网络设备。和XDP不同，tc程序没有对驱动的依赖。</p>
<p><pre class="crayon-plain-tag">clsact</pre>是4.1引入了一个特殊的dummy qdisc，它持有classifier和action，但是不能执行实际的queueing。要挂钩BPF classifier，clsact是必须启用的：</p>
<pre class="crayon-plain-tag">tc qdisc add dev eth0 clsact</pre>
<p>clsact提供了两个特殊的钩子<pre class="crayon-plain-tag">ingress</pre>、<pre class="crayon-plain-tag">egress</pre>，对应了BPF classifier可用的两个挂钩点。这两个钩子位于网络数据路径的中心位置，任何封包都必须经过</p>
<p>下面的命令，将BPF程序挂钩到eth0的ingress路径上：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 ingress bpf da obj prog.o</pre>
<p>下面的命令将BPF程序挂钩到eth0的egress路径上：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 egress bpf da obj prog.o </pre>
<p>ingress钩子在内核中由<pre class="crayon-plain-tag">__netif_receive_skb_core() -&gt; sch_handle_ingress()</pre>调用。</p>
<p>egress钩子在内核中由<pre class="crayon-plain-tag">__dev_queue_xmit() -&gt; sch_handle_egress()</pre>调用。</p>
<p><span style="background-color: #c0c0c0;">clsact是以无锁方式处理的</span>，<span style="background-color: #c0c0c0;">支持挂钩到虚拟的、没有队列概念的网络设备，例如veth</span>。</p>
<p><pre class="crayon-plain-tag">da</pre>即direct-action模式，这是推荐的模式，应当总是在命令中指定。da模式表示BPF classifier不需要调用外部的tc action模块，因为BPF程序会将封包修改、转发或者其它动作都完成，这正是BPF性能优势所在。</p>
<p>类似XDP，如果不使用默认的段名，需要用sec选项：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 egress bpf da obj prog.o sec foobar</pre>
<p>已经挂钩到设备的tc程序的列表，可以用下面的命令查看：</p>
<pre class="crayon-plain-tag">tc filter show dev em1 ingress
filter protocol all pref 49152 bpf

     # 针对的L3协议    优先级      分类器类型  分类器句柄
filter protocol all    pref 49152 bpf        handle 0x1 
  # 从prog.o的ingress段加载了程序
  prog.o:[ingress] 
  # BPF程序运行在da模式
  direct-action 
  # 程序ID是全局范围唯一的BPF程序标识符，可以被bpftool使用
  id 1 
  # 程序指令流的哈希，哈希可以用来关联到Object文件，perf报告栈追踪的时候使用此哈希
  tag c5f7825e5dac396f


tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714</pre>
<p>tc可以挂钩多个BPF程序，这和XDP不同，它提供了多个其它的、可以链接在一起的classifier。尽管如此，单个da模式的BPF程序可以满足所有封包操作需求，它可以直接返回action断言，例如<pre class="crayon-plain-tag">TC_ACT_OK</pre>, <pre class="crayon-plain-tag">TC_ACT_SHOT</pre>。使用单个BPF程序是推荐的用法。</p>
<p>除非打算自动替换挂钩的BPF程序，建议初次挂钩时明确的指定pref和handle，这样，在后续手工替换的时候就不需要查询获取pref、handle：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar</pre>
<p>使用下面的命令原子的替换BPF程序：</p>
<pre class="crayon-plain-tag">tc filter replace dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar</pre>
<p>要移除所有以及挂钩的BPF程序，执行：</p>
<pre class="crayon-plain-tag">tc filter del dev eth0 ingress
tc filter del dev eth0 egress</pre>
<p>要从网络设备上移除整个clsact qdisc，可以：</p>
<pre class="crayon-plain-tag">tc qdisc del dev eth0 clsact</pre>
<p>类似于XDP程序，tc程序也支持offload给职能网卡。你需要首先启用hw-tc-offload：</p>
<pre class="crayon-plain-tag">ethtool -K eth0 hw-tc-offload on</pre>
<p>然后再启用clsact并挂钩BPF程序。XDP和tc的offloading不能同时开启。</p>
<div class="blog_h2"><span class="graybg">netdevsim</span></div>
<p>内核提供了一个dummy驱动netdevsim，它实现了XDP/tc BPF的offloading接口，用于测试目的。</p>
<p>下面的命令可以启用netdevsim设备：</p>
<pre class="crayon-plain-tag">modprobe netdevsim
echo "1 1" &gt; /sys/bus/netdevsim/new_device
devlink dev
# netdevsim/netdevsim1
devlink port
# netdevsim/netdevsim1/0: type eth netdev eth0 flavour physical
ip l
# 4: eth0: &lt;BROADCAST,NOARP,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
#     link/ether 2a:d5:cd:08:d1:3f brd ff:ff:ff:ff:ff:ff</pre>
<div class="blog_h1"><span class="graybg">XDP</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>在网络封包处理方面，出现过一种提升性能的技术 —— 内核旁路（Kernel Bypass ）：完全在用户空间实现网络驱动和整个网络栈，避免上下文切换、内核网络层次、中断处理。具体实现包括Intel的DPDK （Data Plane Development Kit）、Cisco的VPP等。</p>
<p>内核旁路技术的缺点是：</p>
<ol>
<li>作为硬件资源的抽象层，内核是经过良好测试和验证的。在用户空间重新实现驱动，稳定性、可复用性欠佳</li>
<li>实现网络栈也是困难的</li>
<li>作为一个沙盒，网络处理程序难以和内核其它部分集成/交互</li>
<li>无法使用内核提供的安全层</li>
</ol>
<p><span style="background-color: #c0c0c0;">eXpress Data Path，为内核提供了一个基于eBPF的、高性能的、可编程的、运行在驱动层的封包处理框架</span>，它提升性能的思路和内核旁路技术相反 —— 完全在内核空间实现封包处理逻辑，例如过滤、映射、路由等。XDP通过在网络接收路径的最早期挂钩eBPF程序来实现高速封包过滤。最早期意味着：NIC驱动刚刚从receiver rings接收到封包，任何高成本操作，例如分配skb并将封包推入网络栈，尚未进行。</p>
<p>XDP的起源来自于对DDoS攻击的防范。Cloudflare依赖（leverages heavily on）iptables进行封包过滤，在配置相当好的服务器上，可以处理1Mpps的流量。但是当出现DDoS攻击时，流量会高达3Mpps，这会导致Linux系统overflooded by IRQ请求，直到系统变得不稳定。</p>
<p>由于Cloudflare希望继续使用iptables以及其它内核网络栈功能，它不考虑使用DPDK这样的完全控制硬件的方案，而是使用了所谓部分内核旁路（partial kernel bypass），NIC的一部分队列继续附到内核，另外一部分队列则附到一个用户空间应用程序，此程序决定封包是否应该被丢弃。通过在网络栈的最底部就决定是否应该丢弃封包，需要经由内核网络子系统的封包数量大大减少了。</p>
<p><a href="https://blog.cloudflare.com/single-rx-queue-kernel-bypass-with-netmap/">Cloudflare利用了Netmap工具包</a>实现部分内核旁路。但是这个思路可以延伸为，在内核网络栈中增加一个Checkpoint，这个点应该离NIC接收到封包的时刻尽可能的近。这个Checkpoint将把封包交给用户编写的程序，决定是应该丢弃，还是继续正常处理路径。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/05/xdp-packet-processing.png"><img class="wp-image-37127 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/05/xdp-packet-processing.png" alt="xdp-packet-processing" width="1068" height="584" /></a></p>
<p>XDP对应的BPF程序类型是：BPF_PROG_TYPE_XDP。XDP程序可以读写封包，调用助手函数解析封包、计算Checksum，这些操作都不会牵涉系统调用的开销（都在内核空间执行）。</p>
<p>尽管XDP的基本用途是，尽早的决定封包是否应该丢弃。但是，由于网络函数无非是读、写、转发、丢弃等原语的组合，XDP可以用来实现任何网络功能。</p>
<p>XDP的主要优势包括：</p>
<ol>
<li>可以使用各种内核基础设施，例如路由表、套接字、网络栈</li>
<li>运行在内核中，使用和内核其它部分一致的安全模型</li>
<li>运行在内核中，不需要跨越用户/内核空间边界，能够灵活的转发封包给其它内核实体，例如命名空间、网络栈</li>
<li>支持动态替换XDP程序，不会引起网络中断</li>
<li>保证封包的线性（linearly）布局，封包位于单个DMAed内存页中，访问起来很方便</li>
<li>保证封包有256字节可用的额外headroom，可以用于（使用助手函数<pre class="crayon-plain-tag">bpf_xdp_adjust_head</pre>、<pre class="crayon-plain-tag">bpf_xdp_adjust_meta</pre>）添加自定义的封装包头</li>
</ol>
<p>从内核4.8+开始，主要发行版中XDP可用，大部分10G+网络驱动支持XDP。</p>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">DDoS缓解</span></div>
<p>XDP的高性能特征，让它非常适合实现DDoS攻击缓解，以及一般性防火墙。</p>
<div class="blog_h3"><span class="graybg">封包转发</span></div>
<p>BPF程序可以对封包进行任意的修改，甚至是通过助手函数任意的增减headroom大小实现封装/解封装。</p>
<p>处理完的封包通过XDP_REDIRECT动作即可转发封包给其它NIC，或者转发给其它CPU（利用BPF的cpumap）</p>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>使用XDP_TX动作，hairpinned LB可以将修改后的封包从接收它的网卡发送回去。</p>
<div class="blog_h3"><span class="graybg">流量采样和监控</span></div>
<p>XDP支持将部分或截断的封包内容存放到无锁的per-CPU的内存映射ring buffer中。此ring buffer由Linux perf基础设施提供，可以被用户空间访问。</p>
<div class="blog_h2"><span class="graybg">编程接口</span></div>
<div class="blog_h3"><span class="graybg">xdp_buff</span></div>
<p>在XDP中，代表当前封包的结构是：</p>
<pre class="crayon-plain-tag">struct xdp_buff {
    // 内存页中，封包数据的开始点指针
    void *data;
    // 内存页中，封包数据的结束点指针
    void *data_end;
    // 最初和和data指向同一位置。后续可以被bpf_xdp_adjust_meta()调整，向data_hard_start方向移动
    // 可以用于为元数据提供空间。这种元数据对于正常的内核网络栈是不可见的，但是能够被tc BPF程序读取，
    // 因为元数据会从XDP传送到skb中
    // data_meta可以仅仅适用于在尾调用之间传递信息，类似于可被tc访问的skb-&gt;cb[]
    void *data_meta;
    // XDP支持headroom，这个字段给出页中，此封包可以使用的，最小的地址
    // 如果封包被封装，则需要调用bpf_xdp_adjust_head()，将data向data_hard_start方向移动
    // 解封装时，也可以使用bpf_xdp_adjust_head()移动指针
    void *data_hard_start;
    // 提供一些额外的per receive queue元数据，这些元数据在ring setup time生成
    struct xdp_rxq_info *rxq;
};

// 接收队列信息
struct xdp_rxq_info {
    struct net_device *dev;
    u32 queue_index;
    u32 reg_state;
} ____cacheline_aligned; // 缓存线（默认一般是64KB），CPU以缓存线为单位读取内存到CPU高速缓存</pre>
<p>它通过BPF context传递给XDP程序。</p>
<div class="blog_h3"><span class="graybg">xdp_action</span></div>
<pre class="crayon-plain-tag">enum xdp_action {
    // 提示BPF出现错误，和DROP的区别仅仅是会发送一个trace_xdp_exception追踪点
    XDP_ABORTED = 0,
    // 应当在驱动层丢弃封包，不必再浪费额外资源。对于DDos缓和、一般性防火墙很有用
    XDP_DROP,
    // 允许封包通过，进入网络栈进行常规处理
    // 处理此封包的CPU后续将分配skb，将封包信息填充进去，然后传递给GRO引擎
    XDP_PASS,
    // 将封包从接收到的网络接口发送回去，可用于实现hairpinned LB
    XDP_TX,
    // 重定向封包给另外一个NIC
    XDP_REDIRECT,
};</pre>
<p>这个枚举是XDP程序需要返回的断言，告知驱动应该如何处理封包。</p>
<div class="blog_h1"><span class="graybg"><a id="tc"></a>tc</span></div>
<p>关于tc的基础知识，参考<a href="/tc">基于tc的网络QoS管理</a>。</p>
<div class="blog_h2"><span class="graybg">tc程序简介</span></div>
<p>BPF可以和内核的tc层一起工作。tc程序和XDP程序有以下不同：</p>
<ol>
<li>tc程序的BPF输入上下文是skb_buff，而非xdp_buff。在XDP之后，内核会解析封包，存入skb_buff。解析的开销导致tc程序的性能远低于XDP，但是，tc程序可以访问skb的mark, pkt_type, protocol, priority, queue_mapping, napi_id, cb[]数组, hash, tc_classid, tc_index等字段，以及VLAN元数据、XDP传送来的自定义元数据。BPF上下文<pre class="crayon-plain-tag">struct __sk_buff</pre>定义在<pre class="crayon-plain-tag">linux/bpf.h</pre></li>
<li>tc程序可以挂钩到ingress/egress网络路径上，XDP则仅仅能挂钩到ingress路径</li>
<li>tc程序对驱动层没有依赖，可以挂钩到任何类型的网络设备。除非启用tc BPF程序的offloading</li>
</ol>
<p>尽管tc程序的挂钩点没有XDP那么早，但是仍然是<span style="background-color: #c0c0c0;">在内核网络路径的早期。它在GRO运行之后，任何协议处理之前执行</span>。iptables PREROUTING、nftables ingress hook等工具也在相同的位置挂钩。</p>
<div class="blog_h2"><span class="graybg">tc程序工作方式</span></div>
<p>工作在tc层的BPF程序，是从一个名为<pre class="crayon-plain-tag">cls_bpf</pre>的过滤器运行的。tc程序<span style="background-color: #c0c0c0;">不但可以读取skb的元数据、封包内容，还能够对封包进行任意修改，甚至使用action verdict终止tc处理过程</span>。</p>
<p>过滤器<span style="background-color: #c0c0c0;">cls_bpf可以挂钩1-N个BPF程序</span>，当有多个BPF程序情况下，前面的程序返回verdict <pre class="crayon-plain-tag">TC_ACT_UNSPEC</pre>会导致继续执行后面的BPF程序。使用多个BPF程序的缺点是，需要反复解析封包，导致性能降低。</p>
<p>cls_bpf有一个direct-action（da）模式，这样BPF程序能够直接返回action verdict，决定封包命运，结束tc处理流水线。</p>
<p>tc BPF程序也支持在运行时动态更新，而不会中断任何网络流量。</p>
<p>cls_bpf可以挂钩到的ingress/egress钩子，均被一个伪（不在qdisc树形结构中）排队规则<pre class="crayon-plain-tag">sch_clsact</pre>管理。对于ingress qdisc来说，sche_clsact可以作为一个drop-in的替代品。对于在<pre class="crayon-plain-tag">__dev_queue_xmit()</pre>中执行的egress钩子，需要强调，sche_clsact不在内核的root qdisc锁下运行。因此，<span style="background-color: #c0c0c0;">不管是ingress/egress，使用sche_clsact时tc BPF程序都是以无锁方式执行的，这和典型的qdisc完全不同</span>。此外需要注意，<span style="background-color: #c0c0c0;">sch_clsact执行期间不会发生抢占</span>。</p>
<p>典型情况下，egress方向会有附到网络设备的qdisc，例如sch_htb、sch_fq，它们其中有些是classful qdisc。classful qdisc会通过<pre class="crayon-plain-tag">tcf_classify()</pre>调用分类器，cls_bpf也可以被挂到这种qdisc上。这时，BPF程序在root qdisc下运行，可能面临锁争用问题。</p>
<p>为了达到最大性能（减少锁争用），可以考虑这样的用法：使用sch_clsact + cls_bpf，在root qdisc锁之外，完成任务繁重的封包分类工作，并且设置skb-&gt;mark或skb-&gt;priority。然后，由运行在root qdisc锁下的sch_htb快速的根据skb字段完成分类、塑形操作。</p>
<p>sch_clsact + cls_bpf组合使用时，如果cls_bpf是da模式、只包含单个BPF程序、且位于ingress网络路径，则支持offload给智能网卡。</p>
<div class="blog_h2"><span class="graybg">编程接口</span></div>
<div class="blog_h3"><span class="graybg">__sk_buff</span></div>
<p>在tc BPF程序中，代表当前封包的结构是<pre class="crayon-plain-tag">__sk_buff</pre>，这种结构叫UAPI（user space API of the kernel），可以访问内核<pre class="crayon-plain-tag">sk_buff</pre>结构的某些字段。</p>
<pre class="crayon-plain-tag">struct __sk_buff {
	__u32 len;
	__u32 pkt_type;
	__u32 mark;
	__u32 queue_mapping;
	__u32 protocol;
	__u32 vlan_present;
	__u32 vlan_tci;
	__u32 vlan_proto;
	__u32 priority;
	__u32 ingress_ifindex;
	__u32 ifindex;
	__u32 tc_index;
	__u32 cb[5];
	__u32 hash;
	__u32 tc_classid;
	__u32 data;
	__u32 data_end;
	__u32 napi_id;

	/* Accessed by BPF_PROG_TYPE_sk_skb types from here to ... */
	__u32 family;
	__u32 remote_ip4;	/* Stored in network byte order */
	__u32 local_ip4;	/* Stored in network byte order */
	__u32 remote_ip6[4];	/* Stored in network byte order */
	__u32 local_ip6[4];	/* Stored in network byte order */
	__u32 remote_port;	/* Stored in network byte order */
	__u32 local_port;	/* stored in host byte order */
	/* ... here. */

	__u32 data_meta;
	__bpf_md_ptr(struct bpf_flow_keys *, flow_keys);
	__u64 tstamp;
	__u32 wire_len;
	__u32 gso_segs;
	__bpf_md_ptr(struct bpf_sock *, sk);
};</pre>
<div class="blog_h3"><span class="graybg">verdicts</span></div>
<p>tc ingress/egress钩子能够返回的verdict定义在：</p>
<pre class="crayon-plain-tag">// 未指定，如果有多个BPF程序，会继续运行下一个。如果没有更多BPF程序
// 则提示内核在没有任何side-effect的情况下继续处理skb
#define TC_ACT_UNSPEC         (-1)
// 从tc BPF程序角度，TC_ACT_OK、TC_ACT_RECLASSIFY等价
#define TC_ACT_OK               0
// 提示内核丢弃封包，在ingress方向，网络栈上层无法看到封包；在egress方向，封包不会被发出
#define TC_ACT_SHOT             2
// 从tc BPF程序角度，TC_ACT_STOLEN、TC_ACT_QUEUED、TC_ACT_TRAP等价
// 类似于TC_ACT_SHOT，区别：
//   TC_ACT_SHOT导致内核通过kfree_skb()释放封包并返回NET_XMIT_DROP作为即时的反馈
//   TC_ACT_STOLEN导致内核通过consume_skb()释放封包，并且返回NET_XMIT_SUCCESS，
//      效果是上层以为封包是成功发送的
#define TC_ACT_STOLEN           4
// 利用助手函数bpf_redirect()，重定向封包到相同/不同设备的ingress/egress路径
#define TC_ACT_REDIRECT         7 </pre>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">容器网络策略</span></div>
<p>对于容器来说，容器网络命名空间和初始网络命名空间通过一对veth连接。我们可以在宿主机端实现网络策略：</p>
<ol>
<li>主机侧的egress，对应容器的ingress</li>
<li>主机侧的ingress，对应容器的egress</li>
</ol>
<p>将tc BPF程序挂钩到宿主机veth的egress/ingress钩子即可。</p>
<p>对于veth这样的虚拟设备，XDP是不适合的。因为内核在虚拟设备这里，单纯在一个skb上操作，<span style="background-color: #c0c0c0;">XDP由于一些限制，无法和克隆的skb一起工作</span>。克隆skb在内核TCP/IP栈被大量使用，用来存放重传的数据段，这里XDP钩子会被直接跳过。此外，XDP需要线性化（放到单个页）整个skb，这也导致严重的性能影响。</p>
<div class="blog_h3"><span class="graybg">转发和负载均衡</span></div>
<p>容器工作负载的东西向流量是主要的目标场景。</p>
<p>不像XDP仅作用在ingress上，tc BPF可以在某些场景下，应用到容器egress方向。在对容器透明的前提下，可以利用BPF在egress进行NAT和LB处理，利用bpf_redirection助手函数，<span style="background-color: #c0c0c0;">BPF可以将封包转到任何接口的ingress/egress路径，不需要网桥之类的设备辅助</span>。</p>
<div class="blog_h3"><span class="graybg">流采样和监控</span></div>
<p>类似XDP，流采样和监控可以通过高性能、无锁的per-CPU内存映射的perf ring buffer实现，依此tc BPF程序可以调用助手函数<pre class="crayon-plain-tag">bpf_skb_event_output()</pre>推送定制数据、完整/截断的封包内容到用户空间。</p>
<p>由于tc BPF程序可以同时挂到ingress/egress，因此可以为任何节点实现双向的监控。</p>
<p>BPF程序可以预先做一些聚合，而不是把所有东西推送到用户空间。</p>
<div class="blog_h3"><span class="graybg">预处理封包调度</span></div>
<p>如上文所提到，sch_clsact的egress钩子，即<pre class="crayon-plain-tag">sch_handle_egress()</pre>，在获取内核qdisc root锁之前运行。这种无锁的特征让tc BPF程序适合执行较重（耗时）的分类任务，并将分类结果设置到skb的某些字段，在后续交由常规的、有锁的qdisc进行塑形和重排。</p>
<div class="blog_h1"><span class="graybg">开发环境</span></div>
<div class="blog_h2"><span class="graybg">内核和工具</span></div>
<p>本节介绍如何创建完整的BPF开发环境。尽管手工构建iproute2和Linux内核是非必须的（主流发行版已经内置），但是测试最新特性、或者需要贡献BPF补丁到内核、iproute2时则需要。</p>
<p>安装构建需要的软件：</p>
<pre class="crayon-plain-tag">sudo apt-get install -y make gcc libssl-dev bc libelf-dev libcap-dev \
  clang gcc-multilib llvm libncurses5-dev git pkg-config libmnl-dev bison flex \
  graphviz</pre>
<div class="blog_h3"><span class="graybg">构建内核</span></div>
<p>BPF相关的新特性在内核的net-next分支上开发，最后的BPF fixes则在net分支上。</p>
<p>注意打开以下内核配置项：</p>
<pre class="crayon-plain-tag">CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_SCH_INGRESS=m
CONFIG_NET_CLS_BPF=m
CONFIG_NET_CLS_ACT=y
# 如果目标体系结构支持JIT，自动y
CONFIG_BPF_JIT=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_TEST_BPF=m</pre>
<p>从新编译的内核启动后，运行BPF自我测试套件，应该全部通过：</p>
<pre class="crayon-plain-tag">cd tools/testing/selftests/bpf/
make
sudo ./test_verifier</pre>
<div class="blog_h3"><span class="graybg">构建iproute2</span></div>
<p>iproute2具有独立的Git仓库：</p>
<pre class="crayon-plain-tag">git clone https://git.kernel.org/pub/scm/network/iproute2/iproute2.git</pre>
<p>master分支对应内核的net分支，net-next分支对应内核的net-next分支。</p>
<p>执行下面的命令编译：</p>
<pre class="crayon-plain-tag">cd iproute2/
./configure --prefix=/usr
# 确保输出：
# ELF support: yes
# 这样iproute2才能处理LLVM BPF后端产生的ELF文件
sudo make install</pre>
<div class="blog_h3"><span class="graybg">构建bpftool</span></div>
<p>bpftools是BPF程序、Map的调试、introspection工具。它位于内核源码树的tools/bpf/bpftool/</p>
<pre class="crayon-plain-tag">cd tools/bpf/bpftool/
make
sudo make install</pre>
<div class="blog_h3"><span class="graybg">构建libbpf</span></div>
<div class="blog_h2"><span class="graybg">工具链</span></div>
<p>LLVM 3.7+是当前唯一提供BPF后端的编译套件，GCC目前不支持。主流发行版默认启用了LLVM的BPF后端支持，因此直接安装clang和llvm包足够将C编译为BPF Object文件。</p>
<p>通过下面的命令确认你的LLVM支持BPF目标：</p>
<pre class="crayon-plain-tag">llc --version
LLVM (http://llvm.org/):
  LLVM version 10.0.0
  
  Optimized build.
  Default target: x86_64-pc-linux-gnu
  Host CPU: skylake

  Registered Targets:
    ...
    # 默认情况下，bpf目标使用编译它的CPU的端序
    bpf        - BPF (host endian)
    # 这两个目标用于交叉编译
    bpfeb      - BPF (big endian)
    bpfel      - BPF (little endian)</pre>
<div class="blog_h3"><span class="graybg">编译命令</span></div>
<p>对于下面这个最简单的XDP Drop程序：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return XDP_DROP;
}

char __license[] __section("license") = "GPL";</pre>
<p>可以这样编译：</p>
<pre class="crayon-plain-tag">clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o
# 加载，需要4.12或更高版本
# ip link set dev em1 xdp obj xdp-example.o</pre>
<div class="blog_h1"><span class="graybg">BPF CO-RE</span></div>
<p>所谓CO-RE是指一次编译，到处运行（Compile Once – Run Everywhere）。</p>
<p>编写可移植性（能够跨越不同内核版本运行）的BPF程序是一项挑战，BPF CO-RE能够辅助这一过程。</p>
<div class="blog_h2"><span class="graybg">问题</span></div>
<p>BPF程序本质上是一段直接插入内核的代码，它可以访问（被允许访问的）所有内核内部状态。BPF程序无法控制所运行内核的内存布局：</p>
<ol>
<li>不同内核版本，结构体字段的顺序可能不同，某些字段可能在新版本被内嵌到子结构中，字段的类型可能出现不兼容的变更</li>
<li>根据内核构建时的配置不同，某些字段可能被注释掉</li>
</ol>
<p>并不是所有BPF程序都需要访问内核数据结构，例如opensnoop，它依赖 kprobes/tracepoints 来追踪程序打开了那些文件，仅仅需要捕获一些系统调用参数，而系统调用提供稳定的ABI，不会随着内核版本而变化。</p>
<p>不幸的是，opensnoop这样的程序是少数派，不访问内核数据结构也限制了程序的能力。</p>
<p><span style="background-color: #c0c0c0;">内核中的BPF机制提供了一个有限集合的稳定接口</span>，BPF程序可以依赖这套结构，不用担心跨内核的兼容性问题。尽管底层结构会发生变化，但是BPF提供的抽象保持稳定。这种接口的一个例子是<pre class="crayon-plain-tag">__sk_buff</pre>，它是内核数据结构<pre class="crayon-plain-tag">sk_buff</pre>的稳定接口，可以访问sk_buff的一部分重要字段。对__sk_buff字段的访问，会被透明的转写（可以需要多次pointer chasing）为对sk_buff对应字段的访问。类似__sk_buff的、针对特定类型BPF程序的接口抽象还有不少，如果你编写这些类型的BPF程序，可能（如果接口可以满足需求）不用担心可移植性问题。</p>
<p>一旦有了读取内核内部原始状态的需求，例如读取<pre class="crayon-plain-tag">struct task_struct</pre>的某个字段，可移植性问题就出现了。如果某些内核为此结构添加了额外字段，如何保证读取的不是垃圾数据？如果出现了字段重命名，例如4.7将task_struct的fs字段重命名为fsbase，又该怎么办？这些可移植性问题都让你不能简单的使用开发机上的内核头文件来构建BPF程序。</p>
<div class="blog_h2"><span class="graybg">BCC方案</span></div>
<p>解决可移植性问题的一个方案是BCC，使用BCC时，BPF内核程序的C源码作为字符串嵌入在你的用户空间程序（控制程序）中，当控制程序被部署到目标环境后，BCC会调用内嵌的Clang/LLVM，拉取本地内核头文件，执行即席的BPF程序构建。如果字段可能在某些环境下compiled-out，你只需要在源码中使用相应的#ifdef/#else。BCC方案有一些关键的缺点：</p>
<ol>
<li>Clang/LLVM组合非常大，你需要将其部署到所有运行BPF程序的机器</li>
<li>Clang/LLVM在编译时很消耗资源，如果在程序启动时编译BPF代码，可能会生产环境的工作负载产生不利影响</li>
<li>目标机器上可能不包含匹配的内核头文件</li>
<li>开发和测试的迭代变得痛苦，可能在运行时得到很多编译错误</li>
<li>太多的magic，难以定位错误，你需要记住命名约定、自动为tracepoint生成的结构、依赖代码重写来读取内核数据/获取kprobe参数</li>
<li>读写BPF Map时需要编写半面向对象的C代码，和内核中发生的不完全匹配</li>
<li>仍然需要在用户空间编写大量的样板代码</li>
</ol>
<div class="blog_h2"><span class="graybg">CO-RE原理</span></div>
<p>BPF CO-RE将软件栈所有层次 —— 内核、用户空间BPF loader库（libbpf）、编译器（Clang）——的必要功能/数据片段整合到一起，来降低编写可移植性BPF程序的难度。CO-RE需要下列组件的谨慎集成和协作：</p>
<ol>
<li>BTF类型信息：允许捕获关于内核、BPF程序的类型/代码的关键信息</li>
<li>Clang为BPF程序C代码提供了express the intent和记录relocation信息的手段</li>
<li>BPF loader（libbpf）根据内核的BTF和BPF程序，调整编译后的BPF代码，使其适合在目标内核上运行</li>
<li>对于BPF CO-RE不可知的内核，提供了一些高级的BPF特性，满足高级场景</li>
</ol>
<div class="blog_h3"><span class="graybg">BTF</span></div>
<p>即BPF Type Format，类似于DWARF调试信息，但是没有那么generic和verbose。它是一种空间高效的、紧凑的、有足够表达能力的格式，足以描述C程序的所有类型信息。由于它的简单性和BPF去重算法，对比DWARF，BTF能够缩小100x的尺寸。现在，在运行时总是保留BTF信息是常见做法，它对应内核选项<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>，在Ubuntu 20.10开始默认开启。</p>
<p>BTF能够用来增强BPF verifier的能力，能够允许BPF代码直接访问内核内存，不需要<pre class="crayon-plain-tag">bpf_probe_read()</pre>。</p>
<p>对于CO-RE来说，更重要的是，内核通过<pre class="crayon-plain-tag">/sys/kernel/btf/vmlinux</pre>暴露了权威的、自描述的BTF信息。执行下面的命令，你可以得到一个可编译的C头文件：</p>
<pre class="crayon-plain-tag">bpftool btf dump file /sys/kernel/btf/vmlinux format c</pre>
<p>此文件通常命名为<pre class="crayon-plain-tag">vmlinux.h</pre>，其中包含了所有的内核类型信息，甚至包含那些不会通过kernel-devel包暴露的信息。</p>
<div class="blog_h3"><span class="graybg">编译器支持</span></div>
<p>为了启用CO-RE，并且让BPF loader（libbpf）来为正在运行的（目标）内核调整BPF程序，Clang被扩展，增加了一些built-ins。</p>
<p>这些built-ins会发出（emit）BTF relocations，BTF relocations是BPF程序需要读取什么信息的高层描述。假设程序需要访问task_struct-&gt;pid，Clang会将其记录：需要访问pid_t类型的、名为pid、位于task_struct结构中的字段。这样，即使字段顺序调整，甚至pid字段被放入一个内嵌的匿名结构体/联合体中，BPF程序仍然能够正确访问到pid字段。</p>
<p>能够捕获（进而重定位）的信息不单单是字段偏移量，还包括字段是否存在、字段的size。甚至对于位域（bitfield）字段，也能够捕获足够多的信息，让对它的访问能够被重定位。</p>
<div class="blog_h3"><span class="graybg">BPF loader</span></div>
<p>BPF loader在加载程序时，会利用前述的（构建机的）内核BTF信息、Clang重定位信息，并读取当前内核的BTF信息，对BPF程序（ELF object文件）进行裁减（custom tailored） —— 解析和匹配所有类型、字段，更新字段偏移量，以及其它可重定位数据 —— 确保程序在当前内核上能够正确运行。</p>
<div class="blog_h3"><span class="graybg">内核</span></div>
<p>要支持CO-RE，内核不需要更多的改变（除了开启CONFIG_DEBUG_INFO_BTF）。被BPF loader（libbpf）处理过的BPF程序，对于内核来说，和在本机编译的BPF程序是完全等价的。</p>
<div class="blog_h2"><span class="graybg">CO-RE现状</span></div>
<p>截至2021年，BPF CO-RE是很成熟的技术，在大量生产环境下运行。</p>
<p>由于引入了BPF CO-RE，超过25个BCC工具被转换为libbpf +BPF CO-RE方式编写。由于越来越多的Linux发行版（Ubuntu 20.10、RHEL 8.2+）默认开启BTF，BPF CO-RE相关工具的适用面变得越来越广，可以替代笨重的、基于python的BCC工具。</p>
<p>BPF CO-RE在不同BPF应用领域被广泛接受，包括追踪、性能监控、安全/审计，甚至网络BPF程序。</p>
<p>要使用BPF CO-RE，可以考虑从脚手架项目libbpf-bootstrap开始。</p>
<div class="blog_h2"><span class="graybg">使用CO-RE</span></div>
<div class="blog_h3"><span class="graybg">解除内核依赖</span></div>
<p>为了避免依赖于系统头文件，可以生成包含所有内核类型的vmlinux.h：</p>
<pre class="crayon-plain-tag">bpftool btf dump file /sys/kernel/btf/vmlinux format c &gt; vmlinux.h</pre>
<p>这样你的代码中就不需要包含各种内核头文件、也不必须安装kernel-devel包了：</p>
<pre class="crayon-plain-tag">#include &lt;linux/sched.h&gt;
#include &lt;linux/fs.h&gt;</pre>
<p>由于BTF（以及DWARF）不会记录宏信息，因此某些常用的宏可能没有包含在vmlinux.h中，好在其中大部分可以通过bpf_helpers.h访问。</p>
<div class="blog_h3"><span class="graybg">读取内核结构</span></div>
<p>使用BCC时，你可以直接访问：</p>
<pre class="crayon-plain-tag">pid_t pid = task-&gt;pid;</pre>
<p>BCC会自动将其重写为对<pre class="crayon-plain-tag">bpf_probe_read()</pre>的调用。</p>
<p>使用CO-RE的时候，由于没有BCC这种代码重写机制，为了打到同样效果，你可能需要：</p>
<ol>
<li>libbpf + BPF_PROG_TYPE_TRACING：如果编写的是这类程序，你可以直接写：<br />
<pre class="crayon-plain-tag">pid_t pid = task-&gt;pid;</pre></p>
<p>而不需要bpf_probe_read()调用。要实现可移植性，则需要将上述代码包围到 <pre class="crayon-plain-tag">__builtin_preserve_access_index</pre>中：</p>
<pre class="crayon-plain-tag">pid_t pid = __builtin_preserve_access_index(({ task-&gt;pid; }));</pre>
</li>
<li>对于其它类型BPF程序，你不能直接访问结构字段，而需要：<br />
<pre class="crayon-plain-tag">pid_t pid; bpf_probe_read(&amp;pid, sizeof(pid), &amp;task-&gt;pid);</pre>
<p>要实现可移植性，则需要：</p>
<pre class="crayon-plain-tag">pid_t pid; bpf_core_read(&amp;pid, sizeof(pid), &amp;task-&gt;pid);
// 或者
bpf_probe_read(&amp;pid, sizeof(pid), __builtin_preserve_access_index(&amp;task-&gt;pid));</pre>
</li>
</ol>
<p>进行pointer chasing时，使用bpf_probe_read()/bpf_core_read()会变得痛苦：
<pre class="crayon-plain-tag">u64 inode = task-&gt;mm-&gt;exe_file-&gt;f_inode-&gt;i_ino;</pre>
<p>你需要逐步的分配指针临时变量，逐步读取字段，非常麻烦。 幸运的是，CO-RE提供了助手宏：</p>
<pre class="crayon-plain-tag">u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);

// 或者
u64 inode;
BPF_CORE_READ_INTO(&amp;inode, task, mm, exe_file, f_inode, i_ino);</pre>
<p>类似的，和<pre class="crayon-plain-tag">bpf_probe_read_str()</pre>对应的CO-RE函数是<pre class="crayon-plain-tag">bpf_core_read_str()</pre>，以及助手宏<pre class="crayon-plain-tag">BPF_CORE_READ_STR_INTO()</pre>。</p>
<p>要检查字段是否在目标内核存在，可以使用<pre class="crayon-plain-tag">bpf_core_field_exists()</pre>宏：</p>
<pre class="crayon-plain-tag">pid_t pid = bpf_core_field_exists(task-&gt;pid) ? BPF_CORE_READ(task, pid) : -1;</pre>
<p>某些内部的、非UAPI的内核枚举值，可能跨内核版本时发生变动，甚至依赖于特定的内核配置（例如cgroup_subsys_id），这导致硬编码任何值都是不可靠的。使用Enum relocation宏<pre class="crayon-plain-tag">bpf_core_enum_value_exists()</pre>和<pre class="crayon-plain-tag">bpf_core_enum_value()</pre>，可以检查特定枚举值是否存在，并捕获它的值。Enum relocation重定向的一个重要用途是检测BPF助手函数是否存在，如果不存在则使用旧版本的替代物。</p>
<p>要捕获（如果不确定某个字段是否在别的内核版本中发生了类型变更）字段的size，可以使用 <pre class="crayon-plain-tag">bpf_core_field_size()</pre>：</p>
<pre class="crayon-plain-tag">u32 comm_sz = bpf_core_field_size(task-&gt;comm);</pre>
<p>位域字段的读取，可以使用：</p>
<pre class="crayon-plain-tag">struct tcp_sock *s = ...;

// 读取s-&gt;is_cwnd_limited对应的位域字段
bool is_cwnd_limited = BPF_CORE_READ_BITFIELD(s, is_cwnd_limited);

// 或者
u64 is_cwnd_limited;
BPF_CORE_READ_BITFIELD_PROBED(s, is_cwnd_limited, &amp;is_cwnd_limited);</pre>
<div class="blog_h3"><span class="graybg">内核版本和配置差异</span></div>
<p>某些情况下，内核之间不是简单的结构性差异：</p>
<ol>
<li>同一含义的字段可能被重命名</li>
<li>字段的含义可能改变，例如从4.6开始，task_struct.utime/stime从原先的以jiffies为单位改为纳秒为单位</li>
</ol>
<p>内核配置的差异，可能会出现某些内核下无法读取字段的情况。</p>
<p>CO-RE提供处理这些问题的辅助机制是libbpf提供的extern Kconfig variables和struct flavors。</p>
<p>BPF程序可以定义具有知名名称的变量，例如LINUX_KERNEL_VERSION；或者一个匹配内核Kconfig键的变量，例如CONFIG_HZ。libbpf能够自动设置这些外部变量为匹配当前内核的值，BPF verifier也会跟踪这些变量，并进行高级的流分析和dead code消除。</p>
<pre class="crayon-plain-tag">//                              声明外部Kconfig变量
extern u32 LINUX_KERNEL_VERSION __kconfig;
extern u32 CONFIG_HZ __kconfig;

u64 utime_ns;

if (LINUX_KERNEL_VERSION &gt;= KERNEL_VERSION(4, 11, 0))
    utime_ns = BPF_CORE_READ(task, utime);
else
    /* convert jiffies to nanoseconds */
    utime_ns = BPF_CORE_READ(task, utime) * (1000000000UL / CONFIG_HZ);</pre>
<p>struct flavors则用于解决内核存在不兼容类型的情况。实际上就是为不同版本的内核定义不同的结构：</p>
<pre class="crayon-plain-tag">// 新版本内核使用此结构
struct thread_struct {
    ...
    u64 fsbase;
    ...
};

// 4.6或者更低版本使用此结构
// 三下划线及其后面的部分，被认为是结构的一个flavor，flavor部分会被libbpf忽略，
// 这意味着在进行relocation时thread_struct___v46仍然对应着运行中的内核的thread_struct结构

struct thread_struct___v46 { /* ___v46 is a "flavor" part */
    ...
    u64 fs;
    ...
};

extern int LINUX_KERNEL_VERSION __kconfig;
...

struct thread_struct *thr = ...;
u64 fsbase;
if (LINUX_KERNEL_VERSION &gt; KERNEL_VERSION(4, 6, 0))
    // 强制转型为flavor，从而抽取需要的字段
    fsbase = BPF_CORE_READ((struct thread_struct___v46 *)thr, fs);
else
    fsbase = BPF_CORE_READ(thr, fsbase);</pre>
<p>如果没有struct flavors，你就不能编写可移植的BPF程序。你只能通过#ifdef条件编译为多个BPF object文件，然后在控制程序中，判断当前内核版本，然后选择加载匹配的BPF object。</p>
<div class="blog_h3"><span class="graybg">根据用户配置修改行为</span></div>
<p>即使知道目标内核版本、配置，BPF程序可能仍然不知道如何从内核中读取需要的数据。这种情况下，可以通过用户空间的控制程序进行精确的判断，然后通过BPF Map传递一个配置信息给BPF程序，BPF程序根据此配置信息改变自己的行为。这种做法的缺点是：</p>
<ol>
<li>BPF程序每次都需要读取Map的成本，对于高性能BPF程序不可忽略</li>
<li>配置信息即使在BPF程序启动后是不可变的（没有代码去改它），但是对于BPF verifier来说，仍然是一个黑盒。BPF verifier不能根据配置信息来裁减掉dead code，或者进行其它高级的代码分析。这样，为新版本内核（假设这个版本引入了新的助手函数）编写的分支，在旧版本内核上无法被裁减，从而可能破坏程序（无法通过校验，以为助手函数不存在）</li>
</ol>
<p>解决上述缺点的方法是使用只读全局变量。变量的值由控制程序加载BPF object文件后设置（修改ELF文件）。这不会带来Map查询的成本，BPF verifer会将其此变量作为常量看待，从而裁减dead code。</p>
<pre class="crayon-plain-tag">/* global read-only variables, set up by control app */
const bool use_fancy_helper;
const u32 fallback_value;

...

u32 value;
if (use_fancy_helper)
    value = bpf_fancy_helper(ctx);
else
    value = bpf_default_helper(ctx) * fallback_value;</pre>
<p>通过BPF skeleton，可以很容易的从用户空间修改ELF文件。</p>
<div class="blog_h3"><span class="graybg">编译BPF程序</span></div>
<p>利用上文提到的BPF CO-RE提供的多种能力，编写好代码后，你需要用Clang 10+版本，编译得到BPF object文件。</p>
<div class="blog_h3"><span class="graybg">生成BPF skeleton</span></div>
<p>从编译好的BPF object文件，可以利用<pre class="crayon-plain-tag">bpftool gen skeleton</pre>自动生成BPF skeleton。</p>
<div class="blog_h3"><span class="graybg">编写控制程序</span></div>
<p>将BPF skeleton（头文件）包含到你的用户空间控制程序中，获得打开、加载、挂钩BPF程序，以及修改BPF对象等能力。</p>
<div class="blog_h1"><span class="graybg">eBPF编程</span></div>
<div class="blog_h2"><span class="graybg">C编程要点</span></div>
<p>使用C语言编写eBPF程序，需要注意：</p>
<ol>
<li>可以访问助手函数、上下文对象</li>
<li>程序的入口点通过段来指定，而非main函数</li>
<li>在对象文件中包含多个入口点是允许的</li>
<li>所有库函数调用被内联，因而运行时不存在函数调用的开销</li>
<li>没有全局变量（5.5-）</li>
<li>没有循环</li>
<li>没有常量</li>
<li>LLVM的内置函数一般是可用的、并且被内联</li>
<li>栈空间大小限制为512字节</li>
</ol>
<div class="blog_h3"><span class="graybg">内联一切</span></div>
<p>除非使用支持BPF-BPF调用的4.16+内核（和LLVM6.0+），所有函数都需要被内联，没有函数调用（老版本LLVM上）或共享库调用。</p>
<p>BPF程序不能使用共享库，公共代码可以放在头文件中，被主程序include。尽管不能使用共享库，但是通过include头文件来使用静态内联函数、宏定义是很常见的。</p>
<p>为了确保这一点，需要为所有作为库使用的函数标注__inline：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   // 使用always_inline，因为仅仅使用inline编译器仍然可能在
   // 代码过大的情况下不内联，从而导致在ELF文件中生成一个relocation entry
   // iproute2这样的ELF loader不能解析BPF程序
   // 仅仅BPF Map是ELF loader能够处理的relication entry
   inline __attribute__((always_inline))
#endif

static __inline int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";</pre>
<p>从4.16开始支持在BPF程序中使用BPF-BPF函数调用，libbpf v0.2+也对此特性提供的完整的支持，确保代码的relocations/adjustment正确进行。你可以去掉<pre class="crayon-plain-tag">__always_inline</pre>，甚至用<pre class="crayon-plain-tag">__noinline</pre>强制不得进行内联。</p>
<p>非内联的global函数从5.5+开始被支持，但是和static函数比起来具有不同的语义以及校验约束。</p>
<div class="blog_h3"><span class="graybg">每个程序一个段</span></div>
<p>BPF的C程序依赖段（section）注解。典型的C程序被结构化为三个或更多的段，BPF ELF loader<span style="background-color: #c0c0c0;">通过段的名字来抽取、准备相关的信息</span>，以便载入程序和Map。</p>
<p>例如，iproute2使用maps和license作为默认的段名称，来获取创建Map所需元数据，以及BPF程序的License信息。加载时，License信息也被推入内核，这样某些仅仅在GPL协议下暴露的函数（例如bpf_ktime_get_ns和bpf_probe_read）允许被调用，确保BPF程序的License兼容性。</p>
<p>其它的段名，都专用于BPF程序代码。下面的代码使用了ingress/egress两个段，可以被tc加载并挂钩到网络设备的ingress/egress钩子：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;
#include &lt;linux/pkt_cls.h&gt;
#include &lt;stdint.h&gt;
#include &lt;iproute2/bpf_elf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

// 共享的Map是全局变量，因此访问它的时候需要同步
#ifndef lock_xadd
# define lock_xadd(ptr, val)              \
   ((void)__sync_fetch_and_add(ptr, val))
#endif

// 这个宏用于将BPF助手函数映射到C代码
// 函数map_lookup_elem被映射到定义uapi/linux/bpf.h在中的枚举值 BPF_FUNC_map_lookup_elem
// 在载入内核后，Verifier会检查传入的参数是否为期望的类型，并且将对助手函数的调用指向真实函数的调用
#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...)              \
   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
#endif

static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);
// static void (*map_lookup_elem)(void *map, const void *key) = (void *)BPF_FUNC_map_lookup_elem;




// 这个是共享的Map，类型struct bpf_elf_map是iproute2定义的
// iproute2提供了公共的BPF ELF loader，因此struct bpf_elf_map对于XDP和tc程序来说是一样的
//                         必须放在maps段，这样loader才能发现
//                         可以定义多个Map，都必须放在maps段
struct bpf_elf_map acc_map __section("maps") = {
    .type           = BPF_MAP_TYPE_ARRAY,
    .size_key       = sizeof(uint32_t),
    .size_value     = sizeof(uint32_t),
    // 该Map被Pin到PIN_GLOBAL_NS，这意味着Map将被tc钉为BPF伪文件系统中的位于
    // /sys/fs/bpf/tc/globals/目录下的节点。对于此acc_map，节点路径为
    // /sys/fs/bpf/tc/globals/acc_map
    // global是跨越多个Object文件的全局命名空间。如果不同BPF程序中均有名为acc_map
    // 的Map映射到PIN_GLOBAL_NS，这这些程序会共享统一Map。仅仅第一个载入的BPF程序会触发
    // Map的创建，后续载入的程序直接使用

    // 如果取值PIN_NONE则不会映射为BPF文件系统中的节点，当tc退出后，无法从用户空间访问Map
    .pinning        = PIN_GLOBAL_NS,
    .max_elem       = 2,
};

// 这个是共享的内联函数
static __inline int account_data(struct __sk_buff *skb, uint32_t dir)
{
    uint32_t *bytes;
    // 将Map传递给助手函数
    bytes = map_lookup_elem(&amp;acc_map, &amp;dir);
    if (bytes)
            lock_xadd(bytes, skb-&gt;len);

    return TC_ACT_OK;
}

// 两个段，都会调用account_data往Map中写入数据
__section("ingress")
int tc_ingress(struct __sk_buff *skb)
{
    return account_data(skb, 0);
}

__section("egress")
int tc_egress(struct __sk_buff *skb)
{
    return account_data(skb, 1);
}

char __license[] __section("license") = "GPL";</pre>
<p>使用下面的命令编译：</p>
<pre class="crayon-plain-tag">clang -O2 -Wall -target bpf -c tc-example.c -o tc-example.o</pre>
<p>利用tc加载该程序：</p>
<pre class="crayon-plain-tag">tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc-example.o sec ingress
tc filter add dev eth0 egress bpf da obj tc-example.o sec egress

tc filter show dev eth0 ingress
# filter protocol all pref 49152 bpf
# filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[ingress] direct-action id 1 tag c5f7825e5dac396f

tc filter show dev em1 egress
# filter protocol all pref 49152 bpf
# filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[egress] direct-action id 2 tag b2fd5adc0f262714

mount | grep bpf
# sysfs on /sys/fs/bpf type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
# bpf on /sys/fs/bpf type bpf (rw,relatime,mode=0700)

tree /sys/fs/bpf/
# /sys/fs/bpf/
# +-- ip -&gt; /sys/fs/bpf/tc/
# +-- tc
# |   +-- globals
# |       +-- acc_map
# +-- xdp -&gt; /sys/fs/bpf/tc/</pre>
<p>一旦有封包通过eth0接口，则acc_map中的计数器值就会增加。</p>
<div class="blog_h3"><span class="graybg">没有全局变量</span></div>
<p>除非内核版本在5.5+以上，BPF程序中没有普通C程序中的全局变量。5.5+的全局变量底层仍然是基于BPF Map实现的。</p>
<p>作为变通方案，可以使用BPF_MAP_TYPE_PERCPU_ARRAY类型的Map，这种Map为每个CPU核心存储一个任意大小的值。由于<span style="background-color: #c0c0c0;">BPF程序在运行过程中绝不会被抢占</span>，在此Map中初始化一个临时的缓冲区（例如为了突破栈的大小限制）用作全局变量是安全的。在<span style="background-color: #c0c0c0;">发生尾调用的情况下也不会发生抢占，Map的内容不会消失</span>。</p>
<p>对于任何需要跨越多次BPF程序运行保存的状态，都使用普通BPF Map即可。</p>
<div class="blog_h3"><span class="graybg">没有常量字符串或数组</span></div>
<p>由于一切内联、不支持全局变量，定义<pre class="crayon-plain-tag">const</pre>的字符串或其它数组都是不被支持的，生成在ELF文件中的relocation entry会被BPF ELF loaders拒绝。</p>
<p>打印调试消息可以利用助手函数trace_printk：</p>
<pre class="crayon-plain-tag">static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);

#ifndef printk
# define printk(fmt, ...)                                      \
    ({                                                         \
        char ____fmt[] = fmt;                                  \
        trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
    })
#endif


// 调用
printk("skb len:%u\n", skb-&gt;len);

// 在用于空间使用下面的命令查看打印的消息
// tc exec bpf dbg</pre>
<p>不建议在生产环境使用trace_printk()助手函数，因为类似于<pre class="crayon-plain-tag">"skb len:%u\n"</pre>这样的字符串必须在每次调用时载入到BPF Stack，此外助手函数最多支持5个参数。</p>
<p>对于网络应用，可以使用<pre class="crayon-plain-tag">skb_event_output()</pre>或者<pre class="crayon-plain-tag">xdp_event_output()</pre>代替，它们允许从BPF程序传递自定义的结构体，外加一个可选的packet sample到perf event ring buffer。</p>
<div class="blog_h3"><span class="graybg">使用LLVM内置函数</span></div>
<p>除了助手函数之外，BPF程序不能发起任何函数调用，因此公共库代码需要实现为内联函数。此外LLVM提供的一些builtins可以被使用，并且总是保证被内联：</p>
<pre class="crayon-plain-tag">#ifndef memset
# define memset(dest, chr, n)   __builtin_memset((dest), (chr), (n))
#endif

#ifndef memcpy
# define memcpy(dest, src, n)   __builtin_memcpy((dest), (src), (n))
#endif

#ifndef memmove
# define memmove(dest, src, n)  __builtin_memmove((dest), (src), (n))
#endif</pre>
<p>内置函数memcmp()存在一些边缘场景，会导致不发生inline，因此在LLVM解决此问题之前不推荐使用。</p>
<div class="blog_h3"><span class="graybg">尚不支持循环</span></div>
<p>BPF Verifier会检查程序代码，确保其不包含循环，目的是确保程序总是能停止。</p>
<p>只有非常特殊形式的循环被允许：</p>
<pre class="crayon-plain-tag">// 使用该指令，内核5.3+不再需要
#pragma unroll
    // 循环的upper buounds是常量
    for (i = 0; i &lt; IPV6_MAX_HEADERS; i++) {
        // ...
    }</pre>
<p>另外一种比较刁钻的实现循环的方式是，自我尾调用， 使用一个BPF_MAP_TYPE_PERCPU_ARRAY作为变量存储。这种方式的循环次数是动态的，但是最多迭代34次（初始程序，加上最多33次尾调用）。</p>
<div class="blog_h3"><span class="graybg">使用尾调用</span></div>
<p>尾调用提供了一种灵活的、在运行时原子的修改程序行为的方式，它的做法是从一个BPF程序跳转到另外一个，同时保留当前栈帧。尾调用没有return的概念，当前程序直接被替换掉。</p>
<p>发起尾调用时必须使用BPF_MAP_TYPE_PROG_ARRAY类型的Map，传递目标BPF程序所在索引。</p>
<p>使用尾调用可以非常灵活的对程序进行“分区”。例如挂钩到XDP或tc的根BPF程序可以发起对索引为0的BPF程序的尾调用，后者执行流量采样，然后跳转到BPF程序1，在此应用防火墙策略。封包在此被丢弃，或进一步尾调用2来处理，BPF程序2修改封包并将其从网络接口发出。</p>
<pre class="crayon-plain-tag">#ifndef __stringify
# define __stringify(X)   #X
#endif

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __section_tail
# define __section_tail(ID, KEY)          \
   __section(__stringify(ID) "/" __stringify(KEY))
#endif

#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...)              \
   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
#endif

#define BPF_JMP_MAP_ID   1

static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
                     uint32_t index);

// 创建一个eBPF程序数组并且钉到BPF文件系统的全局命名空间下的/jmp_map节点
struct bpf_elf_map jmp_map __section("maps") = {
    .type           = BPF_MAP_TYPE_PROG_ARRAY,
    .id             = BPF_JMP_MAP_ID,
    .size_key       = sizeof(uint32_t),
    .size_value     = sizeof(uint32_t),
    .pinning        = PIN_GLOBAL_NS,
    .max_elem       = 1,
};

// iproute2的BPF ELF loader能够识别标记为__section_tail()的块，将其存放到某个程序数组中
// 第一个参数ID用于决定存放到哪个数组，第二个参数用于决定存放到数组的哪个索引
// 不仅仅是tc，任何iproute2支持的BPF程序类型（XDP，lwt等）都可以使用这种标记
__section_tail(BPF_JMP_MAP_ID, 0)
int looper(struct __sk_buff *skb)
{
    printk("skb cb: %u\n", skb-&gt;cb[0]++);
    tail_call(skb, &amp;jmp_map, 0);
    return TC_ACT_OK;
}

// 主程序
__section("prog")
int entry(struct __sk_buff *skb)
{
    skb-&gt;cb[0] = 0;
    // 发起尾调用
    tail_call(skb, &amp;jmp_map, 0);
    return TC_ACT_OK;
}

char __license[] __section("license") = "GPL";</pre>
<p>钉在BPF伪文件系统的BPF程序数组，可被用户空间程序查询或修改，tc也提供了更新BPF程序的命令：</p>
<pre class="crayon-plain-tag">#                   更换globals/jmp_map的 0索引元素
#                                             用new.o的 foo段代替
tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo</pre>
<div class="blog_h3"><span class="graybg">受限的栈空间</span></div>
<p>BPF程序的栈空间仅有512字节，因此编码时需要小心。要使用一个较大的缓冲区，可以从BPF_MAP_TYPE_PERCPU_ARRAY类型的Map分配。</p>
<div class="blog_h3"><span class="graybg">去除字节对齐补白</span></div>
<p>现代编译器默认情况下会进行字节边界对齐 —— 结构体成员被对齐到是它们长度的整数倍的内存边界，空出的部分自动补白：</p>
<pre class="crayon-plain-tag">struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
}; // size of 20-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte

// Actual compiled composition of struct called_info
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |  PADDING  | &lt;= address aligned to 8
//  |____________|___________|     with 4-byte PADDING.</pre>
<p>由于字节对齐的原因，结构体的大小通常比期望的大。</p>
<p>BPF Verifier会检查栈的边界，确保程序不会越界（512字节）访问，或者访问未初始化的栈区域。使用带补白的结构作为Map的值，可能导致在<pre class="crayon-plain-tag">bpf_prog_load()</pre>时报invalid indirect read from stack错。</p>
<p>你需要使用pack指令移除补白：</p>
<pre class="crayon-plain-tag">#pragma pack(4)
struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
}; // size of 20-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 20-byte

// Actual compiled composition of packed struct called_info
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |             &lt;= address aligned to 4
//  |____________|                 with no PADDING.</pre>
<p>移除字节对齐补白后，会导致CPU内存访问效率的降低，在某些体系结构下，不对齐的访问（unaligned access）可能被Verifier拒绝。</p>
<p>所以，最优的方式是人工添加仅用于实现字节对齐的pad字段：</p>
<pre class="crayon-plain-tag">struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
    u32 pad;    // 4-byte
}; // size of 24-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte

// Actual compiled composition of struct called_info with explicit padding
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |  pad (4)  | &lt;= address aligned to 8
//  |____________|___________|     with explicit PADDING.</pre>
<div class="blog_h3"><span class="graybg">无效引用问题</span></div>
<p>诸如bpf_skb_store_bytes之类的助手函数，会导致封包的size发生变化。由于Verifier无法在运行时跟踪这种变化，因此一旦调用了这类助手函数，对数据的引用（指针）立刻会被Verifer无效化（invalidated）：</p>
<pre class="crayon-plain-tag">struct iphdr *ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;new_saddr, 4, 0);

// Verifier会拒绝下面的代码，因为此处ip4这个指针已经无效了，不能解引用
if (ip4-&gt;protocol == IPPROTO_TCP) {
    // do something
}</pre>
<p>解决办法是重新获取引用：</p>
<pre class="crayon-plain-tag">struct iphdr *ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;new_saddr, 4, 0);

// 重新获取引用
ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

if (ip4-&gt;protocol == IPPROTO_TCP) {
    // do something
}</pre>
<div class="blog_h2"><span class="graybg">bpftool</span></div>
<p>bpftool是内核提供（tools/bpf/bpftool/）的，主要的BPF内省（introspection）和调试工具。它支持：</p>
<ol>
<li>Dump出当前加载到系统中的所有BPF程序，以及Maps</li>
<li>列出指定程序使用的Maps</li>
<li>Dump中Map的所有键值对</li>
<li>对Map键值对进行增删改查</li>
<li>将Map或程序钉到BPF伪文件系统</li>
</ol>
<p>指定bpftool操作的目标时，可以使用ID，或者目标在BPF伪文件系统中的路径。</p>
<div class="blog_h3"><span class="graybg">查看程序和Map</span></div>
<p>列出所有程序：</p>
<pre class="crayon-plain-tag">bpftool prog</pre>
<p>输出为JSON：</p>
<pre class="crayon-plain-tag"># 对于所有子命令，都支持输出JSON
bpftool prog --json --pretty</pre>
<p>列出所有Map：</p>
<pre class="crayon-plain-tag">bpftool map</pre>
<p>查看特定程序：</p>
<pre class="crayon-plain-tag">bpftool prog show id 406
#    程序类型为sched_cls，即BPF_PROG_TYPE_SCHED_CLS
406: sched_cls  tag e0362f5bd9163a0a
#    加载此程序的用户和时间
     loaded_at Apr 09/16:24  uid 0
#    指令序列长度为11144字节
#                   JIT编译后的映像为7721字节
#                               程序本身（不包含Map）占用空间122888字节
#                                                使用的Maps列表
     xlated 11144B  jited 7721B  memlock 12288B  map_ids 18,20,8,5,6,14</pre>
<div class="blog_h3"><span class="graybg">Dump程序指令</span></div>
<p>使用下面的命令可以dump出BPF程序的指令：</p>
<pre class="crayon-plain-tag"># bpftool prog dump xlated id 406
 0: (b7) r7 = 0
 1: (63) *(u32 *)(r1 +60) = r7
 2: (63) *(u32 *)(r1 +56) = r7
 3: (63) *(u32 *)(r1 +52) = r7
[...]
47: (bf) r4 = r10
48: (07) r4 += -40
49: (79) r6 = *(u64 *)(r10 -104)
50: (bf) r1 = r6
51: (18) r2 = map[id:18]                    # &lt;-- 使用ID为18的Map
53: (b7) r5 = 32
54: (85) call bpf_skb_event_output#5656112  # &lt;-- 调用助手函数
55: (69) r1 = *(u16 *)(r6 +192)
[...]</pre>
<p>使用下面的命令可以dump出程序JIT后的汇编指令：</p>
<pre class="crayon-plain-tag"># bpftool prog dump jited id 406
 0:        push   %rbp
 1:        mov    %rsp,%rbp
 4:        sub    $0x228,%rsp
 b:        sub    $0x28,%rbp
 f:        mov    %rbx,0x0(%rbp)
13:        mov    %r13,0x8(%rbp)
17:        mov    %r14,0x10(%rbp)
1b:        mov    %r15,0x18(%rbp)
1f:        xor    %eax,%eax
21:        mov    %rax,0x20(%rbp)
25:        mov    0x80(%rdi),%r9d</pre>
<div class="blog_h3"><span class="graybg">Dump Map</span></div>
<p>Dump整个Map：</p>
<pre class="crayon-plain-tag">bpftool map dump id 5</pre>
<div class="blog_h2"><span class="graybg">libbpf</span></div>
<p>libbpf是一个C/C++库，作为内核的一部分进行维护，位于tools/lib/bpf目录下。内核自带的eBPF代码样例均依赖于此库。libbpf提供了一个eBPF loader，用于处理LLVM生成的ELF文件，将其载入内核。libbpf中的一部分特性源自BCC，它也包含了一些额外的功能，例如全局变量、BPF Skeletons。</p>
<div class="blog_h2"><span class="graybg">BPF系统调用</span></div>
<p>为了支持eBPF相关操作，例如载入eBPF程序、挂钩到特定事件、创建和访问eBPF Map，Linux中引入了一个新的系统调用<pre class="crayon-plain-tag">bpf</pre>。</p>
<p>该系统调用的签名如下：</p>
<pre class="crayon-plain-tag">//      命令      用于内核和用户空间的数据交互  attr的字节数
int bpf(int cmd, union bpf_attr *attr,      unsigned int size);
// cmd有很多，要么和eBPF程序交互、要么和eBPF Map交互，或者同时和两者交互</pre>
<div class="blog_h3"><span class="graybg">BPF_PROG_LOAD</span></div>
<p>该命令用于载入eBPF程序。载入的时候需要指明程序的类型。程序类型决定了以下事项：</p>
<ol>
<li>程序在何处挂钩</li>
<li>校验器允许程序调用哪些助手函数</li>
<li>是否允许直接访问网络封包数据</li>
<li>传递给程序的<span style="background-color: #c0c0c0;">第一个参数的对象的类型</span></li>
</ol>
<p>可以看到，程序类型规定了eBPF程序的API接口。某些时候，定义一个新的程序类型，仅仅是为了限制可调用函数的列表，例如BPF_PROG_TYPE_CGROUP_SKB、BPF_PROG_TYPE_SOCKET_FILTER</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_CREATE</span></div>
<p>用于创建eBPF Maps，参考下文。</p>
<div class="blog_h2"><span class="graybg">BPF程序类型</span></div>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SOCKET_FILTER</span></div>
<p>网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_KPROBE</span></div>
<p>挂钩到一个KProbe，BPF程序在某个内核函数被调用时触发</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SCHED_CLS</span></div>
<p>网络流量控制（TC）的分类器（classifier）</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SCHED_ACT</span></div>
<p>网络流量控制（TC）动作（action）</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_TRACEPOINT</span></div>
<p>挂钩到一个Tracepoint，当执行到内核特定的代码路径时触发BPF程序</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_XDP</span></div>
<p>在设备驱动接收路径上运行的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_PERF_EVENT</span></div>
<p>决定是否应当触发一个Perf Event Handler</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_CGROUP_SKB</span></div>
<p>为控制组提供的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_CGROUP_SOCK</span></div>
<p>同上，但是允许修改套接字选项</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_LWT_*</span></div>
<p>用于轻量级隧道（lightweight tunnels ）的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SOCK_OPS</span></div>
<p>用于设置套接字选项</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SK_SKB</span></div>
<p>用于在套接字之间转发封包</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_CGROUP_DEVICE</span></div>
<p>决定是否允许一个设备操作</p>
<div class="blog_h2"><span class="graybg">BPF Map</span></div>
<p>部分Map专供特定的助手函数使用，以实现特殊任务。</p>
<div class="blog_h3"><span class="graybg">定义Map</span></div>
<p>要在BPF程序中定义一个Map，需要用到如下结构：</p>
<pre class="crayon-plain-tag">struct bpf_map_def {
        unsigned int type; // Map类型
        unsigned int key_size; // 键的长度
        unsigned int value_size; // 值的长度
        unsigned int max_entries; // 最大键值对数量
        unsigned int map_flags; // 标记
        unsigned int inner_map_idx;
        unsigned int numa_node;
};</pre>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">struct bpf_map_def SEC("maps") my_map = {
        .type = BPF_MAP_TYPE_ARRAY,
        .key_size = sizeof(int),
        .value_size = sizeof(u64),
        .max_entries = MAX_CPU,
};</pre>
<p>在加载阶段，<pre class="crayon-plain-tag">bpf_load.c</pre>会扫描BPF object的ELF头以发现Map定义，并调用tools/lib/bpf/bpf.c中的<pre class="crayon-plain-tag">bpf_create_map_node()</pre> 或 <pre class="crayon-plain-tag">bpf_create_map_in_map_node()</pre>来创建Map。这两个函数实际上是调用<pre class="crayon-plain-tag">bpf</pre>系统调用的<pre class="crayon-plain-tag">BPF_MAP_CREATE</pre>命令。</p>
<p>除非你在编写lwt或tc等类型的BPF程序，你都应该使用上面的方式来定义Map。tc之类的程序使用iproute2作为loader，可以使用下面的结构来定义Map：</p>
<pre class="crayon-plain-tag">#define PIN_GLOBAL_NS           2

struct bpf_elf_map {
        __u32 type;
        __u32 size_key;
        __u32 size_value;
        __u32 max_elem;
        __u32 flags;
        __u32 id;
        __u32 pinning;
};    

struct bpf_elf_map SEC("maps") tun_iface = {
        .type = BPF_MAP_TYPE_ARRAY,
        .size_key = sizeof(int),
        .size_value = sizeof(int),
        .pinning = PIN_GLOBAL_NS,
        .max_elem = 1,

};</pre>
<div class="blog_h3"><span class="graybg">钉住Map</span></div>
<p>所谓Pinning是指通过文件系统路径来暴露Map。对于tc之类的iproute2加载的程序，可以使用这些宏：</p>
<pre class="crayon-plain-tag">#define PIN_NONE        0
#define PIN_OBJECT_NS        1
// 钉到/sys/fs/bpf/tc/globals/ 
#define PIN_GLOBAL_NS        2</pre>
<p>其它程序，你可以手工调用libbpf钉住Map： <pre class="crayon-plain-tag">bpf_obj_pin(fd, path)</pre>。其它程序可以调用<pre class="crayon-plain-tag">mapfd = bpf_obj_get(pinned_file_path);</pre>获得Map的文件描述符。</p>
<div class="blog_h3"><span class="graybg">操控Map</span></div>
<p>检查头文件<pre class="crayon-plain-tag">linux/bpf_types.h</pre>你会发现，不同的Map的操作，是由<pre class="crayon-plain-tag">bpf_map_ops</pre>结构所引用的不同函数指针实现的。</p>
<pre class="crayon-plain-tag">BPF_MAP_TYPE(BPF_MAP_TYPE_ARRAY, array_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERCPU_ARRAY, percpu_array_map_ops)</pre>
<p>不过对于BPF开发者来说，所有Map都可以在eBPF或用户空间程序中，通过<pre class="crayon-plain-tag">bpf_map_lookup_elem()</pre>和<pre class="crayon-plain-tag">bpf_map_update_elem()</pre>函数访问。</p>
<div class="blog_h2"><span class="graybg">Map类型</span></div>
<p>所有的Map类型定义在枚举<pre class="crayon-plain-tag">bpf_map_type</pre>中。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_HASH</span></div>
<p>哈希表。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_ARRAY</span></div>
<p>Array Map，为快速查找优化。</p>
<p>键是数组索引值（4字节，64bit），不支持删除键值。所有其它Array Map都具有此特征。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PROG_ARRAY</span></div>
<p>存放对应eBPF程序的文件描述符的数组。用于实现<pre class="crayon-plain-tag">bpf_tail_call()</pre>需要的跳转表（jump tables）。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERCPU_ARRAY</span></div>
<p>per-CPU Array Map，也就是每个CPU对应一个值。<span style="background-color: #c0c0c0;">键仍然是数字，值则是和CPU个数相同的数组</span>：</p>
<pre class="crayon-plain-tag">long values[nr_cpus];
ret = bpf_map_lookup_elem(map_fd, &amp;next_key, values);
if (ret) {
    perror("Error looking up stat");
    continue;
}
for (i = 0; i &lt; nr_cpus; i++) {
    sum += values[i];
} </pre>
<p>此Map可用于代替栈上变量，分配大的缓冲区，以解决栈空间仅512字节的问题。亦可用作全局变量，以解决较旧版本内核中BPF程序没有原生全局变量支持的问题</p>
<p>由于值是per-CPU的，而执行中的BPF程序不会被抢占。因此只要正确编码（仅访问当前CPU的值），就不会产生竞态条件。对于<span style="background-color: #c0c0c0;">会被频繁执行的代码路径，一般会考虑per-CPU的Map</span>。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERF_EVENT_ARRAY</span></div>
<p>即perfbuf，per-CPU的缓冲区。在内核空间，供<pre class="crayon-plain-tag">bpf_perf_event_output()</pre>函数使用，调用该函数，可以输出指定类型的结构到缓冲区。</p>
<p>在用户空间，可以进行epoll，当有数据输出时会得到通知。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_RINGBUF</span></div>
<p>即ringbuf，perfbuf的继任，所有CPU共享的一个环形缓冲区。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_CGROUP_ARRAY</span></div>
<p>在用户空间，存放cgroup的文件描述符。</p>
<p>在内核空间，调用<pre class="crayon-plain-tag">bpf_skb_under_cgroup()</pre>来检查skb是否和Map中指定索引的cgroup关联。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERCPU_HASH</span></div>
<p>per-CPU的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LRU_HASH</span></div>
<p>使用LRU算法的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LRU_PERCPU_HASH</span></div>
<p>per-CPU的使用LRU算法的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LPM_TRIE</span></div>
<p>最长前缀匹配的字典树（<a href="/trie">trie</a>），可用于IP地址范围匹配。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_STACK_TRACE</span></div>
<p>存储栈追踪信息。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_ARRAY_OF_MAPS</span></div>
<p>Map的数组。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_HASH_OF_MAPS</span></div>
<p>Map的Map。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_DEVICE_MAP</span></div>
<p>存储和查找网络设备引用。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_SOCKMAP</span></div>
<p>存储和查找套接字，允许基于助手函数，实现套接字重定向。</p>
<div class="blog_h2"><span class="graybg">助手函数</span></div>
<p>助手函数由libbpf库提供，定义在<pre class="crayon-plain-tag">bpf_helpers.h</pre>中。</p>
<div class="blog_h3"><span class="graybg">bpf_map_lookup_elem</span></div>
<p>签名： <pre class="crayon-plain-tag">void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)</pre></p>
<p>查找eBPF中和一个Key关联的条目。如果找不到条目，返回NULL</p>
<div class="blog_h3"><span class="graybg">bpf_map_update_elem</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)</pre></p>
<p>添加或修改一个Key关联的值。flags可以是以下之一：</p>
<ol>
<li>BPF_NOEXIST 键值必须不存在，即执行添加操作。不能和BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_PERCPU_ARRAY联用</li>
<li>BPF_EXIST，键值必须存在，即执行更新操作</li>
<li>BPF_ANY，更新或修改</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_map_delete_elem</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_map_delete_elem(struct bpf_map *map, const void *key)</pre></p>
<p>从eBPF Map中删除条目</p>
<div class="blog_h3"><span class="graybg">bpf_probe_read</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_read(void *dst, u32 size, const void *src)</pre></p>
<p>对于Tracing用途的eBPF程序，可以安全的从src读取size字节存储到dst</p>
<div class="blog_h3"><span class="graybg">bpf_ktime_get_ns</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_ktime_get_ns(void)</pre></p>
<p>读取系统从启动到现在的纳秒数，返回ktime</p>
<div class="blog_h3"><span class="graybg">bpf_trace_printk</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_trace_printk(const char *fmt, u32 fmt_size, ...)</pre></p>
<p>类似于printk()的调试工具，从DebugFS打印由fmt所定义的格式化字符串到<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace</pre>。最多支持3个额外的u64参数。</p>
<p>每当此函数被调用，它都会打印一行到trace，格式取决于配置<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace_options</pre>。默认格式如下：</p>
<pre class="crayon-plain-tag"># 当前任务的名字
#      当前任务的PID
#            当前CPU序号
#                  每个字符表示一个选项
#                       时间戳
#                                      BPF使用的指令寄存器的Fake值
telnet-470   [001] .N.. 419421.045894: 0x00000001: &lt;formatted msg&gt;</pre>
<p>可以使用的格式化占位符：<pre class="crayon-plain-tag">%d, %i, %u, %x, %ld, %li, %lu, %lx, %lld, %lli, %llu, %llx, %p, %s</pre>。不支持长度、补白等修饰符。</p>
<p>该函数比较缓慢，应该仅用于调试目的。</p>
<div class="blog_h3"><span class="graybg">bpf_get_prandom_u32</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_prandom_u32(void)</pre></p>
<p>获得一个伪随机数。</p>
<div class="blog_h3"><span class="graybg">bpf_get_smp_processor_id</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_smp_processor_id(void)</pre></p>
<p>得到SMP处理器ID，需要注意，所有eBPF都在禁止抢占的情况下运行，这意味着在eBPF程序的执行过程中，此ID不会改变。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_store_bytes</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_store_bytes(struct sk_buff *skb, u32 offset, const void *from, u32 len, u64 flags)</pre></p>
<p>存储缓冲区from的len字节到，skb所关联的封包的offset位置。flags是以下位域的组合：</p>
<ol>
<li>BPF_F_RECOMPUTE_CSUM：自动重新计算修改后的封包的Checksum</li>
<li>BPF_F_INVALIDATE_HASH：重置<pre class="crayon-plain-tag">skb-&gt;hash</pre> <pre class="crayon-plain-tag">skb-&gt;swhash</pre> <pre class="crayon-plain-tag">skb-&gt;l4hash</pre>为0</li>
</ol>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_load_bytes</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_load_bytes(const struct sk_buff *skb, u32 offset, void *to, u32 len)</pre></p>
<p>从skb中的offset位置读取len长的数据，存放到to缓冲区。</p>
<p>从4.7开始，该函数的功能基本被直接封包访问（direct packet access）代替 —— <pre class="crayon-plain-tag">skb-&gt;data</pre>和<pre class="crayon-plain-tag">skb-&gt;data_end</pre>给出了封包数据的位置。如果希望一次性读取大量数据到eBPF，仍然可以使用该函数。</p>
<div class="blog_h3"><span class="graybg">bpf_l3_csum_replace</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_l3_csum_replace(struct sk_buff *skb, u32 offset, u64 from, u64 to, u64 size)</pre></p>
<p>重新计算L3（IP）的Checksum。计算是增量进行的，因此助手函数必须知道被修改的头字段的前值（from）、修改后的值（to），以及被修改字段的字节数（size，2或4）。你亦可将from和size设置为0，并将字段修改前后的差存放到to。offset用于指示封包的IP Checksum的位置</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_l4_csum_replace</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_l4_csum_replace(struct sk_buff *skb, u32 offset, u64 from, u64 to, u64 flags)</pre></p>
<p>重新计算L4（TCP/UDP/ICMP）的Checksum。计算是增量进行的，因此助手函数必须知道被修改的头字段的前值（from）、修改后的值（to），以及被修改字段的字节数（存放在flags的低4bit，2或4）。你亦可将from和flags低4bit设置为0，并将字段修改前后的差存放到to。offset用于指示封包的IP Checksum的位置。</p>
<p>flags的高位用于存放以下标记：</p>
<ol>
<li>BPF_F_MARK_MANGLED_0，如果Checksum是null，则不去修改它，除非设置了BPF_F_MARK_ENFORCE</li>
<li>CSUM_MANGLED_0，对于导致Checksum为null的更新操作，设置此标记</li>
<li>BPF_F_PSEUDO_HDR，提示使用pseudo-header来计算Checksum</li>
</ol>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_tail_call</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)</pre></p>
<p>这是一个特殊的助手函数，用于触发尾调用 —— 跳转到另外一个eBPF程序。新程序将使用一样的栈帧，但是被调用者不能访问调用者在栈上存储的值，以及寄存器。</p>
<p>使用场景包括：</p>
<ol>
<li>突破eBPF程序长度限制</li>
<li>在不同条件下进行跳转（到子程序）</li>
</ol>
<p>出于安全原因，可以连续执行的尾调用次数是受限制的。限制定义在内核宏MAX_TAIL_CALL_CNT中，默认32，无法被用户空间访问</p>
<p>当调用发生后，程序尝试跳转到prog_array_map（BPF_MAP_TYPE_PROG_ARRAY类型的Map）的index索引处的eBPF程序，并且将当前ctx传递给它。</p>
<p>如果调用成功，则当前程序被替换掉，不存在函数调用返回。如果调用失败，则不产生任何作用，当前程序继续运行后续指令。失败的原因包括：</p>
<ol>
<li>指定的index不存在eBPF程序</li>
<li>当前尾调用链的长度超过限制</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_clone_redirect</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_clone_redirect(struct sk_buff *skb, u32 ifindex, u64 flags)</pre></p>
<p>克隆skb关联的封包，并且重定向到由ifindx所指向的网络设备。入站/出站路径都可以用于重定向。标记BPF_F_INGRESS用于确定是重定向到入站（ingress）还是出站（egress）路径，如果该标记存在则入站。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_redirect</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_redirect(u32 ifindex, u64 flags)</pre></p>
<p>重定向封包到ifindex所指向的网络设备。类似于bpf_clone_redirect，但是不会进行封包克隆，因而性能较好。缺点是，redirect操作实际上是在eBPF程序返回后的某个代码路径上发生的。</p>
<p>除了XDP之外，入站/出站路径都可以用于重定向。标记BPF_F_INGRESS用于指定是ingress还是egress。当前XDP仅仅支持重定向到egress接口，不支持设置flag</p>
<p>对于XDP，成功返回XDP_REDIRECT，出错返回XDP_ABORTED。对于其它eBPF程序，成功返回TC_ACT_REDIRECT，出错返回TC_ACT_SHOT</p>
<div class="blog_h3"><span class="graybg">bpf_redirect_map</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_redirect_map(struct bpf_map *map, u32 key, u64 flags)</pre></p>
<p>将封包重定向到map的key键指向的endpoint。根据map的类型，它的值可能指向：</p>
<ol>
<li>网络设备，用于转发封包到其它ports</li>
<li>CPU，用于重定向XDP帧给其它CPU，仅仅支持Native（驱动层支持的） XDP</li>
</ol>
<p>flags必须置零。</p>
<p>当重定向给网络设备时，该函数比bpf_redirect性能更好。这是由一系列底层实现细节之一决定的，其中之一是该函数会以bulk方式将封包发送给设备。</p>
<p>如果成功返回XDP_REDIRECT，否则返回XDP_ABORTED。</p>
<div class="blog_h3"><span class="graybg">bpf_sk_redirect_map</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_sk_redirect_map(struct bpf_map *map, u32 key, u64 flags)</pre></p>
<p>将封包重定向给map（类型BPF_MAP_TYPE_SOCKMAP）的key所指向的套接字。ingress/egress接口都可以用于重定向。标记BPF_F_INGRESS用于确定是不是ingress。</p>
<p>如果成功返回SK_PASS，否则返回SK_DROP。</p>
<div class="blog_h3"><span class="graybg">bpf_sock_map_update</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_sock_map_update(struct bpf_sock_ops *skops, struct bpf_map *map, void *key, u64 flags)</pre></p>
<p>添加/更新map的条目，skopts作为key的新值。flags是以下其中之一：</p>
<ol>
<li>BPF_NOEXIST，仅添加</li>
<li>BPF_EXIST，仅更新</li>
<li>BPF_ANY，添加或更新</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_vlan_push</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_vlan_push(struct sk_buff *skb, __be16 vlan_proto, u16 vlan_tci)</pre> </p>
<p>将vlan_proto协议的vlan_tci（VLAN Tag控制信息）Push给skb关联的封包，并且更新Checksum。需要注意ETH_P_8021Q和ETH_P_8021AD的vlan_proto是不一样的，这里使用前者。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_vlan_pop</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_vlan_pop(struct sk_buff *skb)</pre></p>
<p>弹出skb关联的封包的VLAN头。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_get_tunnel_key</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_get_tunnel_key(struct sk_buff *skb, struct bpf_tunnel_key *key, u32 size, u64 flags)</pre></p>
<p>获取隧道（外层报文）的元数据，skb关联的封包的隧道元数据被填充到key，长度size。标记BPF_F_TUNINFO_IPV6提示隧道是基于IPv6而非IPv4。</p>
<p><pre class="crayon-plain-tag">bpf_tunnel_key</pre>是一个容器结构，它将各种隧道协议的主要参数都存入其中，这样eBPF程序可以方便的根据封装（外层）报文的头来作出各种决定。</p>
<p>对端的IP地址被存放在<pre class="crayon-plain-tag">key-&gt;remote_ipv4</pre> 或 <pre class="crayon-plain-tag">key-&gt;remote_ipv6</pre></p>
<p>通过<pre class="crayon-plain-tag">key-&gt;tunnel_id</pre>可以访问隧道的ID，通常映射到VNI（虚拟网络标识符），调用<pre class="crayon-plain-tag">bpf_skb_set_tunnel_key()</pre>函数需要用到</p>
<p>下面这个示例用在隧道一端的TC Ingress接口，可以过滤掉对端隧道IP不是10.0.0.1的封包：</p>
<pre class="crayon-plain-tag">int ret;
struct bpf_tunnel_key key = {};

ret = bpf_skb_get_tunnel_key(skb, &amp;key, sizeof(key), 0);
if (ret &lt; 0)
        return TC_ACT_SHOT;     // drop packet

if (key.remote_ipv4 != 0x0a000001)
        return TC_ACT_SHOT;     // drop packet

return TC_ACT_OK;               // accept packet</pre>
<p>支持VxLAN、Geneve、GRE、IPIP等类型的隧道。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_set_tunnel_key</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_set_tunnel_key(struct sk_buff *skb, struct bpf_tunnel_key *key, u32 size, u64 flags)</pre></p>
<p>为skb关联的封包生成隧道元数据。隧道元数据被设置为长度为size的bpf_tunnel_key结构。flags是如下位域的组合：</p>
<ol>
<li>BPF_F_TUNINFO_IPV6 指示隧道基于IPv6而非IPv4</li>
<li>BPF_F_ZERO_CSUM_TX 对于IPv4封包，添加一个标记到隧道元数据，提示应该跳过Checksum计算，将其置零</li>
<li>BPF_F_DONT_FRAGMENT，添加一个标记到隧道元数据，提示封包不得被分片（fragmented）</li>
<li>BPF_F_SEQ_NUMBER，添加一个标记到隧道元数据，提示发送封包之前，需要添加sequence number</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">struct bpf_tunnel_key key;
// populate key ...
bpf_skb_set_tunnel_key(skb, &amp;key, sizeof(key), 0);
bpf_clone_redirect(skb, vxlan_dev_ifindex, 0);</pre>
<div class="blog_h3"><span class="graybg">bpf_skb_get_tunnel_opt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_get_tunnel_opt(struct sk_buff *skb, u8 *opt, u32 size)</pre></p>
<p>从skb关联的封包中获取隧道选项元数据，并且将原始的隧道选项信息存储到大小为size的opt中。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_set_tunnel_opt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_set_tunnel_opt(struct sk_buff *skb, u8 *opt, u32 size)</pre></p>
<p>将隧道选项元数据设置给skb关联的封包。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_proto</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_proto(struct sk_buff *skb, __be16 proto, u64 flags)</pre></p>
<p>将skb的协议改为proto。目前仅仅支持将IPv4改为IPv6。助手函数会做好底层工作，例如修改套接字缓冲的大小。eBPF程序需要调用<pre class="crayon-plain-tag">skb_store_bytes</pre>填充必要的新的报文头字段，并调用<pre class="crayon-plain-tag">bpf_l3_csum_replace</pre>、<pre class="crayon-plain-tag">bpf_l4_csum_replace</pre>重新计算Checksum。</p>
<p>该助手函数的主要意义是执行一个NAT64操作。</p>
<p>在内部实现上，封包的GSO（generic segmentation offload）类型标记为dodgy，因而报文头被检查，TCP分段被GSO/GRO引擎重新分段。</p>
<p>flags必须清零，这个参数暂时没有使用。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_csum_diff</span></div>
<p>签名：<pre class="crayon-plain-tag">s64 bpf_csum_diff(__be32 *from, u32 from_size, __be32 *to, u32 to_size, __wsum seed)</pre></p>
<p>计算两个缓冲区from到to的checksum difference。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_type</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_type(struct sk_buff *skb, u32 type)</pre></p>
<p>修改封包类型，即设置<pre class="crayon-plain-tag">skb-&gt;pkt_type</pre>为type。主要用途是将skb改为PACKET_HOST。type的取值：</p>
<ol style="list-style-type: undefined;">
<li>PACKET_HOST 单播给本机的封包</li>
<li>PACKET_BROADCAST 广播封包</li>
<li>PACKET_MULTICAST 组播封包</li>
<li>PACKET_OTHERHOST单播给其它机器的封包</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_change_head</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_head(struct sk_buff *skb, u32 len, u64 flags)</pre></p>
<p>增长封包的headroom，增长len长度，调整MAC头的偏移量。如果需要，该函数会自动扩展和重新分配内存。</p>
<p>该函数可以用于在L3的skb上，推入一个MAC头，然后将其重定向到L2设备。</p>
<p>flags为保留字段，全部置空。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_under_cgroup</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_under_cgroup(struct sk_buff *skb, struct bpf_map *map, u32 index)</pre></p>
<p>检查skb是否是由BPF_MAP_TYPE_CGROUP_ARRAY类型的Map的index位置所指向的CGroup2的descendant。</p>
<p>返回值：</p>
<p style="padding-left: 30px;">0 ：不是目标Cgroup2的descendant<br />1：是目标Cgroup2的descendant<br />负数：出错</p>
<div class="blog_h3"><span class="graybg">bpf_set_hash_invalid</span></div>
<p>签名：<pre class="crayon-plain-tag">void bpf_set_hash_invalid(struct sk_buff *skb)</pre></p>
<p>无效化<pre class="crayon-plain-tag">skb-&gt;hash</pre>。在通过直接封包访问修改报文头之后调用此函数，以提示哈希值以及过期，内核下一次访问哈希或者调用bpf_get_hash_recalc时会触发哈希值的重新计算。</p>
<div class="blog_h3"><span class="graybg">bpf_get_hash_recalc</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_hash_recalc(struct sk_buff *skb)</pre></p>
<p>获取封包哈希值<pre class="crayon-plain-tag">skb-&gt;hash</pre>，如果该字段没有设置（特别是因为封包修改导致哈希被清空）则计算并设置哈希。后续可以直接访问skb-&gt;哈希获取哈希值。</p>
<p>调用bpf_set_hash_invalid()、bpf_skb_change_proto()、bpf_skb_store_bytes()+BPF_F_INVALIDATE_HASH标记，都会导致哈希值清空，并导致下一次bpf_get_hash_recalc()调用重新生成哈希值。</p>
<div class="blog_h3"><span class="graybg">bpf_set_hash</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_set_hash(struct sk_buff *skb, u32 hash)</pre></p>
<p>设置完整哈希值到skb-&gt;hash</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_tail</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_tail(struct sk_buff *skb, u32 len, u64 flags)</pre></p>
<p>Resize(trim/grow) skb关联的封包到len长。flags必须置零。</p>
<p>改变封包长度后，eBPF程序可能需要调用bpf_skb_store_bytes、bpf_l3_csum_replace、bpf_l3_csum_replace等函数填充数据、重新计算Checksum。</p>
<p>一般用于回复ICMP控制报文。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_pull_data</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_pull_data(struct sk_buff *skb, u32 len)</pre></p>
<p>所谓non-linear的skb，是指被fragmented的skb，即有一部分数据没有存放在skb所在内存，而是存放在其它内存页（可能有多个），并通过skb_shared_info记录这些数据位置。</p>
<p>当skb是non-linear的、并且不是所有len长是linear section的一部分的前提下，拉取skb的non-linear数据。确保skb的len字节是可读写的。如果len设置为0，则拉取拉取skb的整个长度的数据。</p>
<p>进行封包直接访问时，通过<pre class="crayon-plain-tag">skb-&gt;data_end</pre>来测试某个偏移量是否在封包范围内，可能因为两个原因失败：</p>
<ol>
<li>偏移量是无效的</li>
<li>偏移量对应的数据是在skb的non-linear部分中</li>
</ol>
<p>该助手函数可以用来一次性拉取non-linear数据，然后再进行偏移量测试和数据访问。</p>
<p>此函数确保skb是uncloned，这是直接封包访问的前提。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_get_socket_cookie</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_socket_cookie(struct sk_buff *skb)</pre></p>
<p>如果skb关联到一个已知的套接字，则得到套接字的cookie（由内核生成），如果尚未设置cookie，则生成之。一旦cookie生成，在套接字的生命周期范围内都不会改变。</p>
<p>该助手用于监控套接字网络流量统计信息，它在网络命名空间范围内为套接字提供唯一标识。</p>
<div class="blog_h3"><span class="graybg">bpf_get_socket_uid</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_socket_uid(struct sk_buff *skb)</pre></p>
<p>获得套接字的owner UID。如果套接字是NULL，或者不是full socket（time-wait状态，或者是一个request socket），则返回overflowuid，overflowuid不一定是socket的实际UID。</p>
<div class="blog_h3"><span class="graybg">bpf_csum_update</span></div>
<p>签名：<pre class="crayon-plain-tag">s64 bpf_csum_update(struct sk_buff *skb, __wsum csum)</pre></p>
<p>如果驱动已经为整个封包提供了Checksum，那么此函数将csum加到<pre class="crayon-plain-tag">skb-&gt;csum</pre>字段上，其它情况下返回错误。</p>
<p>该助手函数应当和<pre class="crayon-plain-tag">bpf_csum_diff()</pre>联合使用，典型场景是，通过封包直接访问修改了封包内容之后，进行Checksum更新。</p>
<div class="blog_h3"><span class="graybg">bpf_get_route_realm</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_route_realm(struct sk_buff *skb)</pre></p>
<p>得到路由的Realm，也就是skb的destination的tclassid字段。这个字段是用户提供的一个tag，类似于net_cls的classid。不同的是，这里的tag关联到路由条目（destination entry）。</p>
<p>可以在clsact TC egress钩子中调用此函数，或者在经典的classful egress qdiscs上使用。不能在TC ingress路径上使用。</p>
<p>要求内核配置选项CONFIG_IP_ROUTE_CLASSID。</p>
<p>返回skb关联的封包的路由的realm。</p>
<div class="blog_h3"><span class="graybg">bpf_setsockopt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_setsockopt(struct bpf_sock_ops *bpf_socket, int level, int optname, char *optval, int optlen)</pre></p>
<p>针对bpf_socket关联的套接字发起一个setsockopt()操作，此套接字必须是full socket。optname为选项名，optval/optlen指定了选项值，level指定了选项的位置。</p>
<p>该函数实际上实现了setsockopt()的子集，支持以下level：</p>
<ol>
<li>SOL_SOCKET，支持选项SO_RCVBUF, SO_SNDBUF, SO_MAX_PACING_RATE, SO_PRIORITY, SO_RCVLOWAT, SO_MARK</li>
<li>IPPROTO_TCP，支持选项TCP_CONGESTION, TCP_BPF_IW, TCP_BPF_SNDCWND_CLAMP</li>
<li>IPPROTO_IP，支持选项IP_TOS</li>
<li>IPPROTO_IPV6，支持选项IPV6_TCLASS</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_adjust_room</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_adjust_room(struct sk_buff *skb, u32 len_diff, u32 mode, u64 flags)</pre></p>
<p>增加/缩小skb关联的封包的数据的room，增量为len_diff。mode可以是：</p>
<ol>
<li>BPF_ADJ_ROOM_NET，在网络层调整room，即在L3头上增加/移除room space</li>
</ol>
<p>flags必须置零。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_xdp_adjust_head</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_xdp_adjust_head(struct xdp_buff *xdp_md, int delta)</pre></p>
<p>移动<pre class="crayon-plain-tag">xdp_md-&gt;data</pre> delta字节，delta可以是负数。</p>
<p>该函数准备用于push/pop headers的封包。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_xdp_adjust_meta</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_xdp_adjust_meta(struct xdp_buff *xdp_md, int delta)</pre></p>
<p>调整 xdp_md-&gt;data_meta所指向的地址delta字节。该操作改变了存储在xdp_md-&gt;data中的地址信息。</p>
<div class="blog_h3"><span class="graybg">bpf_get_current_task</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_task(void)</pre></p>
<p>获取当前Task结构的指针。</p>
<div class="blog_h3"><span class="graybg">bpf_get_stackid</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_get_stackid(struct pt_reg *ctx, struct bpf_map *map, u64 flags)</pre></p>
<p>获取一个用户/内核栈，得到其ID。需要传入ctx，即当前追踪程序在其中执行的上下文对象，以及一个BPF_MAP_TYPE_STACK_TRACE类型的Map。通过flags指示需要跳过多少栈帧（0-255），masked with BPF_F_SKIP_FIELD_MASK。flags的其它位如下：</p>
<ol>
<li>BPF_F_USER_STACK 收集用户空间的栈，而非内核栈</li>
<li>BPF_F_FAST_STACK_CMP 基于哈希来对比栈</li>
<li>BPF_F_REUSE_STACKID 如果两个不同的栈哈希到同一个stackid，丢弃旧的</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_get_current_pid_tgid</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_pid_tgid(void)</pre></p>
<p>返回一个包含了当前tgid和pid的64bit整数。值为<pre class="crayon-plain-tag">current_task-&gt;tgid &lt;&lt; 32 | current_task-&gt;pid</pre></p>
<div class="blog_h3"><span class="graybg">bpf_get_current_uid_gid</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_uid_gid(void)</pre></p>
<p>返回一个包含了当前GID和UID的整数。值为<pre class="crayon-plain-tag">current_gid &lt;&lt; 32 | current_uid</pre></p>
<div class="blog_h3"><span class="graybg">bpf_get_current_comm</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_get_current_comm(char *buf, u32 size_of_buf)</pre></p>
<p>将当前任务的comm属性拷贝到长度为size_of_buf的buf中。comm属性包含可执行文件的路径</p>
<p>调用成功时助手函数确保buf是NULL-terminated。如果失败，则填满0</p>
<div class="blog_h3"><span class="graybg">bpf_get_cgroup_classid</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_cgroup_classid(struct sk_buff *skb)</pre></p>
<p>得到当前任务的classid，即skb所属的<a href="/cgroup-illustrated#net-cls">net_cls控制组</a>的classid。该助手函数可用于TC的egress路径，不能用于ingress路径。</p>
<p>Linux支持两个版本的Cgroups，v1和v2，用户可以混合使用。但是，net_cls是v1特有的Cgroup。这意味着此助手函数和run on cgroups（v2 only）的eBPF程序不兼容，套接字一次仅仅能携带一个版本Cgroup的数据。</p>
<p>内核必须配置CONFIG_CGROUP_NET_CLASSID=y/m才能使用此助手函数。</p>
<p>返回classid，或者0，即默认的没有被配置的classid。</p>
<div class="blog_h3"><span class="graybg">bpf_probe_write_user</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_write_user(void *dst, const void *src, u32 len)</pre></p>
<p>尝试在以一个安全方式来写入src的len字节到dst中。仅仅对于运行在用户上下文的线程可用，dst必须是有效的用户空间地址。</p>
<p>由于TOC-TOU攻击的原因，此助手函数不得用于实现任何类型的安全机制。</p>
<p>此函数用于试验目的，存在的导致系统、进程崩溃的风险。当调用了此函数的eBPF程序被挂钩后，内核日志会打印一条警告信息，包含PID和进程名信息。</p>
<div class="blog_h3"><span class="graybg">bpf_probe_read_str</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_read_str(void *dst, int size, const void *unsafe_ptr)</pre></p>
<p>从unsafe_ptr拷贝一个NULL结尾的字符串到dst，size包含结尾的NULL字符。如果字符串长度小于size，不会补NUL；如果字符串长度大于size，则截断（保证填充字符串结尾NULL）</p>
<div class="blog_h3"><span class="graybg">bpf_current_task_under_cgroup</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_current_task_under_cgroup(struct bpf_map *map, u32 index)</pre></p>
<p>检查当前正在运行的探针是否在map的index所指向的Cgroup2之下。</p>
<div class="blog_h3"><span class="graybg">bpf_get_numa_node_id</span></div>
<p>签名： <pre class="crayon-plain-tag">int bpf_get_numa_node_id(void)</pre></p>
<p>得到当前NUMA节点的ID。该函数的主要目的是用于选取本地NUMA节点的套接字。</p>
<div class="blog_h3"><span class="graybg">bpf_perf_event_output</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_perf_event_output(struct pt_reg *ctx, struct bpf_map *map, u64 flags, void *data, u64 size)</pre></p>
<p>将长度为size的blob写入到Map所存放的特殊BPF perf event。map的类型是BPF_MAP_TYPE_PERF_EVENT_ARRAY</p>
<p>perf event必须具有属性：</p>
<p style="padding-left: 30px;">sample_type = PERF_SAMPLE_RAW<br />type = PERF_TYPE_SOFTWARE<br />config = PERF_COUNT_SW_BPF_OUTPUT</p>
<p>flags用于指定写入到数组的索引。masked by BPF_F_INDEX_MASK，如果指定BPF_F_CURRENT_CPU则取当前CPU的值。</p>
<p>当前程序的ctx也需要传递给助手函数。</p>
<p>在用户空间，希望读取值的程序需要针对perf event调用perf_event_open() ，然后将文件描述符存储到Map中。这个操作必须在eBPF程序第一次写入数据到Map之前完成。参考内核中的例子samples/bpf/trace_output_user.c </p>
<p>要和用户空间进行数据交互，该函数优于bpf_trace_printk()，性能更好。适合从eBPF程序stream数据给用户空间读取。</p>
<div class="blog_h3"><span class="graybg">bpf_perf_event_read</span></div>
<p>签名： <pre class="crayon-plain-tag">u64 bpf_perf_event_read(struct bpf_map *map, u64 flags)</pre></p>
<p>读取一个perf event counter的值，该助手函数操作BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。这个Map本质上是一个数组，它的size和CPU数量一致，其值和对应CPU相关。取哪个CPU的值，masked by BPF_F_INDEX_MASK，如果指定BPF_F_CURRENT_CPU则取当前CPU的值。</p>
<p>在4.13之前，仅仅支持hardware perf event。</p>
<p>成功时返回计数器值，否则返回负数。</p>
<p>考虑使用<pre class="crayon-plain-tag">bpf_perf_event_read_value</pre>代替此函数。</p>
<div class="blog_h2"><span class="graybg">BPF Skeleton</span></div>
<p>一个BPF Application由1-N个BPF program、BPF Maps、全局变量组成。所有BPF program（各自对应一个ELF section）、用户空间（的控制）程序可以共享Map/全局变量。</p>
<div class="blog_h3"><span class="graybg">管理生命周期</span></div>
<p>BPF Application通常会经历以下生命周期阶段：</p>
<ol>
<li>Open：控制程序打开BPF object文件，解析了programs/map/global vars，但是尚未在内核中创建这些对象。打开BPF object文件后，控制程序可能进行一些调整，例如设置程序类型、设置全局变量的值，等等</li>
<li>Load：在此阶段，BPF Maps被创建，各种relocations被解析，BPF programs被载入内核并校验。这个阶段结束时，BPF程序的所有组件都被校验并且存在于内核，但是BPF program还不会被内核执行，可以保证在没有竞态条件的前提下，对BPF Map进行初始化</li>
<li>Attach：BPF programs被挂钩到相应的BPF挂钩点（例如tracepoint、kprobes、cgroup hooks、网络封包处理流水线…），BPF program开始工作，读写BPF Maps和全局变量</li>
<li>Teardown：BPF程序被detach并unload，BPF Map被销毁</li>
</ol>
<p>通过bpftool生成的BPF Skeleton，包含了触发上述阶段的函数：</p>
<ol>
<li><pre class="crayon-plain-tag">&lt;name&gt;__open()</pre>：创建并打开BPF Application</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__load()</pre>：实例化、加载、校验BPF Application组件</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__attach()</pre>：将BPF programs挂钩到内核的hook point。你也可以选择直接使用libbpf API进行细粒度控制</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__destroy()</pre>：销毁BPF programs并释放所有资源</li>
</ol>
<div class="blog_h3"><span class="graybg">访问全局变量</span></div>
<p>在内核空间，访问全局变量使用普通的C语法，你甚至可以取地址并将其传递给助手函数。</p>
<p>在控制程序中，你需要通过BPF skeleton来访问这些变量：</p>
<ol>
<li><pre class="crayon-plain-tag">skel-&gt;rodata</pre>，访问只读变量（常量）</li>
<li><pre class="crayon-plain-tag">skel-&gt;bss</pre>，访问可变的、以0初始化的变量</li>
<li><pre class="crayon-plain-tag">skel-&gt;data</pre>，访问可变的、非0初始化的变量</li>
</ol>
<p>在用户空间对这些变量的修改，会立刻反映到内核空间。</p>
<div class="blog_h2"><span class="graybg">BPF Ring Buffer</span></div>
<p>BPF ring buffer是一个新的BPF数据结构，它解决了BPF perf buffer（现有的给用户空间发送数据的事实标准工具）的内存效率、事件re-ordering问题。</p>
<p>ringbuf提供了兼容perfbuf的API，便于迁移。同时也提供了新的reserve/commit API，具有更好的易用性。</p>
<div class="blog_h3"><span class="graybg">对比perfbuf</span></div>
<p>当BPF程序将收集的信息发送到用户空间，供后续处理时，通常会利用perfbuf。perfbuf是per-CPU的环形缓冲区，允许内核和用户空间进行高效的数据交换。但是由于它的per-CPU设计，会导致两个缺点：内存低效、事件乱序。</p>
<p>因为这些缺点，内核5.8+开始，BPF提供了新的数据结构，BPF ring buffer。它是一个多生产者、单消费者（MPSC）的队列，可以安全的被多CPU环境共享。</p>
<p>ringbuf支持perfbuf的特性：</p>
<ol>
<li>可变长度数据记录</li>
<li>基于内存映射区域，高效的从用户空间读取数据的能力，不需要内存拷贝或者执行系统调用</li>
<li>支持epoll通知，或者忙循环（最小化延迟）</li>
</ol>
<div class="blog_h3"><span class="graybg">内存低效问题</span></div>
<p>perfbuf为每个CPU都分配了独立的缓冲区，开发者可能需要进行权衡：</p>
<ol>
<li>如果分配足够大的缓冲，则会浪费内存，特别是核心数很多的情况下</li>
<li>如果分配较小的缓冲，那么出现事件spike时，会丢失数据</li>
</ol>
<p>而ringbuf使用单个缓冲区，因而能够容易应对spike，同时不需要太大的内存消耗。</p>
<div class="blog_h3"><span class="graybg">事件乱序问题</span></div>
<p>在追踪相关事件（例如进程启动/退出、网络连接生命周期事件）的时候，事件顺序非常重要。</p>
<p>使用perfbuf时，如果两个相关事件在很短事件内（若干ms）被不同CPU处理，则可能发出到用户空间的顺序是错乱的。</p>
<div class="blog_h3"><span class="graybg">关于性能</span></div>
<p>在所有应用场景下，BPF ringbuf都有着和perfbuf可比较的性能。</p>
<p>唯一需要考虑使用perfbuf的场景是在NMI (non-maskable interrupt)上下文下运行的BPF程序，例如处理cpu-cycles之类的perf事件。由于ringbuf内部使用一个轻量的自旋锁，在NMI上下文下可能发生锁争用并导致reserve失败。</p>
<div class="blog_h3"><span class="graybg">用法对比</span></div>
<p>为了简化代码的迁移，ringbuf提供了一套类似于perfbuf的API，本节做一个对比。</p>
<p>下面的数据结构代表BPF程序需要收集的一个事件：</p>
<pre class="crayon-plain-tag">#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 512

struct event {
	int pid;
	char comm[TASK_COMM_LEN];
	char filename[MAX_FILENAME_LEN];
};</pre>
<p>perfbuf和ringbuf对应不同的Map类型：</p>
<pre class="crayon-plain-tag">/*** perfbuf ***/
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} pb SEC(".maps");

/*** ringbuf ***/
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    // 和perfbuf不同，ringbuf的尺寸可以在内核端定义
    // 但是，在用户空间也可以通过bpf_map__set_max_entries()指定或覆盖ringbuf的尺寸
    // 单位是字节，必须是内核页（一般都是4KB）大小的整数倍，并且是2的幂
    __uint(max_entries, 256 * 1024 /* 256 KB */);
} rb SEC(".maps");</pre>
<p>由于event超过512字节，因此不能直接在栈上分配，需要存储到BPF_MAP_TYPE_PERCPU_ARRAY：</p>
<pre class="crayon-plain-tag">struct {
	__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
	__uint(max_entries, 1);  // 只需要1个元素即可
	__type(key, int);
	__type(value, struct event);
} heap SEC(".maps");</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 挂钩到sched:sched_process_exec，每当成功的exec()系统调用后触发
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
	unsigned fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
	struct event *e;
	int zero = 0;
	// 获得事件的指针
	e = bpf_map_lookup_elem(&amp;heap, &amp;zero);
	if (!e) /* can't happen */
		return 0;

	// 填充字段
	e-&gt;pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
	bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);

	/*** 填充数据的API不同 ***/
	/*** perfbuf ***/
	// 输出追踪样本，要求在perfbuf中保留e大小的缓冲区，然后通知用户空间有数据可用
	bpf_perf_event_output(ctx, &amp;pb, BPF_F_CURRENT_CPU, e, sizeof(*e));
        /*** ringbuf ***/
	// 不需要传递ctx对象
	bpf_ringbuf_output(&amp;rb, e, sizeof(*e), 0);

	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
struct perfbuf_output_bpf *skel;

/*** 回调函数的签名不同 ***/
/*** perfbuf ***/
void handle_event(void *ctx, int cpu, void *data, unsigned int data_sz)
{
    const struct event *e = data;
    struct tm *tm;
    char ts[32];
    time_t t;
    time(&amp;t);
    tm = localtime(&amp;t);
    strftime(ts, sizeof(ts), "%H:%M:%S", tm);

    printf("%-8s %-5s %-7d %-16s %s\n", ts, "EXEC", e-&gt;pid, e-&gt;comm, e-&gt;filename);
}
/*** ringbuf ***/
int handle_event(void *ctx, void *data, size_t data_sz)
{
    // ...
}

/*** 创建用户空间缓冲区的API不同 ***/
/*** perfbuf ***/
pb_opts.sample_cb = handle_event; // 回调函数
//                    指向内核空间对应物，也就是那个Map
//                                                为每个CPU分配8个页，也就是32KB的缓冲区
pb = perf_buffer__new(bpf_map__fd(skel-&gt;maps.pb), 8 /* 32KB per CPU */, &amp;pb_opts);
if (libbpf_get_error(pb)) {
    err = -1;
    fprintf(stderr, "Failed to create perf buffer\n");
    goto cleanup;
}
/*** ringbuf ***/
rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
if (!rb) {
    err = -1;
    fprintf(stderr, "Failed to create ring buffer\n");
    goto cleanup;
}


// 开始epoll轮询
while (!exiting) {
    /*** 轮询接口不同 ***/
    /*** perfbuf ***/
    err = perf_buffer__poll(pb, 100 /* timeout, ms */);
    /*** ringbuf ***/
    err = ring_buffer__poll(rb, 100 /* timeout, ms */);
    
    // ...
}</pre>
<div class="blog_h3"><span class="graybg"> reserve/commit</span></div>
<p>上面的perfbuf兼容API，具有与perfbuf类似的缺点：</p>
<ol>
<li>额外的内存拷贝：你需要额外的内存空间来构建追踪样本对象，然后才能将它拷贝到缓冲区中</li>
<li>very late data reservation：构建（可能需要采集多种内存数据）追踪样本对象的工作可能是无意义的，如果缓冲区中剩余空间（由于用户空间程序处理缓慢或者事件burst）不足，构建的对象无处存放。使用xxx_output()接口不能提前感知缓冲区剩余空间，从而避免不必要的样本对象构造</li>
</ol>
<p>ringbuf提供了一套新的API，应对上述缺点。</p>
<ol>
<li><pre class="crayon-plain-tag">bpf_ringbuf_reserve()</pre>：尝试在缓冲区中预定空间，这个操作可以在构建样本对象之前就进行，尽早获取空间不足的状况。如果预定成功，则返回指针，并且可以保证后续可以将数据commit到其中；如果预定失败，则返回NULL，我们可以跳过后续的、无意义的操作</li>
<li><pre class="crayon-plain-tag">bpf_ringbuf_commit()</pre>：将样本对象发送到缓冲区</li>
</ol>
<p>此外，reserve的空间，在commit之前，对于用户空间是不可见的。因此你可以直接用它来构造样本对象，不用担心半初始化的对象被看到，同时达到节约内存的目的。</p>
<p>唯一的限制是：reserve的空间大小，必须能够被BPF verifier感知（常量）。如果样本尺寸是动态的，则只能使用<pre class="crayon-plain-tag">bpf_ringbuf_output()</pre>并且承受内存拷贝的代价。</p>
<p>用法示例：</p>
<pre class="crayon-plain-tag">// 不再需要到per-CPU array中预先构造对象：
//   e = bpf_map_lookup_elem(&amp;heap, &amp;zero);
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);

// 不再需要将per-CPU array中的对象拷贝到缓冲区
//   bpf_ringbuf_output(&amp;rb, e, sizeof(*e), 0);
bpf_ringbuf_submit(e, 0);</pre>
<div class="blog_h3"><span class="graybg">通知用户空间</span></div>
<p>不管是perfbuf还是ringbuf，如果内核每写入一个样本，就通知（也就是唤醒在poll/epoll上等待的）用户空间程序，其开销是相当大的。</p>
<p>perfbuf的解决办法是，允许用户空间程序构造perfbuf时，指定每发生N个样本才唤醒一次，这可能会让你丢失最多N-1个样本。</p>
<p>ringbuf则允许为<pre class="crayon-plain-tag">bpf_ringbuf_output()</pre>或<pre class="crayon-plain-tag">bpf_ringbuf_commit()</pre>指定额外的标记：</p>
<ol>
<li>BPF_RB_FORCE_WAKEUP 本次写入样本，强制唤醒用户空间程序</li>
<li>BPF_RB_NO_WAKEUP本次写入样本，不会唤醒用户空间程序</li>
</ol>
<p>如果不指定上述标记，ringbuf会根据用户空间程序lagging的情况决定是否唤醒。不指定标记通常是安全的默认值。</p>
<div class="blog_h2"><span class="graybg">调试BPF程序</span></div>
<p>BPF没有常规的调试工具，支持设置断点、探查变量、单步跟踪的那种。</p>
<div class="blog_h3"><span class="graybg">bpf_printk</span></div>
<p>使用<pre class="crayon-plain-tag">bpf_printk(fmt, args...)</pre>可以打印一些信息到<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace_pipe</pre>，来帮助你理解发生了什么，这个函数最多支持3个args。</p>
<p>这个函数的成本很高，不能在生产环境使用。</p>
<div class="blog_h3"><span class="graybg">bpf_trace_printk</span></div>
<p>助手函数<pre class="crayon-plain-tag">long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...)</pre>是bpf_printk的wrapper。</p>
<div class="blog_h2"><span class="graybg">libbpf-bootstrap</span></div>
<p>这是libbpf提供的脚手架，可以让你快速开始编写自己的eBPF程序。该脚手架目前提供一些demo程序：</p>
<ol>
<li>minimal：最小化的、能编译、加载、运行的BPF hello world程序</li>
<li>bootstrap：现实可用的、可移植的、全功能的BPF程序，依赖BPF CO-RE。需要当前使用的内核开启<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>，该demo还示例了5.5+支持的BPF全局变量、5.8+支持的BPF ring buffer</li>
</ol>
<p>libbpf-bootstrap自带了libbpf（作为git子模块）和bpftool（仅x84），避免依赖你的Linux发行版所携带的特定版本。libbpf依赖的库包括libelf、zlib，需要确保已经安装。</p>
<p>执行下面的命令获得脚手架：</p>
<pre class="crayon-plain-tag">git clone https://github.com/libbpf/libbpf-bootstrap.git
git submodule init</pre>
<p>目录结构如下：</p>
<pre class="crayon-plain-tag">.
├── examples
│   ├── c  # C代码示例
│   │   ├── bootstrap.bpf.c  # 在内核空间执行的BPF程序
│   │   ├── bootstrap.c      # 在用户空间执行的逻辑，负责加载BPF字节码，在程序生命周期内和BPF程序交互
│   │   ├── bootstrap.h      # 用户/内核空间程序共享的头
│   │   ├── CMakeLists.txt
│   │   ├── fentry.bpf.c
│   │   ├── fentry.c
│   │   ├── kprobe.bpf.c
│   │   ├── kprobe.c
│   │   ├── Makefile
│   │   ├── minimal.bpf.c
│   │   ├── minimal.c
│   │   ├── uprobe.bpf.c
│   │   ├── uprobe.c
│   │   └── xmake.lua
│   └── rust # Rust代码示例
│       ├── Cargo.lock
│       ├── Cargo.toml
│       └── xdp
│           ├── build.rs
│           ├── Cargo.lock
│           ├── Cargo.toml
│           └── src
│               ├── bpf
│               │   ├── vmlinux.h -&gt; ../../../../../vmlinux/vmlinux.h
│               │   └── xdppass.bpf.c
│               └── main.rs
├── libbpf # 自带的libbpf
├── LICENSE
├── README.md
├── tools  # 自带bpftool，用于为你的BPF代码构建BPF skeleton、生成vmlinux.h头
│   ├── bpftool
│   ├── cmake
│   │   ├── FindBpfObject.cmake
│   │   └── FindLibBpf.cmake
│   └── gen_vmlinux_h.sh  # 用于生成自定义的vmlinux.h
└── vmlinux
    ├── vmlinux_508.h
    └── vmlinux.h -&gt; vmlinux_508.h  # 预生成的vmlinux.h</pre>
<div class="blog_h3"><span class="graybg">minimal</span></div>
<p>最小化的样例，不依赖BPF CO-RE，可以在很老版本的内核上运行。该程序会安装一个每秒触发一次的tracepoint handler，它使用<pre class="crayon-plain-tag">bpf_printk()</pre>和外部通信，你可以通过/sys/kernel/debug/tracing/trace_pipe查看输出。</p>
<p>构建此样例：</p>
<pre class="crayon-plain-tag">cd examples/c
make minimal</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 包含基本的BPF相关的类型和常量。为了使用内核空间BPF API，例如助手函数的flags，需要引入此头
#include &lt;linux/bpf.h&gt;
// 由libbpf提供，包含了大部分常用宏、常量、助手函数，几乎每个BPF程序都会使用
#include &lt;bpf/bpf_helpers.h&gt;
// 这个变量定义你的代码的License，内核强制要求此字段存在，某些功能对于非GPL兼容的License不可用
//             必须定义在license段
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 全局变量，要求内核版本5.5+，全局变量甚至可以从用户空间读写
// 可以用于配置BPF程序、存放轻量的统计数据、在内核和用户空间传递数据
int my_pid = 0;

// 这里定义了BPF内核程序
// 这个注解，说明了需要创建的BPF程序类型，以及如何挂钩到内核
//   tracepoint BPF程序
//      在进入write系统调用时触发
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
	// 调用助手函数，获得PID（内核的术语叫TGID）。为了节约空间，很多助手函数使用高低字节存储不同的数据
	int pid = bpf_get_current_pid_tgid() &gt;&gt; 32;

	// 判断是否是关注进程发出的系统调用
	if (pid != my_pid)
		return 0;
	// 如果是，打印信息到/sys/kernel/debug/tracing/trace_pipe
	// 注意，由于性能问题，这个函数不能用于生产环境
	bpf_printk("BPF triggered from PID %d.\n", pid);

	return 0;
}

// 你还可以定义更多的BPF程序，只需要为它们声明适当的SEC即可。所有这些程序共享全局变量</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
// 由bpftool自动生成的，映射了minimal.bpf.c的高层结构的BPF skeleton
// 编译后的BPF object被内嵌到此头文件（也就是用户空间代码）中，简化了开发和部署
// 文件路径 .output/&lt;app&gt;.skel.h
#include "minimal.skel.h"

// 此回调打印libbpf日志到控制台
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	return vfprintf(stderr, format, args);
}

static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur	= RLIM_INFINITY,
		.rlim_max	= RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &amp;rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

int main(int argc, char **argv)
{
	struct minimal_bpf *skel;
	int err;

	// 为所有libbpf日志设置回调函数
	libbpf_set_print(libbpf_print_fn);

	// 增大内核内部的per-user内存限制，允许BPF子系统为程序、Map分配足够的资源
	bump_memlock_rlimit();

	// 打开BPF skeleton
	skel = minimal_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	// 访问BSS段中的全局变量
	skel-&gt;bss-&gt;my_pid = getpid();

	// 加载和校验BPF程序
	err = minimal_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	// 挂钩BPF程序，在此即注册tracepoint handler
	// libbpf能够根据SEC注解，自动的为大部分BPF程序类型（tracepoints, kprobes等）选择适当的挂钩点
	// 如果不能满足需求，可以调用libbpf提供的函数手工挂钩的API
	err = minimal_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	// 触发write系统调用，进而触发BPF程序
	for (;;) {
		/* trigger our BPF program */
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	// 清除所有内核/用户空间的资源
	// 在大部分情况下，即使程序崩溃，没有清理，内核也会作自动清理
	minimal_bpf__destroy(skel);
	return -err;
}</pre>
<p>构建此程序的Makefile：</p>
<pre class="crayon-plain-tag"># SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
OUTPUT := .output
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPFTOOL ?= $(abspath ../../tools/bpftool)
LIBBPF_SRC := $(abspath ../../libbpf/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
# vmlinux.h由bpftool从当前运行的内核中抽取出，包含了所有内核使用的类型定义
VMLINUX := ../../vmlinux/vmlinux.h
# 使用自己的libbpf API头文件、Linux UAPI头文件，避免依赖当前系统的（可能缺失或过期的）头文件
INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX))
# 使用-g保留调试信息
CFLAGS := -g -Wall
# 体系结构信息会传递给后续的BPF构建步骤，使用bpf_tracing.h中低级别tracing助手函数需要此信息
ARCH := $(shell uname -m | sed 's/x86_64/x86/')

# 这个示例项目包含多个demo，每个对应一个APP
APPS = minimal bootstrap uprobe kprobe fentry

# 获取Clang在当前系统上默认的includes目录，当以-target bpf编译时，这些目录被显式添加到
# include列表。如果不这样做，某些体系结构/发行版下，体系结构特定的目录可能missing，诸如
# asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h之类
# 的头文件可能missing
CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - &lt;/dev/null 2&gt;&amp;1 \
	| sed -n '/&lt;...&gt; search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')

ifeq ($(V),1)
	Q =
	msg =
else
	Q = @
	msg = @printf '  %-8s %s%s\n'					\
		      "$(1)"						\
		      "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))"	\
		      "$(if $(3), $(3))";
	MAKEFLAGS += --no-print-directory
endif

.PHONY: all
all: $(APPS)

.PHONY: clean
clean:
	$(call msg,CLEAN)
	$(Q)rm -rf $(OUTPUT) $(APPS)

$(OUTPUT) $(OUTPUT)/libbpf:
	$(call msg,MKDIR,$@)
	$(Q)mkdir -p $@

# 1. 构建 libbpf 为静态库，存放在.output目录下
#    如果希望和系统的libbpf共享库链接，去掉这个目标
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
	$(call msg,LIB,$@)
	$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1		      \
		    OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@)		      \
		    INCLUDEDIR= LIBDIR= UAPIDIR=			      \
		    install

# 2. 构建BPF object
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT)
	$(call msg,BPF,$@)
	# 必须使用-g -O2     目标必须是bpf 为bpf_tracing.h定义必要的宏
	# 
	$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@
	$(Q)$(LLVM_STRIP) -g $@ # 去除DWARF信息，从来不会用到。由于BPF程序最终以文本形式嵌入到BPF skeleton，因此要尽量精简

# 3. 生成BPF skeletons, 依赖2
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
	$(call msg,GEN-SKEL,$@)
	$(Q)$(BPFTOOL) gen skeleton $&lt; &gt; $@

# 4. 构建用户空间程序object，依赖3
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h

$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
	$(call msg,CC,$@)
	$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@

# 5. 构建用户空间程序
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
	$(call msg,BINARY,$@)
	$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@

# delete failed targets
.DELETE_ON_ERROR:

# keep intermediate (.skel.h, .bpf.o, etc) targets
.SECONDARY:</pre>
<div class="blog_h3"><span class="graybg">bootstrap</span></div>
<p>minimal是最小化的BPF程序实例，在现代Linux环境下开发BPF程序，bootstrap可以作为一个不错的开始点。它实现的功能包括：</p>
<ol>
<li>命令行参数解析</li>
<li>信号处理</li>
<li>多个BPF程序之间的交互 —— 通过Map共享状态</li>
<li>使用BPF ring buffer来发送数据到用户空间</li>
<li>用于行为参数化的全局常量，以及如何通过修改段数据初始化常量值</li>
<li>使用BPF CO-RE和vmlinux.h来读取内核<pre class="crayon-plain-tag">struct task_struct</pre>暴露的进程的额外信息</li>
</ol>
<p>该程序依赖BPF CO-RE，需要内核配置<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>。</p>
<p>公共头文件：</p>
<pre class="crayon-plain-tag">#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
	int pid;
	int ppid;
	unsigned exit_code;
	unsigned long long duration_ns;
	char comm[TASK_COMM_LEN];
	char filename[MAX_FILENAME_LEN];
	bool exit_event;
};

#endif /* __BOOTSTRAP_H */</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 该头文件包含内核的所有数据类型，通过gen_vmlinux_h.sh自动生成

// 其中的类型应用了__attribute__((preserve_access_index))注解，可以让Clang
// 生成BPF CO-RE relocations，这让libbpf能够自动的将BPF代码是配到宿主机内核的内存布局，即使
// 该布局和构建BPF程序时候的主机不一致
// BPF CO-RE relocations是创建pre-compiled、可移植的BPF程序的关键，它不需要在目标及其上部署
// Clang/LLVM工具链。一个被选的技术是BCC的运行时编译，这种技术有多个缺点

// 需要注意，vmlinux.h不能和其它系统级的头文件联合适用，会导致类型重定义和冲突
// 因此开发BPF程序时，仅可使用vmlinux.h、libbpf提供的头、你自己定义的头
#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
// 下面两个头文件，用于编写基于BPF CO-RE的追踪应用程序
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
// 用于/内核空间共享头
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 定义一个哈希表类型的Map
struct {
	// 这几个__开头的是宏bpf_helpers.h中定义的宏，用来定义结构体字段
	// 定义一个BPF_MAP_TYPE_HASH类型的，最大条目8192的，key类型为pid_t，值类型为u64的Map
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 8192);
	__type(key, pid_t);
	__type(value, u64);
} exec_start SEC(".maps");
//           为了让libbpf知道它需要创建BPF Map，必须添加此注解

// 定义一个BPF ring buffer类型的Map
// 用于向用户空间发送数据，本样例使用bpf_ringbuf_reserve()/bpf_ringbuf_submit()来实现最高
// 的易用性和性能
struct {
	__uint(type, BPF_MAP_TYPE_RINGBUF);
	__uint(max_entries, 256 * 1024);
} rb SEC(".maps");

// 全局常量，对于用户/内核空间均不可变。在BPF程序校验期间，此常量值已知，BPF Verifier可能依据值来裁减
// 某些dead code
// volatile是必须的，可以防止Clang将变量优化掉（直接没了）
const volatile unsigned long long min_duration_ns = 0;

// 本样例由两个BPF程序组成，这个监控exec系统调用
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
	struct task_struct *task;
	unsigned fname_off;
	struct event *e;
	pid_t pid;
	u64 ts;

	// 得到当前PID
	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	// 获取当前内核时间
	ts = bpf_ktime_get_ns();
	// 记录当前进程的创建时间
	bpf_map_update_elem(&amp;exec_start, &amp;pid, &amp;ts, BPF_ANY);

	/* don't emit exec events when minimum duration is specified */
	if (min_duration_ns)
		return 0;

	// 在ring buffer中保留event长度的空间，返回得到的空间指针
	e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
	if (!e)
		return 0;

	// 得到当前Task对象
	task = (struct task_struct *)bpf_get_current_task();
	// 并填充event
	e-&gt;exit_event = false;
	e-&gt;pid = pid;
	// BPF CO-RE读取
	// 相当于e-&gt;ppid = task-&gt;real_parent-&gt;tgid;
	// 但是对于BPF程序来说，BPF verifier需要额外的检查，因为存在读取任意内核内存的可能
	// BPF_CORE_READ()通过简洁的方式完成此过程，并且记录必要的BPF CO-RE relocations，从而
	// 允许libbpf根据宿主机的内存布局，来调整字段的偏移量
	e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
	// 读取当前任务的可执行文件名（不包含路径）
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));

	// 获取新进程文件名在ctx的偏移量            移除u32高16位
	fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
	// 从ctx读取文件名，存入e
	bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);

	// 提交给用户空间供处理
	bpf_ringbuf_submit(e, 0);
	return 0;
}

// 本样例由两个BPF程序组成，这个监控进程退出，即exit系统调用
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
	struct task_struct *task;
	struct event *e;
	pid_t pid, tid;
	u64 id, ts, *start_ts, duration_ns = 0;
	
	// 获取正在退出的进程的PID
	id = bpf_get_current_pid_tgid();
	// 高32位是pid，低32位是线程ID
	pid = id &gt;&gt; 32;
	tid = (u32)id;

	// 如果pid和tid不同，则意味着是非主线程退出，忽略
	if (pid != tid)
		return 0;

	// 查找进程开始时间，并计算进程持续时间
	start_ts = bpf_map_lookup_elem(&amp;exec_start, &amp;pid);
	if (start_ts)
		duration_ns = bpf_ktime_get_ns() - *start_ts;
	else if (min_duration_ns)
		return 0;
	// 从Map删除条目
	bpf_map_delete_elem(&amp;exec_start, &amp;pid);

	// 忽略持续时间过短的进程
	if (min_duration_ns &amp;&amp; duration_ns &lt; min_duration_ns)
		return 0;

	// 从ring buffer分配空间
	e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
	if (!e)
		return 0;

	// 填充并提交给用户空间处理
	task = (struct task_struct *)bpf_get_current_task();

	e-&gt;exit_event = true;
	e-&gt;duration_ns = duration_ns;
	e-&gt;pid = pid;
	e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
	e-&gt;exit_code = (BPF_CORE_READ(task, exit_code) &gt;&gt; 8) &amp; 0xff;
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));

	/* send data to user-space for post-processing */
	bpf_ringbuf_submit(e, 0);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;argp.h&gt;
#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "bootstrap.h"
#include "bootstrap.skel.h"

// 使用libc的argp做命令行参数解析
static struct env {
	bool verbose;
	long min_duration_ms;
} env;

const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "&lt;bpf@vger.kernel.org&gt;";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d &lt;min-duration-ms&gt;] [-v]\n";

static const struct argp_option opts[] = {
	{ "verbose", 'v', NULL, 0, "Verbose debug output" },
	{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
	{},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
	switch (key) {
	case 'v':
		env.verbose = true;
		break;
	case 'd':
		errno = 0;
		env.min_duration_ms = strtol(arg, NULL, 10);
		if (errno || env.min_duration_ms &lt;= 0) {
			fprintf(stderr, "Invalid duration: %s\n", arg);
			argp_usage(state);
		}
		break;
	case ARGP_KEY_ARG:
		argp_usage(state);
		break;
	default:
		return ARGP_ERR_UNKNOWN;
	}
	return 0;
}

static const struct argp argp = {
	.options = opts,
	.parser = parse_arg,
	.doc = argp_program_doc,
};

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	if (level == LIBBPF_DEBUG &amp;&amp; !env.verbose)
		return 0;
	return vfprintf(stderr, format, args);
}

static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur	= RLIM_INFINITY,
		.rlim_max	= RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &amp;rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

static volatile bool exiting = false;

static void sig_handler(int sig)
{
	exiting = true;
}

static int handle_event(void *ctx, void *data, size_t data_sz)
{
	// 直接转换为event
	const struct event *e = data;
	struct tm *tm;
	char ts[32];
	time_t t;

	time(&amp;t);
	tm = localtime(&amp;t);
	strftime(ts, sizeof(ts), "%H:%M:%S", tm);

	if (e-&gt;exit_event) {
		printf("%-8s %-5s %-16s %-7d %-7d [%u]", ts, "EXIT", e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;exit_code);
		if (e-&gt;duration_ns) printf(" (%llums)", e-&gt;duration_ns / 1000000);
		printf("\n");
	} else {
		printf("%-8s %-5s %-16s %-7d %-7d %s\n",  ts, "EXEC", e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;filename);
	}
	return 0;
}

int main(int argc, char **argv)
{
	struct ring_buffer *rb = NULL;
	struct bootstrap_bpf *skel;
	int err;

	// 解析命令行参数
	err = argp_parse(&amp;argp, argc, argv, 0, NULL, NULL);
	if (err)
		return err;

	// 日追回调函数
	libbpf_set_print(libbpf_print_fn);

	// 设置RLIMIT_MEMLOCK
	bump_memlock_rlimit();

	// 去除默认Ctrl-C处理逻辑
	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

	// 由于有一个常量需要设置值，因此这里没有使用bootstrap_bpf__open_and_load()
	// 而是先open
	skel = bootstrap_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open and load BPF skeleton\n");
		return 1;
	}

	// 然后设置常量值
	skel-&gt;rodata-&gt;min_duration_ns = env.min_duration_ms * 1000000ULL;

	// 最后load，加载之后，用户空间只能读取，不能修改 skel-&gt;rodata-&gt;min_duration_ns
	err = bootstrap_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	// 挂钩BPF程序到内核
	err = bootstrap_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	// 创建ring buffer轮询
	// 映射位ring_buffer类型   内核空间对应物              轮询到数据后的回调
	rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
	if (!rb) {
		err = -1;
		fprintf(stderr, "Failed to create ring buffer\n");
		goto cleanup;
	}

	// 打印表头
	printf("%-8s %-5s %-16s %-7s %-7s %s\n",
	       "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
	// 接收到信号前，反复轮询
	while (!exiting) {
		err = ring_buffer__poll(rb, 100 /* timeout, ms */);
		/* Ctrl-C will cause -EINTR */
		if (err == -EINTR) {
			err = 0;
			break;
		}
		if (err &lt; 0) {
			printf("Error polling perf buffer: %d\n", err);
			break;
		}
	}

cleanup:
	// 需要清理ring buffer和BPF
	ring_buffer__free(rb);
	bootstrap_bpf__destroy(skel);

	return err &lt; 0 ? -err : 0;
}</pre>
<div class="blog_h3"><span class="graybg">uprobe</span></div>
<p>上面两个例子都是tracepoint BPF程序。tracepoint是内核中比较稳定的追踪机制，属于静态instrument，它由一系列预定义在内核源码中的挂钩点组成。</p>
<p>kprobe/uprobe则属于动态instrument，在运行时动态的进行instrument，可以对任何函数进行调试追踪。例如在函数的入口、出口地址、或者某一行值入代码，执行到这些代码的时候，你就可以获得上下文信息，例如当前函数的名字、参数、返回值、寄存器甚至全局数据结构信息。</p>
<p>kprobe/uprobe很强大，但是依赖于程序/内核的特定版本，因为函数的签名可能改变，函数也可能被删除。</p>
<p>libbpf-bootstrap提供了一个uprobe样例，此样例能够处理用户空间的entry/exit(return)探针。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;
#include &lt;linux/ptrace.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 函数entry探针
SEC("uprobe/func")
// 这里是调用一个宏，在函数体中可以使用参数 struct pt_regs *ctx
//                     逐个列出函数的参数
int BPF_KPROBE(uprobe, int a, int b)
{
	bpf_printk("UPROBE ENTRY: a = %d, b = %d\n", a, b);
	return 0;
}

// 函数exit探针
SEC("uretprobe/func")
//                           列出函数的返回值
int BPF_KRETPROBE(uretprobe, int ret)
{
	bpf_printk("UPROBE EXIT: return = %d\n", ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;errno.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "uprobe.skel.h"

//...

/* 通过/proc/self/maps查找进程的base load address, 寻找第一个可执行的（r-xp）内存映射：
 * 绝对起始地址                    此区域的偏移量
 * 5574fd254000-5574fd258000 r-xp 00002000 fd:01 668759                     /usr/bin/cat
 * ^^^^^^^^^^^^                   ^^^^^^^^
 * 绝对起始地址 - 此区域的偏移量 即得到进程的base load address
 */
static long get_base_addr() {
	size_t start, offset;
	char buf[256];
	FILE *f;

	f = fopen("/proc/self/maps", "r");
	if (!f)
		return -errno;

	while (fscanf(f, "%zx-%*x %s %zx %*[^\n]\n", &amp;start, buf, &amp;offset) == 3) {
		if (strcmp(buf, "r-xp") == 0) {
			fclose(f);
			return start - offset;
		}
	}

	fclose(f);
	return -1;
}

// 这个是被追踪的函数
int uprobed_function(int a, int b)
{
	return a + b;
}

int main(int argc, char **argv)
{
	struct uprobe_bpf *skel;
	long base_addr, uprobe_offset;
	int err, i;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	base_addr = get_base_addr();
	if (base_addr &lt; 0) {
		fprintf(stderr, "Failed to determine process's load address\n");
		err = base_addr;
		goto cleanup;
	}

	// uprobe/uretprobe期望获知被追踪函数的相对偏移量，这个偏移量是相对于process的base load address的
	// 这里直接得到目标函数的绝对地址，然后减去上面我们得到的base load address，即可得到偏移量
	//
	// 通常情况下，被追踪的函数不会在当前程序中，这可能需要解析其所在程序的ELF，
	// 函数相对base load address的偏移量 = .text段的偏移量 + 函数在.text段内的偏移量
	//
	//              得到函数的绝对地址
	uprobe_offset = (long)&amp;uprobed_function - base_addr;

	//                   挂钩
	skel-&gt;links.uprobe = bpf_program__attach_uprobe(skel-&gt;progs.uprobe,
							false /* entry钩子 */,
							0 /* 到当前进程 */,
							"/proc/self/exe", // 当前进程的二进制文件路径
							uprobe_offset); // 追踪的目标函数的偏移量
	err = libbpf_get_error(skel-&gt;links.uprobe);
	if (err) {
		fprintf(stderr, "Failed to attach uprobe: %d\n", err);
		goto cleanup;
	}

	// 这里示例了如何挂钩uprobe/uretprobe到任何现存的/未来创建的、使用同一二进制文件的进程
	skel-&gt;links.uretprobe = bpf_program__attach_uprobe(skel-&gt;progs.uretprobe,
							   true /* exit钩子 */,
							   -1 /* 任何进程 */,
							   "/proc/self/exe",
							   uprobe_offset);
	err = libbpf_get_error(skel-&gt;links.uretprobe);
	if (err) {
		fprintf(stderr, "Failed to attach uprobe: %d\n", err);
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	for (i = 0; ; i++) {
		// 调用目标函数，触发BPF程序
		fprintf(stderr, ".");
		uprobed_function(i, i + 1);
		sleep(1);
	}

cleanup:
	uprobe_bpf__destroy(skel);
	return -err;
}</pre>
<p>运行此程序，就可以看到函数uprobed_function每次调用的参数、返回值了。</p>
<div class="blog_h3"><span class="graybg">kprobe</span></div>
<p>kprobe和uprobe工作方式很类似，只是挂钩的是内核函数。</p>
<p>libbpf-bootstrap提供了一个kprobe样例，该样例挂钩到do_unlinkat()函数，并且打印PID、文件名、返回值等信息。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 注解里必须包含目标内核函数的名字
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
	pid_t pid;
	const char *filename;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	// 必须使用此助手函数访问指针参数，fentry则不必
	filename = BPF_CORE_READ(name, name);
	bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
	return 0;
}

SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "kprobe.skel.h"

// ...

static volatile sig_atomic_t stop;

static void sig_int(int signo)
{
	stop = 1;
}

int main(int argc, char **argv)
{
	struct kprobe_bpf *skel;
	int err;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	// 挂钩处理比起uprobe要简单的多，不需要计算目标函数的地址
	err = kprobe_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	if (signal(SIGINT, sig_int) == SIG_ERR) {
		fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	while (!stop) {
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	kprobe_bpf__destroy(skel);
	return -err;
}</pre>
<div class="blog_h3"><span class="graybg">fentry</span></div>
<p>内核5.5引入了BPF trampoline，可以让内核和BPF程序更快速的相互调用。fentry/fexit是它的一个用例，fentry/fexit基本上等价于kprobe/kretprobe，但是调用BPF程序不会引起额外的overhead。</p>
<p>在XDP开发中BPF trampoline能很大程度上改善BPF相关的网络troubleshooting的体验。BPF trampoline支持将fentry/fexit BPF程序挂钩到任何网络BPF程序上，从而可以看到任何XDP/TC/lwt/cgroup程序的输入输出封包，同时不会产生干扰。</p>
<p>libbpf-bootstrap提供了一个fentry样例，类似于kprobe样例，它也是挂钩到do_unlinkat()函数，从而在文件被删除后触发BPF程序，记录各种信息。</p>
<p>fentry/fexit提升了性能的同时，也改善了开发体验。<span style="background-color: #c0c0c0;">访问指针参数时</span>，不再需要调用助手函数，<span style="background-color: #c0c0c0;">可以直接解引用</span>。<span style="background-color: #c0c0c0;">fexit</span>和kretprobe比起来，<span style="background-color: #c0c0c0;">可以同时返回输入参数和返回值</span>，后者只能访问返回值。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 注解里必须包含目标内核函数的名字
SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("fentry: pid = %d, filename = %s\n", pid, name-&gt;name);
	return 0;
}

SEC("fexit/do_unlinkat")
//                             可以同时访问入参和返回值
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name-&gt;name, ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "fentry.skel.h"

static volatile sig_atomic_t stop;

int main(int argc, char **argv)
{
	struct fentry_bpf *skel;
	int err;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	// 挂钩
	err = fentry_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	while (!stop) {
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	fentry_bpf__destroy(skel);
	return -err;
}</pre>
<div class="blog_h2"><span class="graybg">内核样例程序</span></div>
<p>内核在samples/bpf/下包含了若干eBPF程序示例。这些程序都包含两部分：</p>
<ol>
<li><pre class="crayon-plain-tag">*_kern.c</pre>会编译为BPF内核程序</li>
<li><pre class="crayon-plain-tag">*_user.c</pre>为用户空间程序，加载上述内核程序</li>
</ol>
<p>这些样例没有封装，直接使用libbpf提供的函数完成各种功能，包括助手函数调用、ELF加载、BPF挂钩、Map创建和读写。</p>
<p>执行下面的命令构建示例：</p>
<pre class="crayon-plain-tag"># 可能需要进行必要的清理
make -C tools clean
make -C samples/bpf clean
make clean

# 当前正在构建版本的内核头文件，复制到usr/include子目录。构建samples会优先使用这些头文件
make headers_install

# 构建BPF样例
make M=samples/bpf</pre>
<div class="blog_h3"><span class="graybg">tracex4</span></div>
<p>eBPF程序代码：</p>
<pre class="crayon-plain-tag">#include &lt;linux/ptrace.h&gt;
#include &lt;linux/version.h&gt;
#include &lt;uapi/linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

struct pair {
	u64 val;
	u64 ip;
};

// 定义一个Map
struct {
    // eBPF提供多种Map，BPF_MAP_TYPE_HASH是其中之一
	__uint(type, BPF_MAP_TYPE_HASH);
	// 键值的类型
	__type(key, long);
	__type(value, struct pair);
	// 容量
	__uint(max_entries, 1000000);
} my_map SEC(".maps");
        // SEC宏用于在二进制文件中创建一个新的段

// 这个段实际上声明了挂钩点
SEC("kprobe/kmem_cache_free")
// 从Map中删除一个键值
int bpf_prog1(struct pt_regs *ctx)
{
	long ptr = PT_REGS_PARM2(ctx);

	bpf_map_delete_elem(&amp;my_map, &amp;ptr);
	return 0;
}

// 这个段实际上声明了挂钩点
SEC("kretprobe/kmem_cache_alloc_node")
// 添加一个键值到Map
int bpf_prog2(struct pt_regs *ctx)
{
	long ptr = PT_REGS_RC(ctx);
	long ip = 0;

	/* get ip address of kmem_cache_alloc_node() caller */
	BPF_KRETPROBE_READ_RET_IP(ip, ctx);

	struct pair v = {
		.val = bpf_ktime_get_ns(),
		.ip = ip,
	};

	bpf_map_update_elem(&amp;my_map, &amp;ptr, &amp;v, BPF_ANY);
	return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;</pre>
<p>用于空间程序源码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;signal.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdbool.h&gt;
#include &lt;string.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/resource.h&gt;

#include &lt;bpf/bpf.h&gt;
#include &lt;bpf/libbpf.h&gt;

struct pair {
	long long val;
	__u64 ip;
};

static __u64 time_get_ns(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &amp;ts);
	return ts.tv_sec * 1000000000ull + ts.tv_nsec;
}

static void print_old_objects(int fd)
{
	long long val = time_get_ns();
	__u64 key, next_key;
	struct pair v;

	key = write(1, "\e[1;1H\e[2J", 12); /* clear screen */

	key = -1;
	while (bpf_map_get_next_key(fd, &amp;key, &amp;next_key) == 0) {
		bpf_map_lookup_elem(fd, &amp;next_key, &amp;v);
		key = next_key;
		if (val - v.val &lt; 1000000000ll)
			/* object was allocated more then 1 sec ago */
			continue;
		printf("obj 0x%llx is %2lldsec old was allocated at ip %llx\n",
		       next_key, (val - v.val) / 1000000000ll, v.ip);
	}
}

int main(int ac, char **argv)
{
	struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
	struct bpf_link *links[2];
	struct bpf_program *prog;
	struct bpf_object *obj;
	char filename[256];
	int map_fd, i, j = 0;

	if (setrlimit(RLIMIT_MEMLOCK, &amp;r)) {
		perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
		return 1;
	}

	snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
	// 打开编译好的eBPF程序，tracex4_kern.o
	obj = bpf_object__open_file(filename, NULL);
	if (libbpf_get_error(obj)) {
		fprintf(stderr, "ERROR: opening BPF object file failed\n");
		return 0;
	}

	// 载入eBPF程序
	// 调用之后，定义在eBPF中的探针被添加到/sys/kernel/debug/tracing/kprobe_events
	//   cat /sys/kernel/debug/tracing/kprobe_events
	//   p:kprobes/kmem_cache_free kmem_cache_free
	//   r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node
	if (bpf_object__load(obj)) {
		fprintf(stderr, "ERROR: loading BPF object file failed\n");
		goto cleanup;
	}

	// 找到共享的Map
	map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
	if (map_fd &lt; 0) {
		fprintf(stderr, "ERROR: finding a map in obj file failed\n");
		goto cleanup;
	}

	// 执行挂钩
	bpf_object__for_each_program(prog, obj) {
		links[j] = bpf_program__attach(prog);
		if (libbpf_get_error(links[j])) {
			fprintf(stderr, "ERROR: bpf_program__attach failed\n");
			links[j] = NULL;
			// 如果出错，解除所有已经注册的钩子
			goto cleanup;
		}
		j++;
	}

	// 读取Map并打印
	for (i = 0; ; i++) {
		print_old_objects(map_fd);
		sleep(1);
	}

cleanup:
	for (j--; j &gt;= 0; j--)
		bpf_link__destroy(links[j]);

	bpf_object__close(obj);
	return 0;
}</pre>
<div class="blog_h2"><span class="graybg">XDP样例程序</span></div>
<div class="blog_h3"><span class="graybg">过滤IPv6</span></div>
<p>这个简单的、可以和iproute2一起工作的XDP样例，判断封包是否是IPv6的，如果是，则丢弃：</p>
<pre class="crayon-plain-tag">#define KBUILD_MODNAME "xdp_ipv6_filter"
#include &lt;uapi/linux/bpf.h&gt;
#include &lt;uapi/linux/if_ether.h&gt;
#include &lt;uapi/linux/if_packet.h&gt;
#include &lt;uapi/linux/if_vlan.h&gt;
#include &lt;uapi/linux/ip.h&gt;
#include &lt;uapi/linux/in.h&gt;
#include &lt;uapi/linux/tcp.h&gt;
#include "bpf_helpers.h"

#define DEBUG 1

#ifdef  DEBUG
/* Only use this for debug output. Notice output from  bpf_trace_printk()
 * end-up in /sys/kernel/debug/tracing/trace_pipe
 */
#define bpf_debug(fmt, ...)                     \
        ({                          \
            char ____fmt[] = fmt;               \
            // 这个函数打印消息到 /sys/kernel/debug/tracing/trace_pipe
            bpf_trace_printk(____fmt, sizeof(____fmt),  \
                     ##__VA_ARGS__);            \
        })
#else
#define bpf_debug(fmt, ...) { } while (0)
#endif

// 解析包头得到ethertype
static __always_inline bool parse_eth(struct ethhdr *eth, void *data_end, u16 *eth_type)
{
    u64 offset;

    offset = sizeof(*eth);
    if ((void *)eth + offset &gt; data_end)
        return false;
    *eth_type = eth-&gt;h_proto;
    return true;
}

// 定义一个名为prog的段
// 由于我们使用iproute2来挂钩，因此必须用prog这个段名。否则iproute2无法识别
SEC("prog")
// xdp_md结构包含访问封包所需的所有数据
int xdp_ipv6_filter_program(struct xdp_md *ctx)
{
    // 得到封包头尾指针
    void *data_end = (void *)(long)ctx-&gt;data_end;
    void *data     = (void *)(long)ctx-&gt;data;
    struct ethhdr *eth = data;
    u16 eth_type = 0;
    // 调用parse_eth函数，可以获取封包的ethertype
    if (!(parse_eth(eth, data_end, eth_type))) {
        bpf_debug("Debug: Cannot parse L2\n");
        return XDP_PASS;
    }

    bpf_debug("Debug: eth_type:0x%x\n", ntohs(eth_type));
    // 判断ethertype是否IPv6
    if (eth_type == ntohs(0x86dd)) {
        return XDP_PASS;
    } else {
        return XDP_DROP;
    }
}

char _license[] SEC("license") = "GPL";</pre>
<p>编译此程序后得到xdp_ipv6_filter.o文件。要将此文件加载到网络接口，可以使用两种方法：</p>
<ol>
<li>编写一个用户空间程序，加载对象文件，挂钩到一个网络接口</li>
<li>调用iproute2进行等价操作</li>
</ol>
<p>某些网络接口在驱动层支持XDP，包括ixgbe, i40e, mlx5, veth, tap, tun, virtio_net等。这种情况下，XDP钩子实现在网络栈的最底部，即NIC从Rx Ring中接收到封包之后，具有最好的性能。对于其它类型的网络接口，XDP钩子则在更高的层次实现，性能相对较差。</p>
<p>我们这里用一对veth来测试：</p>
<pre class="crayon-plain-tag">sudo ip link add dev veth0 type veth peer name veth1
sudo ip link set up dev veth0
sudo ip link set up dev veth1

# 挂钩eBPF程序到接口上，程序必须写在prog段中
sudo ip link set dev veth1 xdp object xdp_ipv6_filter.o

# 查看接口信息
sudo ip link show veth1
8: veth1@veth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 xdp qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:05:fc:9a:d8:75 brd ff:ff:ff:ff:ff:ff
    # 可以看到xdp标记
    prog/xdp id 32 tag bdb81fb6a5cf3154 jited</pre>
<p>为了验证eBPF程序能正常工作，我们通过tcpdump回放<a href="https://github.com/dpino/xdp_ipv6_filter/blob/master/ipv4-and-ipv6-data.pcap">一段录制的封包</a>：</p>
<pre class="crayon-plain-tag"># 在veth1上录制IPv6封包，如果程序正常工作，应该接收不到
sudo tcpdump "ip6" -i veth1 -w captured.pcap -c 10
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
# 预期结果：
# 10 packets captured
# 10 packets received by filter
# 0 packets dropped by kernel


# 将IPv4/IPv6混合流量（此pcap中包含10个IPv4，10个IPv6）回放到veth0
# q由于eBPF程序的存在IPv6的封包应该不会发到veth1
sudo tcpreplay -i veth0 ipv4-and-ipv6-data.pcap</pre>
<p>在/sys/kernel/debug/tracing/trace_pipe 中可以看到日志：</p>
<pre class="crayon-plain-tag">sudo cat /sys/kernel/debug/tracing/trace_pipe
tcpreplay-4496  [003] ..s1 15472.046835: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046847: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046855: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046862: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046869: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046878: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046885: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046892: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046903: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046911: 0: Debug: eth_type:0x800
...</pre>
<div class="blog_h1"><span class="graybg">BCC</span></div>
<div class="blog_h2"><span class="graybg">转换为CO-RE</span></div>
<p>BCC提供了开发可移植的BPF程序的一套方法，但是比起新出现的BPF CO-RE有不少缺点，建议切换为BPF CO-RE。</p>
<div class="blog_h3"><span class="graybg">过渡</span></div>
<p>如果你需要同时支持BCC和CO-RE，可以利用<pre class="crayon-plain-tag">BCC_SEC</pre>宏，对于BCC来说，它会自动定义：</p>
<pre class="crayon-plain-tag">#ifdef BCC_SEC
#define __BCC__
#endif

#ifdef __BCC__
/* BCC-specific code */
#else
/* libbpf-specific code */
#endif</pre>
<div class="blog_h3"><span class="graybg">头文件</span></div>
<p>使用BPF CO-RE时，你不需要包含任何内核头文件（<pre class="crayon-plain-tag">#include &lt;linux/whatever.h&gt;</pre>），而仅仅需要包含一个<pre class="crayon-plain-tag">vmlinux.h</pre>即可。</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
/* linux headers needed for BCC only */
#else /* __BCC__ */
#include "vmlinux.h"               /* all kernel types */
#include &lt;bpf/bpf_helpers.h&gt;       /* most used helpers: SEC, __always_inline, etc */
#include &lt;bpf/bpf_core_read.h&gt;     /* for BPF CO-RE helpers */
#include &lt;bpf/bpf_tracing.h&gt;       /* for getting kprobe arguments */
#endif /* __BCC__ */</pre>
<p>由于vmlinux.h中可能缺少一部分内核通过#define定义的常量，因此你可能需要重新声明他们，这些变量中最常用的，已经声明在<pre class="crayon-plain-tag">bpf_helpers.h</pre>中了。</p>
<div class="blog_h3"><span class="graybg"> 字段访问</span></div>
<p>BCC会自动将<pre class="crayon-plain-tag">tsk-&gt;parent-&gt;pid</pre>这样的point chasing转换为bpf_probe_read()调用，CO-RE则没有这么好用功能，你需要使用<pre class="crayon-plain-tag">bpf_core_read.h</pre>中定义的助手函数/宏，并将上述表达式改写为<pre class="crayon-plain-tag">BPF_CORE_READ(tsk, parent, pid)</pre>形式。</p>
<p>从5.5+开始，<pre class="crayon-plain-tag">tp_btf</pre>和<pre class="crayon-plain-tag">fentry</pre>/<pre class="crayon-plain-tag">fext</pre>类型的BPF程序类型，可以使用原生的C指针访问语法。考虑你的目标环境。</p>
<p><pre class="crayon-plain-tag">BPF_CORE_READ</pre>和BCC兼容，也就是说，你可以统一使用该宏，避免重复代码。</p>
<div class="blog_h3"><span class="graybg">声明BPF Maps</span></div>
<p>下面给出BCC和libbpf声明Map语法的对比：</p>
<pre class="crayon-plain-tag">/* Array */
#ifdef __BCC__
BPF_ARRAY(my_array_map, struct my_value, 128);
#else
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 128);
    __type(key, u32);
    __type(value, struct my_value);
} my_array_map SEC(".maps");
#endif

/* Hashmap */
#ifdef __BCC__
BPF_HASH(my_hash_map, u32, struct my_value);
#else
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, struct my_value);
} my_hash_map SEC(".maps")
#endif

/* per-CPU array */
#ifdef __BCC__
BPF_PERCPU_ARRAY(heap, struct my_value, 1);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, struct my_value);
} heap SEC(".maps");
#endif</pre>
<p>注意：BCC中的Map默认大小一般是10240，使用libbpf时则需要精确的设置。</p>
<p>PERF_EVENT_ARRAY、STACK_TRACE以及一些其它的特殊Map（DEVMAP、CPUMAP）尚不支持BTF类型的键值，需要直接指定key_size/value_siz：</p>
<pre class="crayon-plain-tag">/* Perf event array (for use with perf_buffer API) */
#ifdef __BCC__
BPF_PERF_OUTPUT(events);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} events SEC(".maps");
#endif</pre>
<div class="blog_h3"><span class="graybg">访问BPF Maps</span></div>
<p>BCC使用伪C++风格的语法，这些语法会自动被改写为BPF助手函数调用。使用CO-RE时候，你需要将：</p>
<pre class="crayon-plain-tag">some_map.operation(some, args)</pre>
<p>改写为：</p>
<pre class="crayon-plain-tag">bpf_map_operation_elem(&amp;some_map, some, args);</pre>
<p>下面是一些例子：</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
    struct event *data = heap.lookup(&amp;zero);
#else
    struct event *data = bpf_map_lookup_elem(&amp;heap, &amp;zero);
#endif

#ifdef __BCC__
    my_hash_map.update(&amp;id, my_val);
#else
    bpf_map_update_elem(&amp;my_hash_map, &amp;id, &amp;my_val, 0 /* flags */);
#endif

#ifdef __BCC__
    events.perf_submit(args, data, data_len);
#else
    bpf_perf_event_output(args, &amp;events, BPF_F_CURRENT_CPU, data, data_len);
#endif</pre>
<div class="blog_h3"><span class="graybg">注册BPF Program</span></div>
<p>使用CO-RE时必须用<pre class="crayon-plain-tag">SEC()</pre>宏标记满足<a href="https://github.com/libbpf/libbpf/blob/787abf721ec8fac1a4a0a7b075acc79a927afed9/src/libbpf.c#L7935-L8075">约定</a>的段名：</p>
<pre class="crayon-plain-tag">#if !defined(__BCC__)
SEC("tracepoint/sched/sched_process_exec")
#endif
int tracepoint__sched__sched_process_exec(
#ifdef __BCC__
    struct tracepoint__sched__sched_process_exec *args
#else
    struct trace_event_raw_sched_process_exec *args
#endif
) {
/* ... */
}</pre>
<p>常用的约定例如：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">tp/&lt;category&gt;/&lt;name&gt;</pre> 用于tracepoint</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">kprobe/&lt;func_name&gt;</pre> 用于kprobe；<pre class="crayon-plain-tag">kretprobe/&lt;func_name&gt;</pre> 用于kretprobe</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">raw_tp/&lt;name&gt;</pre> 用于raw tracepoint</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">cgroup_skb/ingress</pre>, <pre class="crayon-plain-tag">cgroup_skb/egress</pre>, <pre class="crayon-plain-tag">cgroup/&lt;subtype&gt;</pre></p>
<div class="blog_h3"><span class="graybg">关于tracepoint</span></div>
<p>BCC和libbpf关于tracepoint上下文类型的命名有所不同。BCC的格式是<pre class="crayon-plain-tag">tracepoint__&lt;category&gt;__&lt;name&gt;</pre>，BCC会在运行时编译的时候，自动生成对应的C类型。</p>
<p>libbpf则没有这种自动生成类型的能力，不过内核已经提供了类似的，包含了所有tracepoint数据的类型，其命名为<pre class="crayon-plain-tag">trace_event_raw_&lt;name&gt;</pre>。</p>
<p>某些内核中的tracepoint会复用公共类型，因此上述类型对应关系不一定可用。例如没有<pre class="crayon-plain-tag">trace_event_raw_sched_process_exit</pre>，你需要使用<pre class="crayon-plain-tag">trace_event_raw_sched_process_template</pre>，具体需要关注内核源码或者vmlinux.h。</p>
<p>BCC和libbpf访问tracepoint上下文大部分字段，用同样的字段名。除了一些可变长度字符串字段。BBC是<pre class="crayon-plain-tag">data_loc_&lt;some_field&gt;</pre> 而libbpf是 <pre class="crayon-plain-tag">__data_loc_&lt;some_field&gt;</pre>格式。</p>
<div class="blog_h3"><span class="graybg">关于kprobe</span></div>
<p>Kprobe BPF程序以一个<pre class="crayon-plain-tag">struct pt_regs</pre>作为上下文参数，BCC支持直接声明为函数参数，libbpf则需要借助<pre class="crayon-plain-tag">BPF_KPROBE</pre> / <pre class="crayon-plain-tag">BPF_KRETPROBE</pre>宏：</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead)
#else
SEC("kprobe/acct_collect")
int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead)
#endif
{
    // 对于libbpf，在这里也可以访问名为ctx的pt_regs*
}</pre>
<p>另外一个需要注意的点，在4.17+，系统调用函数发生了重命名，例如<pre class="crayon-plain-tag">sys_kill</pre>被重命名为<pre class="crayon-plain-tag">__x64_sys_kill</pre>（对于x86，其它平台下有不同的前缀）。使用kprobe时需要注意到这种变化，如果可能，尽量使用tracepoint。</p>
<p>从5.5+开始，可以考虑用<pre class="crayon-plain-tag">raw_tp</pre>/<pre class="crayon-plain-tag">fentry</pre>/<pre class="crayon-plain-tag">fexit</pre>代替<pre class="crayon-plain-tag">tracepoint</pre>/<pre class="crayon-plain-tag">kprobe</pre>/<pre class="crayon-plain-tag">kretprobe</pre>。这些新探针具有更好的性能、易用性。</p>
<div class="blog_h3"><span class="graybg">处理编译时#if</span></div>
<p>在BCC代码中依赖预处理器（#ifdef 或者 #if）是流行的做法，原因通常是为了适配不同版本的内核、根据程序配置启用/禁用代码片段。CO-RE提供类似的能力：Kconfig externs和struct flavors。</p>
<div class="blog_h1"><span class="graybg">附录</span></div>
<div class="blog_h2"><span class="graybg">相关项目</span></div>
<div class="blog_h3"><span class="graybg">BCC</span></div>
<p>参考：<a href="/bcc">基于BCC进行性能追踪和网络监控</a></p>
<p>BCC（BPF Compiler Collection，BPF编译器集合）是一个工具箱，它提供了很多追踪用途的BPF程序，加载这些程序时使用BCC提供的Python接口。</p>
<div class="blog_h3"><span class="graybg">bpftrace</span></div>
<p>一个DTrace风格的动态追踪工具，使用LLVM作为后端，将脚本编译为BPF字节码，并使用BCC和内核的BPF tracing基础设施进行交互。和原生BCC相比，它提供了更加高级的、用于实现追踪脚本的语言。</p>
<div class="blog_h3"><span class="graybg">perf</span></div>
<p>参考：<a href="/perf">利用perf剖析Linux应用程序</a></p>
<p>perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中。它有三种工作模式，其中最新的是4.4引入的BPF模式，支持载入tracing BPF程序。</p>
<div class="blog_h2"><span class="graybg">参考</span></div>
<ol>
<li><a href="https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/">Dive into BPF: a list of reading material</a></li>
<li><a href="https://docs.cilium.io/en/v1.9/bpf">https://docs.cilium.io/en/v1.9/bpf</a></li>
<li><a href="https://github.com/iovisor/bpf-docs/blob/master/bpf_helpers.rst">list of eBPF helper functions</a></li>
</ol>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ebpf">eBPF学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ebpf/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>IPVS模式下ClusterIP泄露宿主机端口的问题</title>
		<link>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode</link>
		<comments>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode#comments</comments>
		<pubDate>Tue, 05 Jan 2021 10:50:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[IPVS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35061</guid>
		<description><![CDATA[<p>问题 在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。 这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。 在宿主机上尝试[crayon-69e2a15487c38283096018-i/]，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。 定义 如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。 这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。 如果kube-proxy使用iptables模式，不会出现这种异常行为。 原因 启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）： [crayon-69e2a15487c3d172552865/] <a class="read-more" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">问题</span></div>
<p>在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。</p>
<p>这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。</p>
<p>在宿主机上尝试<pre class="crayon-plain-tag">telnet RegistryClusterIP 443</pre>，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。</p>
<div class="blog_h1"><span class="graybg">定义</span></div>
<p>如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。</p>
<p>这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。</p>
<p>如果kube-proxy使用iptables模式，不会出现这种异常行为。</p>
<div class="blog_h1"><span class="graybg">原因</span></div>
<p>启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）：</p>
<pre class="crayon-plain-tag">5: kube-ipvs0: &lt;BROADCAST,NOARP&gt; mtu 1500 qdisc noop state DOWN group default 
    link/ether fa:d9:9e:37:12:68 brd ff:ff:ff:ff:ff:ff
    inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
       valid_lft forever preferred_lft forever</pre>
<p>这种绑定是必须的，因为IPVS的工作原理是，<span style="background-color: #c0c0c0;">在netfilter挂载点LOCAL_IN上注册钩子ip_vs_in，拦截目的地是VIP（ClusterIP）的封包</span>。而要使得封包进入到LOCAL_IN，它的目的地址必须是本机地址。</p>
<p>每当为网络接口添加一个IP地址，内核都会<span style="background-color: #c0c0c0;">自动</span>在local路由表中增加一条规则，对于上面的10.96.0.10，会增加：</p>
<pre class="crayon-plain-tag"># 对于目的地址是10.96.0.10的封包，从kube-ipvs0发出，如果没有指定源IP，使用10.96.0.10
local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>上述自动添加路由的一个副作用是，<span style="background-color: #c0c0c0;">对于任意一个端口Port，如果不存在匹配ClusterIP:Port的IPVS规则，同时宿主机上某个应用在0.0.0.0:Port上监听，封包就会交由此应用处理</span>。</p>
<p>在宿主机上执行<pre class="crayon-plain-tag">telnet 10.96.0.10 22</pre>，会发生以下事件序列：</p>
<ol>
<li>出站选路，根据local表路由规则，从kube-ipvs0接口发出封包</li>
<li>由于kube-ipvs0是dummy的，封包<span style="background-color: #c0c0c0;">立刻从kube-ipvs0的出站队列移动到入站队列</span></li>
<li>目的地址是本地地址，因此进入LOCAL_IN挂载点</li>
<li>由于22不是ServicePort，封包被转发给本地进程处理，即监听了22的那个进程</li>
</ol>
<p>如果删除内核自动在local表中添加的路由：</p>
<pre class="crayon-plain-tag">ip route del table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>则会出现以下现象：</p>
<ol>
<li>无法访问10.96.0.10:22。这是我们期望的，因为10.96.0.10这个服务没有暴露22端口，此端口理当不通</li>
<li>无法ping 10.96.0.10。这不是我们期望的，但是一般情况下不会有什么问题。iptables模式下ClusterIP就是无法ping的，IPVS模式下可以在本机ping仅仅是绑定ClusterIP到kube-ipvs0的一个副作用。通常应用程序不应该对ClusterIP做ICMP检测，来判断服务是否可用，因为这依赖了kube-proxy的特定工作模式</li>
<li>在宿主机上，可以访问10.96.0.10:53。这是我们期望的，宿主机上可以访问ClusterIP</li>
<li>在某个容器的网络命名空间下，无法访问10.96.0.10:53。这不是我们期望的，相当于Pod无法访问ClusterIP了</li>
</ol>
<p>以上4条，惟独3难以理解。<span style="background-color: #c0c0c0;">为什么路由没了，宿主机仍然能访问ClusterIP:ServicePort</span>？这个我们还没有从源码级别深究，但是很明显和IPVS有关。IPVS在LOCAL_OUT上挂有钩子，它可能在此钩子中检测到来自本机（主网络命名空间）的、访问ClusterIP+ServicePort（即IPVS虚拟服务）的封包，并进行了某种“魔法”处理，从而避开了没有路由的问题。</p>
<p>下面我们进一步验证上述“魔法”处理的可能性。使用<pre class="crayon-plain-tag">tcpdump -i any host 10.96.0.10</pre>来捕获流量，从容器命名空间访问ClusterIP:ServicePort时，可以看到：</p>
<pre class="crayon-plain-tag">#                  容器IP
11:32:00.448470 IP 172.27.0.24.56378 &gt; 10.96.0.10.53: Flags [S], seq 2946888109, win 28200, options...</pre>
<p>但是从宿主机访问ClusterIP:ServicePort时，则捕获不到任何流量。但是，通过iptables logging，我们可以确定，内核的确<span style="background-color: #c0c0c0;">以ClusterIP为源地址和目的地址</span>，发起了封包：</p>
<pre class="crayon-plain-tag">iptables -t mangle -I OUTPUT 1 -p tcp --dport 53 -j LOG --log-prefix 'out-d53: '

# dmesg -w
#                                      源地址          目的地址
# [3374381.426541] out-d53: IN= OUT=lo SRC=10.96.0.100 DST=10.96.0.100 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=18885 DF PROTO=TCP SPT=42442 DPT=53 WINDOW=86 RES=0x00 ACK URGP=0 </pre>
<p>回顾一下数据报出站、入站的处理过程：</p>
<ol>
<li>出站，依次经过 <strong><span style="background-color: #99cc00;">netfilter/iptables</span></strong> ⇨ <strong><span style="background-color: #cc99ff;">tcpdump</span></strong> ⇨ 网络接口 ⇨网线</li>
<li>入站，依次经过 网线 ⇨ 网络接口 ⇨ tcpdump ⇨ netfilter/iptables</li>
</ol>
<p>只有当IPVS在宿主机请求10.96.0.10的封包出站时，在netfilter中对匹配IPVS虚拟服务的封包进行如下处理，才能解释<span style="background-color: #99cc00;"><strong>iptables</strong></span>中能看到10.96.0.10，而紧随其后的<strong><span style="background-color: #cc99ff;">tcpdump</span></strong>中却又看不到的现象：</p>
<ol>
<li>修改目的地址为Service的Endpoint地址，这就是NAT模式的IPVS（即kube-proxy使用NAT模式）应有的行为</li>
<li>修改了源地址为当前宿主机的地址，不这样做，回程报文就无法路由回来</li>
</ol>
<p>另外注意一下，如果从宿主机访问ClusterIP:NonServicePort，则tcpdump能捕获到源或目的地址为ClusterIP的流量。这是因为IPVS发现它不匹配任何虚拟服务，会直接返回NF_ACCEPT，然后封包就按照常规流程处理了。</p>
<div class="blog_h1"><span class="graybg">后果</span></div>
<div class="blog_h2"><span class="graybg">安全问题</span></div>
<p>如果宿主机上有一个在0.0.0.0上监听的、存在安全漏洞的服务，则可能被恶意的工作负载利用。</p>
<div class="blog_h2"><span class="graybg">行为异常</span></div>
<p>少部分的应用程序，例如docker manifest，其行为取决于端口探测的结果，会无法正常工作。</p>
<div class="blog_h1"><span class="graybg">解决</span></div>
<p>可能的解决方案有：</p>
<ol>
<li>在iptables中匹配哪些针对ClusterIP:NonServicePort的流量，Drop或Reject掉</li>
<li>使用基于fwmark的IPVS虚拟服务，这需要在iptables中对针对ClusterIP:ServicePort的流量打fwmark，而且每个ClusterIP都需要占用独立的fwmark，难以管理</li>
</ol>
<p>对于解决方案1，可以使用如下iptables规则： </p>
<pre class="crayon-plain-tag">#                 如果目的地址是ClusterIP    但是目的端口不是ServicePort           则拒绝
iptables -A INPUT -d  10.96.0.0/12 -m set ! --match-set KUBE-CLUSTER-IP dst,dst -j REJECT</pre>
<p>这个规则能够为容器解决宿主机端口泄露的问题，但是会导致宿主机上无法访问ClusterIP。</p>
<p>引起此问题的原因是，在宿主机访问ClusterIP时，会同时使用ClusterIP作为源地址/目的地址。这样，来自Endpoint的回程报文，unNATed后的目的地址，就会匹配到上面的iptables规则，从而导致封包被Reject掉。</p>
<p>要解决此问题，我们可以修改内核自动添加的路由，提示使用其它地址作为源地址：</p>
<pre class="crayon-plain-tag"># 这条路由给出src提示，当访问10.96.0.10时，选取192.168.104.82（节点IP）作为源地址
ip route replace table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 192.168.104.82</pre>
<div class="blog_h1"><span class="graybg">深入</span></div>
<p>上文我们提到了一个“魔法”处理的猜想，这里我们对IPVS的实现细节进行深入学习，证实此猜想。</p>
<p>本节牵涉到的内核源码均来自linux-3.10.y分支。</p>
<div class="blog_h2"><span class="graybg">Netfilter</span></div>
<p>这是从2.4.x引入内核的一个框架，用于实现防火墙、NAT、封包修改、记录封包日志、用户空间封包排队之类的功能。</p>
<p>netfilter运行在内核中，允许内核模块在Linux网络栈的<span style="background-color: #c0c0c0;">不同位置注册钩子（回调函数），当每个封包穿过网络栈时，这些钩子函数会被调用</span>。</p>
<p><a href="/iptables">iptables</a>是经典的，基于netfilter的用户空间工具。它的继任者是nftables，它更加灵活、可扩容、性能好。</p>
<div class="blog_h3"><span class="graybg">钩子挂载点</span></div>
<p>netfilter提供了5套钩子（的挂载点）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">挂载点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NF_IP_PER_ROUTING</td>
<td>
<p>当封包进入网络栈时调用。封包的目的地可能是本机，或者需要转发</p>
<p>ip_rcv / ipv6_rcv是内核接受并处理IP数据报的入口，此函数会调用这类钩子：</p>
<pre class="crayon-plain-tag">int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	// ...
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
		       ip_rcv_finish);
}</pre>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_IN</td>
<td>
<p>当路由判断封包应该由本机处理时（目的地址是本机地址）调用
<p>ip_local_deliver / ip6_input负责将IP数据报向上层传递，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_FORWARD</td>
<td>
<p>当路由判断封包应该被转发给其它机器（或者网络命名空间）时调用</p>
<p>ip_forward / ip6_forward负责封包转发，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_POST_ROUTING</td>
<td>
<p>在封包即将离开网络栈（进入网线）时调用，不管是转发的、还是本机发出的，都需要经过此挂载点</p>
<p>ip_output / ip6_finish_output2会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_OUT</td>
<td>
<p>当封包由本机产生，需要往外发送时调用</p>
<p>__ip_local_out / __ip6_local_out会调用这类钩子</p>
</td>
</tr>
</tbody>
</table>
<p>这些挂载点，和iptables的各链是对应的。</p>
<div class="blog_h3"><span class="graybg">注册钩子</span></div>
<p>要在内核中使用netfilter的钩子，你需要调用函数：</p>
<pre class="crayon-plain-tag">// 注册钩子
int nf_register_hook(struct nf_hook_ops *reg){}
// 反注册钩子
void nf_unregister_hook(struct nf_hook_ops *reg){}</pre>
<p>入参nf_hook_ops是一个结构：</p>
<pre class="crayon-plain-tag">struct nf_hook_ops {
	// 钩子的函数指针，依据内核的版本不同此函数的签名有所差异
	nf_hookfn		*hook;
	struct net_device	*dev;
	void			*priv;
	// 钩子针对的协议族，PF_INET表示IPv4
	u_int8_t		pf;
	// 钩子类型代码，参考上面的表格
	unsigned int		hooknum;
	// 每种类型的钩子，都可以有多个，此数字决定执行优先级
	int			priority;
};


// 钩子函数的签名
typedef unsigned int nf_hookfn(unsigned int hooknum,
			       struct sk_buff *skb, // 正被处理的数据报
			       const struct net_device *in, // 输入设备
			       const struct net_device *out, // 是出设备
			       int (*okfn)(struct sk_buff *)); // 如果通过钩子检查，则调用此函数，通常用不到</pre>
<div class="blog_h3"><span class="graybg">钩子返回值</span></div>
<pre class="crayon-plain-tag">/* Responses from hook functions. */
// 丢弃该报文，不再继续传输或处理
#define NF_DROP 0
// 继续正常传输报文，如果后面由低优先级的钩子，仍然会调用它们
#define NF_ACCEPT 1
// 告知netfilter，报文被别人偷走处理了，不需要再对它做任何处理
// 下文的分析中，我们有个例子。一个netfilter钩子在内部触发了对netfilter钩子的调用
// 外层钩子返回的就是NF_STOLEN，相当于将封包的控制器转交给内层钩子了
#define NF_STOLEN 2
// 对该数据报进行排队，通常用于将数据报提交给用户空间进程处理
#define NF_QUEUE 3
// 再次调用该钩子函数
#define NF_REPEAT 4
// 继续正常传输报文，不会调用此挂载点的后续钩子
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP </pre>
<div class="blog_h3"><span class="graybg">钩子优先级</span></div>
<p>优先级通常以下面的枚举为基准+/-：</p>
<pre class="crayon-plain-tag">enum nf_ip_hook_priorities {
	// 数值越小，优先级越高，越先执行
	NF_IP_PRI_FIRST = INT_MIN,
	NF_IP_PRI_CONNTRACK_DEFRAG = -400,
	// 可以看到iptables各表注册的钩子的优先级
	NF_IP_PRI_RAW = -300,
	NF_IP_PRI_SELINUX_FIRST = -225,
	NF_IP_PRI_CONNTRACK = -200,
	NF_IP_PRI_MANGLE = -150,
	NF_IP_PRI_NAT_DST = -100,
	NF_IP_PRI_FILTER = 0,
	NF_IP_PRI_SECURITY = 50,
	NF_IP_PRI_NAT_SRC = 100,
	NF_IP_PRI_SELINUX_LAST = 225,
	NF_IP_PRI_CONNTRACK_HELPER = 300,
	NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
	NF_IP_PRI_LAST = INT_MAX,
};</pre>
<div class="blog_h2"><span class="graybg"><a id="ipvs"></a>IPVS</span></div>
<div class="blog_h3"><span class="graybg">钩子列表</span></div>
<p> ip_vs模块初始化时，会通过ip_vs_init函数，调用nf_register_hook，注册以下netfilter钩子：</p>
<pre class="crayon-plain-tag">static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
	// 注册到LOCAL_IN，这两个钩子处理外部客户端的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复外部客户端的报文，例如修改IP地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 2,
	},
	// 转而调用ip_vs_in，用于处理外部客户端进入IPVS的请求报文
	// 如果没有对应请求报文的连接，则使用调度函数创建连接结构，这其中牵涉选择RS负载均衡算法
	{
		.hook		= ip_vs_remote_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 1,
	},

	// 注册到LOCAL_OUT，这两个钩子处理LVS本机的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复客户端的报文
	{
		.hook		= ip_vs_local_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 1,
	},
	// 转而调用ip_vs_in，调度并转发（给RS）本机的请求
	{
		.hook		= ip_vs_local_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 2,
	},

	// 这两个函数注册到FORWARD
	// 转而调用ip_vs_in_icmp，用于处理外部客户端发到IPVS的ICMP报文，并转发到RS
	{
		.hook		= ip_vs_forward_icmp,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 99,
	},
	// 转而调用ip_vs_out，用于NAT模式下，修改RS给的应答报文的源地址为IPVS虚拟地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 100,
	}
};</pre>
<div class="blog_h3"><span class="graybg">ip_vs_in</span></div>
<p>从上面的钩子我们可以看到：</p>
<ol>
<li>针对外部发起的、本机发起的，对IPVS的请求（目的是VIP的SYN），钩子的位置是不一样的：
<ol>
<li>对于外部的请求，在LOCAL_IN中处理，钩子函数为ip_vs_remote_request4</li>
<li>对于本机的请求，在LOCAL_OUT中处理，钩子函数为ip_vs_local_request4</li>
</ol>
</li>
<li> 尽管钩子的位置不同，但是函数ip_vs_remote_request4、ip_vs_local_request4都是调用ip_vs_in。实际上，这两个函数的逻辑完全一样：<br />
<pre class="crayon-plain-tag">/*
 *	AF_INET handler in NF_INET_LOCAL_IN chain
 *	Schedule and forward packets from remote clients
 */
static unsigned int
ip_vs_remote_request4(unsigned int hooknum, struct sk_buff *skb,
		      const struct net_device *in,
		      const struct net_device *out,
		      int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}

/*
 *	AF_INET handler in NF_INET_LOCAL_OUT chain
 *	Schedule and forward packets from local clients
 */
static unsigned int
ip_vs_local_request4(unsigned int hooknum, struct sk_buff *skb,
		     const struct net_device *in, const struct net_device *out,
		     int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}</pre>
</li>
</ol>
<p>回顾一下上文我们关于“魔法”处理的疑惑。对于从宿主机发起对10.96.0.10:53的请求，我们通过iptables logging证实了使用的源IP地址是10.96.0.10：</p>
<ol>
<li>这个请求为什么tcpdump捕获不到？</li>
<li>为什么删除路由不影响宿主机对ClusterIP的请求（却又导致容器无法请求ClusterIP）？</li>
</ol>
<p>这两个问题的答案，很可能就隐藏在ip_vs_in函数中，因为它是处理进入IPVS的数据报的统一入口。如果该函数同时修改了原始封包的源/目的地址，就解释了问题1；如果该函数在内部进行了选路操作，则解释了问题2。</p>
<p>下面分析一下ip_vs_in的代码：</p>
<pre class="crayon-plain-tag">static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff *skb, int af)
{
	// 网络命名空间
	struct net *net;
	// IPVS的IP头，其中存有3层头len、协议、标记、源/目的地址
	struct ip_vs_iphdr iph;
	// 持有协议（TCP/UDP/SCTP/AH/ESP）信息，更重要的是带着很多函数指针。这些指针负责针对特定协议的IPVS逻辑
	struct ip_vs_protocol *pp;
	// 每个命名空间一个此对象，包含统计计数器、超时表
	struct ip_vs_proto_data *pd;
	// 当前封包所属的IPVS连接对象，此对象最重要的是packet_xmit函数指针。它负责将封包发走
	struct ip_vs_conn *cp;
	int ret, pkts;
	// 描述当前网络命名空间的IPVS状态
	struct netns_ipvs *ipvs;

	// 如果封包已经被标记为IPVS请求/应答，不做处理，继续netfilter常规流程
	// 后续ip_vs_nat_xmit会让封包“重入”netfilter，那时封包已经打上IPVS标记
	// 这里的判断确保重入的封包走netfilter常规流程，而不是进入死循环
	if (skb-&gt;ipvs_property)
		return NF_ACCEPT;


	// 如果封包目的地不是本机且当前不在LOCAL_OUT
	// 或者封包的dst_entry不存在，不做处理，继续netfilter常规流程
	if (unlikely((skb-&gt;pkt_type != PACKET_HOST &amp;&amp;
		      hooknum != NF_INET_LOCAL_OUT) ||
		     !skb_dst(skb))) {
		ip_vs_fill_iph_skb(af, skb, &amp;iph);
		IP_VS_DBG_BUF(12, "packet type=%d proto=%d daddr=%s"
			      " ignored in hook %u\n",
			      skb-&gt;pkt_type, iph.protocol,
			      IP_VS_DBG_ADDR(af, &amp;iph.daddr), hooknum);
		return NF_ACCEPT;
	}
	// 如果当前IPVS主机是backup，或者当前命名空间没有启用IPVS，不做处理，继续netfilter常规流程
	net = skb_net(skb);
	ipvs = net_ipvs(net);
	if (unlikely(sysctl_backup_only(ipvs) || !ipvs-&gt;enable))
		return NF_ACCEPT;

	// 使用封包的IP头填充IPVS的IP头
	ip_vs_fill_iph_skb(af, skb, &amp;iph);

	// 如果是RAW套接字，不做处理，继续netfilter常规流程
	if (unlikely(skb-&gt;sk != NULL &amp;&amp; hooknum == NF_INET_LOCAL_OUT &amp;&amp;
		     af == AF_INET)) {
		struct sock *sk = skb-&gt;sk;
		struct inet_sock *inet = inet_sk(skb-&gt;sk);

		if (inet &amp;&amp; sk-&gt;sk_family == PF_INET &amp;&amp; inet-&gt;nodefrag)
			return NF_ACCEPT;
	}

	// 处理ICMP报文，和我们的场景无关
	if (unlikely(iph.protocol == IPPROTO_ICMP)) {
		int related;
		int verdict = ip_vs_in_icmp(skb, &amp;related, hooknum);
		if (related)
			return verdict;
	}

	// 如果协议不受IPVS支持，不做处理，继续netfilter常规流程
	pd = ip_vs_proto_data_get(net, iph.protocol);
	if (unlikely(!pd))
		return NF_ACCEPT;
	// 协议被支持，得到pp
	pp = pd-&gt;pp;
	// 尝试获取封包所属的IPVS连接对象
	cp = pp-&gt;conn_in_get(af, skb, &amp;iph, 0);
	// 如果封包属于既有IPVS连接，且此连接的RS（dest）已经设置，且RS的权重为0
	// 认为是无效连接，设为过期
	if (unlikely(sysctl_expire_nodest_conn(ipvs)) &amp;&amp; cp &amp;&amp; cp-&gt;dest &amp;&amp;
	    unlikely(!atomic_read(&amp;cp-&gt;dest-&gt;weight)) &amp;&amp; !iph.fragoffs &amp;&amp;
	    is_new_conn(skb, &amp;iph)) {
		ip_vs_conn_expire_now(cp);
		__ip_vs_conn_put(cp);
		cp = NULL;
	}

	// 调度一个新的IPVS连接，这里牵涉到RS的LB算法
	if (unlikely(!cp) &amp;&amp; !iph.fragoffs) {
		int v;
		if (!pp-&gt;conn_schedule(af, skb, pd, &amp;v, &amp;cp, &amp;iph))
			// 如果返回0，通常v是NF_DROP，这以为这调度失败，封包丢弃
			return v;
	}

	if (unlikely(!cp)) {
		IP_VS_DBG_PKT(12, af, pp, skb, 0,
			      "ip_vs_in: packet continues traversal as normal");
		if (iph.fragoffs) {
			IP_VS_DBG_RL("Unhandled frag, load nf_defrag_ipv6\n");
			IP_VS_DBG_PKT(7, af, pp, skb, 0, "unhandled fragment");
		}
		return NF_ACCEPT;
	}

	// 入站封包 —— 在我们的场景中，这是本地客户端入了IPVS系统的封包
	// 从网络栈的角度来说，我们正在处理的是出站封包...
	IP_VS_DBG_PKT(11, af, pp, skb, 0, "Incoming packet");

	// IPVS连接的RS不可用
	if (cp-&gt;dest &amp;&amp; !(cp-&gt;dest-&gt;flags &amp; IP_VS_DEST_F_AVAILABLE)) {
		// 立即将连接设为过期
		if (sysctl_expire_nodest_conn(ipvs)) {
			ip_vs_conn_expire_now(cp);
		}
		// 丢弃封包
		__ip_vs_conn_put(cp);
		return NF_DROP;
	}
	// 更新计数器
	ip_vs_in_stats(cp, skb);
	// 更新IPVS连接状态机，做的事情包括
	//   根据数据包 tcp 标记字段来更新当前状态机
	//   更新连接对应的统计数据，包括：活跃连接和非活跃连接
	//   根据连接状态，设置超时时间
	ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pd);

	if (cp-&gt;packet_xmit)
		// 调用packet_xmit将封包发走，实际上是重入netfilter的LOCAL_OUT，封包控制权转移走，后续不该再操控skb
		ret = cp-&gt;packet_xmit(skb, cp, pp, &amp;iph);
	else {
		IP_VS_DBG_RL("warning: packet_xmit is null");
		ret = NF_ACCEPT;
	}

	if (cp-&gt;flags &amp; IP_VS_CONN_F_ONE_PACKET)
		pkts = sysctl_sync_threshold(ipvs);
	else
		pkts = atomic_add_return(1, &amp;cp-&gt;in_pkts);

	if (ipvs-&gt;sync_state &amp; IP_VS_STATE_MASTER)
		ip_vs_sync_conn(net, cp, pkts);

	// 放回连接对象，重置连接定时器
	ip_vs_conn_put(cp);
	return ret;
}</pre>
<p>上面这段代码中，“魔法”处理最可能发生在：</p>
<ol>
<li>conn_schedule：在这里需要进行IPVS连接的调度</li>
<li>packet_xmit：在这里发送经过IPVS处理的封包</li>
</ol>
<p>二者都是函数指针，在TCP协议下，conn_schedule指向tcp_conn_schedule。在NAT模式下，packet_xmit指向ip_vs_nat_xmit。packet_xmit指针是在conn_schedule过程中初始化的。</p>
<div class="blog_h3"><span class="graybg">tcp_conn_schedule</span></div>
<p>我们看一下TCP协议下IPVS连接的调度过程。</p>
<pre class="crayon-plain-tag">static int
tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd,
		  int *verdict, struct ip_vs_conn **cpp,
		  struct ip_vs_iphdr *iph)
{
	// 网络命名空间
	struct net *net;
	// IPVS虚拟服务对象
	struct ip_vs_service *svc;
	struct tcphdr _tcph, *th;

	// 解析L4头，如果失败，提示ip_vs_in丢弃封包
	th = skb_header_pointer(skb, iph-&gt;len, sizeof(_tcph), &amp;_tcph);
	if (th == NULL) {
		*verdict = NF_DROP;
		return 0;
	}
	net = skb_net(skb);
	rcu_read_lock();
	if (th-&gt;syn &amp;&amp;
	    // 根据封包特征，去查找匹配的虚拟服务
	    (svc = ip_vs_service_find(net, af, skb-&gt;mark, iph-&gt;protocol,
				      &amp;iph-&gt;daddr, th-&gt;dest))) {
		int ignored;
		// 如果当前网络命名空间“过载”了，丢弃封包
		if (ip_vs_todrop(net_ipvs(net))) {
			rcu_read_unlock();
			*verdict = NF_DROP;
			return 0;
		}

		// 选择一个RS，建立IPVS连接
		// 如果找不到RS，或者发生致命错误，则ignore为0或-1，这种情况下
		// IPVS连接没有成功创建，提示ip_vs_in丢弃封包，可能附带回复ICMP
		*cpp = ip_vs_schedule(svc, skb, pd, &amp;ignored, iph);
		if (!*cpp &amp;&amp; ignored &lt;= 0) {
			if (!ignored)
				// ignored=0，找不到RS
				*verdict = ip_vs_leave(svc, skb, pd, iph);
			else
				*verdict = NF_DROP;
			rcu_read_unlock();
			return 0;
		}
	}
	rcu_read_unlock();
	// 如果调度成功，IPVS连接对象不为空，返回1
	/* NF_ACCEPT */
	return 1;
}</pre>
<p>到这里我们还没有看到IPVS对封包地址进行更改，需要进一步阅读ip_vs_schedule。 </p>
<div class="blog_h3"><span class="graybg">ip_vs_schedule</span></div>
<p>这是IPVS调度的核心函数，它支持TCP/UDP，它为虚拟服务选择一个RS，创建IPVS连接对象。</p>
<pre class="crayon-plain-tag">struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
	       struct ip_vs_proto_data *pd, int *ignored,
	       struct ip_vs_iphdr *iph)
{
	struct ip_vs_protocol *pp = pd-&gt;pp;
	// IPVS连接对象（connection entry）
	struct ip_vs_conn *cp = NULL;
	struct ip_vs_scheduler *sched;
	struct ip_vs_dest *dest;
	__be16 _ports[2], *pptr;
	unsigned int flags;

	// ...

	*ignored = 0;

	/*
	 *    Non-persistent service
	 */
	// 调度工作委托给虚拟服务的scheduler
	sched = rcu_dereference(svc-&gt;scheduler);
	// 调度器就是选择一个RS（ip_vs_dest）
	dest = sched-&gt;schedule(svc, skb);
	if (dest == NULL) {
		IP_VS_DBG(1, "Schedule: no dest found.\n");
		return NULL;
	}

	flags = (svc-&gt;flags &amp; IP_VS_SVC_F_ONEPACKET
		 &amp;&amp; iph-&gt;protocol == IPPROTO_UDP) ?
		IP_VS_CONN_F_ONE_PACKET : 0;

	// 初始化IPVS连接对象 ip_vs_conn
	{
		struct ip_vs_conn_param p;

		ip_vs_conn_fill_param(svc-&gt;net, svc-&gt;af, iph-&gt;protocol,
				      &amp;iph-&gt;saddr, pptr[0], &amp;iph-&gt;daddr,
				      pptr[1], &amp;p);
		// 操控ip_vs_conn的逻辑包括：
		//   初始化定时器
		//   设置网络命名空间
		//   设置地址、fwmark、端口
		//   根据IP版本、IPVS模式（NAT/DR/TUN）为连接设置一个packet_xmit
		cp = ip_vs_conn_new(&amp;p, &amp;dest-&gt;addr,
				    dest-&gt;port ? dest-&gt;port : pptr[1],
				    flags, dest, skb-&gt;mark);
		if (!cp) {
			*ignored = -1;
			return NULL;
		}
	}

	// ...
	return cp;
}</pre>
<p>到这里我们可以看到， conn_schedule仍然没有对封包做任何修改。看来关键在packet_xmit函数中。</p>
<div class="blog_h3"><span class="graybg">ip_vs_nat_xmit</span></div>
<pre class="crayon-plain-tag">int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
	       struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
	// 路由表项
	struct rtable *rt;		/* Route to the other host */
	// 是否本机    是否输入路由
	int local, rc, was_input;

	EnterFunction(10);

	rcu_read_lock();
	// 是否尚未设置客户端端口
	if (unlikely(cp-&gt;flags &amp; IP_VS_CONN_F_NO_CPORT)) {
		__be16 _pt, *p;

		p = skb_header_pointer(skb, ipvsh-&gt;len, sizeof(_pt), &amp;_pt);
		if (p == NULL)
			goto tx_error;
		// 设置IPVS连接对象的cport
		// caddr cport 客户端地址
		// vaddr vport 虚拟服务地址
		// daddr dport RS地址
		ip_vs_conn_fill_cport(cp, *p);
		IP_VS_DBG(10, "filled cport=%d\n", ntohs(*p));
	}

	was_input = rt_is_input_route(skb_rtable(skb));
	// 出口路由查找，依据是封包、RS的地址、以及若干标识位
	// 返回值提示路由目的地是否是本机
	local = __ip_vs_get_out_rt(skb, cp-&gt;dest, cp-&gt;daddr.ip,
				   IP_VS_RT_MODE_LOCAL |
				   IP_VS_RT_MODE_NON_LOCAL |
				   IP_VS_RT_MODE_RDR, NULL);
	if (local &lt; 0)
		goto tx_error;
	rt = skb_rtable(skb);

	// 如果目的地是本机，RS地址是环回地址，是输入
	if (local &amp;&amp; ipv4_is_loopback(cp-&gt;daddr.ip) &amp;&amp; was_input) {
		IP_VS_DBG_RL_PKT(1, AF_INET, pp, skb, 0, "ip_vs_nat_xmit(): "
				 "stopping DNAT to loopback address");
		goto tx_error;
	}

	// 封包将被修改，执行copy-on-write
	if (!skb_make_writable(skb, sizeof(struct iphdr)))
		goto tx_error;

	if (skb_cow(skb, rt-&gt;dst.dev-&gt;hard_header_len))
		goto tx_error;

	// 修改封包，dnat_handler指向tcp_dnat_handler
	if (pp-&gt;dnat_handler &amp;&amp; !pp-&gt;dnat_handler(skb, pp, cp, ipvsh))
		goto tx_error;
	// 更改目的地址
	ip_hdr(skb)-&gt;daddr = cp-&gt;daddr.ip;
	// 为出站封包生成chksum
	ip_send_check(ip_hdr(skb));

	IP_VS_DBG_PKT(10, AF_INET, pp, skb, 0, "After DNAT");

	skb-&gt;local_df = 1;

	// 发送封包：
	//   如果发送出去了，返回 NF_STOLEN
	//   如果没有发送（local=1，目的地是本机），返回NF_ACCEPT
	rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local);
	rcu_read_unlock();

	LeaveFunction(10);
	return rc;

  tx_error:
	kfree_skb(skb);
	rcu_read_unlock();
	LeaveFunction(10);
	return NF_STOLEN;
}

static inline int ip_vs_nat_send_or_cont(int pf, struct sk_buff *skb,  struct ip_vs_conn *cp, int local)
{
	// 注意这个NF_STOLEN的含义，参考上文
	int ret = NF_STOLEN;
	// 给封包设置IPVS标记，NF_HOOK会导致当前封包重入netfilter，此标记会让重入后的封包立即NF_ACCEPT、
	// 重入让修改后的封包有机会被ipables处理
	skb-&gt;ipvs_property = 1;
	if (likely(!(cp-&gt;flags &amp; IP_VS_CONN_F_NFCT)))
		ip_vs_notrack(skb);
	else
		ip_vs_update_conntrack(skb, cp, 1);
	// 如果目的地不是本机
	if (!local) {
		skb_forward_csum(skb);
		// 调用LOCAL_OUT挂载点
		NF_HOOK(pf, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)-&gt;dev, dst_output);
	} else
		ret = NF_ACCEPT;
	return ret;
}</pre>
<p>在ip_vs_nat_xmit中，我们可以了解到，对于宿主机发起的针对ClusterIP:ServicePort的请求</p>
<ol>
<li>封包的目的地址被修改为Endpoint（通常是Pod，IPVS中的RS）的地址</li>
<li>修改后的封包，重新被塞入netfilter（内层），注意当前就正在netfilter（外层）中
<ol>
<li>外层钩子的返回值是NF_STOLEN：封包处理权转移给内层钩子，停止后续netfilter流程</li>
<li>内层钩子的返回值是NF_ACCEPT：不做IPVS相关处理，继续后续netfilter流程。IPVS前、后的LOCAL_OUT、POSTROUTING钩子都会正常执行。也就是说，对于修改后的封包，内核会进行完整、常规的netfilter处理，就像没有IPVS存在一样</li>
</ol>
</li>
</ol>
<p>到这里，我们确定了，IPVS会在LOCAL_OUT中进行DNAT。但是只有同时进行SNAT，才能解释上文的中的疑惑。</p>
<div class="blog_h2"><span class="graybg">SNAT</span></div>
<p>花费了不少时间在IPVS上探究后，我们意识到走错了方向。我们忘记了SANT是kube-proxy会去做的事情。查看一下iptables规则就一目了然了：</p>
<pre class="crayon-plain-tag"># iptables -t nat -L -n -v

Chain OUTPUT (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source        destination         
# 所有出站流量都要经过自定义的 KUBE-SERVICES 链
  21M 3825M KUBE-SERVICES  all  --  *  *   0.0.0.0/0         0.0.0.0/0            /* kubernetes service portals */

Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source        destination         
# 如果目的IP:PORT属于K8S服务，则调用KUBE-MARK-MASQ链
    0     0 KUBE-MARK-MASQ  all  --  * *  !172.27.0.0/16     0.0.0.0/0   match-set KUBE-CLUSTER-IP dst,dst

# 给封包打上标记 0x4000
Chain KUBE-MARK-MASQ (5 references)
 pkts bytes target     prot opt in     out     source         destination         
   98  5880 MARK       all  --  *       *  0.0.0.0/0          0.0.0.0/0            MARK or 0x4000

 
Chain POSTROUTING (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source         destination         
  44M 5256M KUBE-POSTROUTING  all  --  *  *       0.0.0.0/0   0.0.0.0/0            /* kubernetes postrouting rules */

Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source         destination         
# 仅仅处理 0x4000标记的封包
 1781  166K RETURN     all  --  *      *  0.0.0.0/0           0.0.0.0/0            mark match ! 0x4000/0x4000
# 执行SNAT     
   97  5820 MARK       all  --  *      *  0.0.0.0/0           0.0.0.0/0   MARK xor 0x4000
   97  5820 MASQUERADE  all  --  *     *  0.0.0.0/0           0.0.0.0/0   /* kubernetes service traffic requiring SNAT */</pre>
<p>由于ip_vs_local_request4挂钩在LOCAL_OUT，优先级为NF_IP_PRI_NAT_DST+2 ，因此它是发生在上面nat表OUTPUT链中MARK之后的。也就是说在IPVS处理之前，kube-proxy已经给原始的封包打上标记。</p>
<p>重入的、DNAT后的封包进入LOCAL_OUT，随后进入POSTROUTING。由于标记的缘故，封包被kube-proxy的规则SNAT。</p>
<p>经过POSTROUTING的封包，经过tcpdump，但是由于源、目的IP地址，以及目的端口都改变了，因而我们看到tcpdump没有任何输出。</p>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>这里做一下小结。</p>
<p>为什么IPVS模式下，能够ping通ClusterIP？ 这是因为IPVS模式下，ClusterIP被配置为宿主机上一张虚拟网卡kube-ipvs0的IP地址。</p>
<p>为什么IPVS模式下，宿主机端口被ClusterIP泄漏？每当添加一个ClusterIP给网络接口后，内核自动在local表中增加一条路由，此路由保证了针对ClusterIP的访问，在没有IPVS干涉的情况下，路由到本机处理。这样，在0.0.0.0上监听的进程，就接收到报文并处理。</p>
<p>为什么删除内核添加的路由后：</p>
<ol>
<li>宿主机上访问ClusterIP:NonServicePort不通了？因为没有路由了</li>
<li>没有路由了，为什么宿主机上访问ClusterIP:ServicePort仍然畅通？如上文分析，IPVS在ip_vs_nat_xmit中仍然会进行选路操作</li>
<li>那为什么从容器网络命名空间访问ClusterIP:ServicePort不通呢？IPVS处理本地、远程客户端的代码路径不一样。容器网络命名空间是远程客户端，需要首先进入PER_ROUTING，然后选路，路由目的地是本机，才会进入LOCAL_IN，IPVS才有介入的时机。由于路由被删掉了，选路那一步就会出问题</li>
</ol>
<p>为什么通过--match-set KUBE-CLUSTER-IP匹配目的地址，如果封包目的端口是NonServicePort则Reject：</p>
<ol>
<li>这种方案对容器命名空间有效？容器请求的源地址不会是ClusterIP，因此回程报文的目的地址不会因为匹配规则而Reject</li>
<li>这种方案导致宿主机无法访问ClusterIP？宿主机发起请求时用的是ClusterIP，请求端口是随机的。这种请求的回程报文必然匹配规则导致Reject</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>重温iptables</title>
		<link>https://blog.gmem.cc/iptables</link>
		<comments>https://blog.gmem.cc/iptables#comments</comments>
		<pubDate>Sat, 18 Apr 2020 11:07:23 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=31863</guid>
		<description><![CDATA[<p>工作流图 下面这张图描述了一个L3的IP封包如何通过iptables：  对于此图的说明： Iptables和内核路由的关系：执行完PREROUTING链之后，会进行路由表的查询 通过lo接口的封包，不走PREROUTING的DNAT表 出站封包在OUTPUT链之前就进行了路由处理。但是如果OUTPUT进行了DNAT，则会进行重新选路 入站封包，如果使用了隧道，则会经由PREROUTING - INPUT链逐层的解除隧道 出站封包，如果使用了隧道，则会经由OUTPUT -POSTROUTING链逐层的进行隧道封装 下面的图示意了更接近实际的流程： 基础 iptables是用户空间命令，通过netlink和内核的netfilter模块进行交互，在L3/L4操控封包。 规则链 netfilter是Linux网络安全大厦的基石，从2.4开始引入。它提供了一整套Hook函数机制，IP层的5个钩子点对应了iptables的5个内置链条： PREROUTING，在此DNAT POSTROUTING，在此SNAT INPUT，处理输入给本地进程的封包 OUTPUT，处理本地进程输出的封包 FORWARD，处理转发给其他机器、其他网络命名空间的封包 对于从网络接口入站的IP封包，首先进入PREROUTING链，然后进行路由判断： <a class="read-more" href="https://blog.gmem.cc/iptables">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/iptables">重温iptables</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">工作流图</span></div>
<p>下面这张图描述了一个<strong><span style="background-color: #c0c0c0;">L3</span></strong>的IP封包如何通过iptables：</p>
<p><img class="wp-image-27047 size-full aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2008/04/packet_flow10.png" alt="packet_flow10" width="726" height="1445" /></p>
<p> 对于此图的说明：</p>
<ol>
<li>Iptables和内核路由的关系：执行完<span style="background-color: #c0c0c0;"><strong>PREROUTING链之后，会进行路由表的查询</strong></span></li>
<li>通过<span style="background-color: #c0c0c0;"><strong>lo接口的封包，不走PREROUTING的DNAT表</strong></span></li>
<li>出站封包在<span style="background-color: #c0c0c0;"><strong>OUTPUT链之前就进行了路由处理</strong></span>。但是如果<span style="background-color: #c0c0c0;"><strong>OUTPUT进行了DNAT，则会进行重新选路</strong></span></li>
<li>入站封包，如果使用了隧道，则会经由PREROUTING - INPUT链<span style="background-color: #c0c0c0;"><strong>逐层的解除</strong></span>隧道</li>
<li>出站封包，如果使用了隧道，则会经由OUTPUT -POSTROUTING链<span style="background-color: #c0c0c0;"><strong>逐层的进行隧道封装</strong></span></li>
</ol>
<p>下面的图示意了更接近实际的流程：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/04/tables_traverse.jpg"><img class="size-full wp-image-31891 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/04/tables_traverse.jpg" alt="tables_traverse" width="647" height="1100" /></a></p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<p>iptables是用户空间命令，通过netlink和内核的netfilter模块进行交互，在L3/L4操控封包。</p>
<div class="blog_h2"><span class="graybg">规则链</span></div>
<p>netfilter是Linux网络安全大厦的基石，从2.4开始引入。它提供了一整套Hook函数机制，IP层的5个钩子点对应了iptables的5个内置链条：</p>
<ol>
<li>PREROUTING，在此DNAT</li>
<li>POSTROUTING，在此SNAT</li>
<li>INPUT，处理输入给本地进程的封包</li>
<li>OUTPUT，处理本地进程输出的封包</li>
<li>FORWARD，处理转发给其他机器、其他网络命名空间的封包</li>
</ol>
<p>对于从网络接口入站的IP封包，首先进入PREROUTING链，然后进行路由判断：</p>
<ol>
<li>如果封包路由目的地是本机，则进入INPUT链，然后发给本地进程</li>
<li>如果封包路由目的地不是本机，并且启用了IP转发，则进入FORWARD链，然后通过POSTROUTING链，最后经过网络接口发走</li>
</ol>
<p>对于本地进程发往协议栈的封包，则首先通过OUTPUT链，然后通过POSTROUTING链，最后经过网络接口发走</p>
<div class="blog_h2"><span class="graybg">表</span></div>
<p>除了链条，iptables还包含一个正交的概念，表是用于<span style="background-color: #c0c0c0;">分类管理iptables规则</span>的。不同的表，通常用作不同的目的：</p>
<ol>
<li>filter表：用于控制到达某条链条上的数据包如何处理，是放行（Accept）、丢弃（Drop）还是拒绝（Reject）。如果不指定-t参数，这是<span style="background-color: #c0c0c0;">默认操控的表格</span></li>
<li>nat表：用于修改源地址，或者目的地址。该表格<strong><span style="background-color: #c0c0c0;">在某个创建了新连接的封包出现时生效</span></strong></li>
<li>mangle表：该表格用于修改封包的内容</li>
<li>raw表：具有<span style="background-color: #c0c0c0;"><strong>高优先级</strong></span>，可以对接收到的封包在连接跟踪（Connection Tracking，为了支持NAT，iptables会对每个L4连接进行跟踪，也就是维护每个L4连接的状态）前进行处理（也就是取消跟踪），可以应用在那些不需要做NAT的情况下提高性能。例如高访问的Web服务，可以不让iptables做80端口封包的连接跟踪</li>
<li>security表：用于强制访问控制（Mandatory Access Control），很少使用</li>
</ol>
<div class="blog_h3"><span class="graybg">表的优先级</span></div>
<p>表的优先级从高到低为 raw ⇨ mangle ⇨ nat ⇨ filter ⇨ security。<span style="background-color: #c0c0c0;">用户不能自定义新表</span>。</p>
<p>封包会<strong><span style="background-color: #c0c0c0;">依次经过相关的链，在每个链中，会根据表的优先级，依次遍历各个表中的规则</span></strong>。任何表中的规则都有机会拒绝封包。</p>
<div class="blog_h3"><span class="graybg">表和链的关系</span></div>
<p>需要注意，<span style="background-color: #c0c0c0;"><strong>不是任何链条上可以挂任何表</strong></span>：</p>
<ol>
<li>raw可以挂在PREROUTING、OUTPUT</li>
<li>mangle可以挂在任何链上</li>
<li>nat（SNAT）可以挂在POSTROUTING、INPUT</li>
<li>nat（DNAT）可以挂在PREROUTING、OUTPUT</li>
<li>filter可以挂在FORWARD、INPUT、OUTPUT</li>
<li>security可以挂在FORWARD、INPUT、OUTPUT</li>
</ol>
<div class="blog_h3"><span class="graybg">mangle表</span></div>
<p>用于修改封包。<span style="background-color: #c0c0c0;">某些目标只能用在mangle表</span>中，包括：</p>
<ol>
<li>TOS，此目标设置封包的Type Of Servier字段，以便影响封包的处理策略，例如如何进行路由</li>
<li>TTL，设置封包的Time To Live字段</li>
<li>MARK，给封包添加一个标记值（数字），iproute2能够识别此mark并且进行特殊的路由处理。此外基于MARK还可以进行带宽限制、基于Class的排队</li>
</ol>
<div class="blog_h3"><span class="graybg">nat表</span></div>
<p>应当仅仅用于网络地址转换。也就是使用以下目标：</p>
<ol>
<li>DNAT：用在你有一个公共地址，别人访问此地址时，你需要将访问重定向到防火墙背后的某个服务时</li>
<li>SNAT：允许隐藏在防火墙背后的内网机器访问外部网络</li>
<li>MASQUERADE：类似SNAT，不同之处在于每次都需要动态计算使用什么作为转换后的源地址。用在主机地址不固定的情况下</li>
<li>REDIRECT：类似于DNAT，但是<span style="background-color: #c0c0c0;">新的目的地址被锁定为接收封包的那个网卡地址，同时端口改为随机的或指定的值</span></li>
</ol>
<p>上面这些目标都会改变IP封包的首部。</p>
<div class="blog_h3"><span class="graybg">filter表</span></div>
<p>用于过滤封包，使用ACCEPT、DROP之类的目标</p>
<div class="blog_h2"><span class="graybg">规则</span></div>
<p>表中存放的是一个个规则，规则由匹配+目标组成。目标包括：</p>
<ol>
<li>ACCEPT，允许封包通过，如果<span style="background-color: #c0c0c0;">在子链ACCEPT，则相当于在父链也ACCEPT了</span>，不会继续遍历父链后续规则</li>
<li>RETURN，从当前链返回，行为类似于编程语言函数中的return。如果当前：
<ol>
<li>位于子规则链，则返回到父链调用子链的那个规则的下一条规则处执行</li>
<li>不位于子规则链，则执行默认策略</li>
</ol>
</li>
<li>DROP，直接丢弃数据包，不进行后续处理</li>
<li>REJECT，返回connection refused或者destination unreachable报文</li>
<li>QUEUE，将数据包放入用户空间的队列，供用户空间应用程序处理</li>
<li>JUMP，跳转到用户自定义链继续执行</li>
<li>LOG 让内核记录匹配的封包</li>
<li>AUDIT 对封包进行审计</li>
<li>DNAT 目标地址映射</li>
<li>SNAT 源地址映射</li>
<li>MASQUERADE 源地址映射，用于本机地址是动态获取的情况下</li>
</ol>
<p>通常情况下，一旦匹配了某个规则，就会执行它的目标。同时，<span style="background-color: #c0c0c0;"><strong>不再检查同规则链、同表的后续规则，这意味着规则的顺序非常重要</strong></span>。某些目标是<span style="background-color: #c0c0c0;"><strong>例外，例如LOG、MARK，会继续遍历</strong></span>后面的规则</p>
<p>如果内置规则链<span style="background-color: #c0c0c0;"><strong>执行到结尾</strong><strong>，或者内置规则链中的某个目标是RETURN</strong></span>的规则匹配封包，那么<a href="#iptables-chain-policy">规则链策略（Chain Policy）</a>中定义的目标将决定如何处理当前封包。</p>
<div class="blog_h2"><span class="graybg">封包遍历步骤</span></div>
<div class="blog_h3"><span class="graybg">目的地是本机的</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 46px; text-align: center;">步骤</td>
<td style="width: 80px; text-align: center;">表</td>
<td style="width: 120px; text-align: center;">链</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td> </td>
<td> </td>
<td>封包在网络上</td>
</tr>
<tr>
<td>2</td>
<td> </td>
<td> </td>
<td>封包进入本机网卡</td>
</tr>
<tr>
<td>3</td>
<td>mangle</td>
<td>PREROUTING</td>
<td>在此修改封包，例如改变TOS。连接跟踪发生在这个链上 </td>
</tr>
<tr>
<td>4</td>
<td>nat</td>
<td>PREROUTING</td>
<td>主要用于DNAT。避免在此进行封包过滤，某些情况下可能被bypass </td>
</tr>
<tr>
<td>5</td>
<td> </td>
<td> </td>
<td>路由决策：封包目的地是本机，还是需要被转发，转发到哪里 </td>
</tr>
<tr>
<td>6</td>
<td>mangle</td>
<td>INPUT</td>
<td>路由决策之后，发送到本机进程之前修改封包</td>
</tr>
<tr>
<td>7</td>
<td>filter</td>
<td>INPUT</td>
<td>针对所有目的地是本地的封包进行过滤，不管它来自什么网络接口 </td>
</tr>
<tr>
<td>8</td>
<td> </td>
<td> </td>
<td>封包到达本地进程</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">从本机发起的</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 46px; text-align: center;">步骤</td>
<td style="width: 80px; text-align: center;">表</td>
<td style="width: 120px; text-align: center;">链</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td> 1</td>
<td> </td>
<td> </td>
<td>本地进程发送封包</td>
</tr>
<tr>
<td>2</td>
<td> </td>
<td> </td>
<td><span style="background-color: #c0c0c0;">路由决策：使用什么源地址，什么出口网卡</span>，收集一些其它信息</td>
</tr>
<tr>
<td>3</td>
<td>mangle</td>
<td>OUTPUT</td>
<td>修改封包。不要在此进行封包过滤，因为具有副作用。本地生成的连接跟踪也在此进行</td>
</tr>
<tr>
<td>4</td>
<td>nat</td>
<td>OUTPUT</td>
<td>对出站封包进行NAT</td>
</tr>
<tr>
<td>5</td>
<td> </td>
<td> </td>
<td><span style="background-color: #c0c0c0;">再次路由决策</span>，因为mangle或nat可能导致需要重新路由</td>
</tr>
<tr>
<td>6</td>
<td>filter</td>
<td>OUTPUT</td>
<td>对本机发出的封包进行过滤</td>
</tr>
<tr>
<td>7</td>
<td>mangle</td>
<td>POSTROUTING</td>
<td>在路由决策之后，发送出网卡之前，修改封包</td>
</tr>
<tr>
<td>8</td>
<td>nat</td>
<td>POSTROUTING</td>
<td>在此进行SNAT。不要在此进行封包过滤，即使默认策略设置为DROP也可能导致某些封包溜走</td>
</tr>
<tr>
<td>9</td>
<td> </td>
<td> </td>
<td>到达网卡</td>
</tr>
<tr>
<td>10</td>
<td> </td>
<td> </td>
<td>进入网线</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">需要转发的</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 46px; text-align: center;">步骤</td>
<td style="width: 80px; text-align: center;">表</td>
<td style="width: 120px; text-align: center;">链</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td> </td>
<td> </td>
<td>封包在网线上</td>
</tr>
<tr>
<td>2</td>
<td> </td>
<td> </td>
<td>封包到达入口网卡</td>
</tr>
<tr>
<td>3</td>
<td>mangle</td>
<td>PREROUTING</td>
<td>修改封包</td>
</tr>
<tr>
<td>4</td>
<td>nat</td>
<td>PREROUTING</td>
<td>进行DNAT</td>
</tr>
<tr>
<td>5</td>
<td> </td>
<td> </td>
<td>路由决策</td>
</tr>
<tr>
<td>6</td>
<td>mangle</td>
<td>FORWARD</td>
<td>仅用于特殊场景下，在<span style="background-color: #c0c0c0;">初始路由决策之后</span>修改封包</td>
</tr>
<tr>
<td>7</td>
<td>filter</td>
<td>FORWARD</td>
<td>对转发封包进行过滤</td>
</tr>
<tr>
<td>8</td>
<td> </td>
<td> </td>
<td>最终路由决策</td>
</tr>
<tr>
<td>9</td>
<td>mangle</td>
<td>POSTROUTING</td>
<td>所有路由决策完成之后，修改封包</td>
</tr>
<tr>
<td>10</td>
<td>nat</td>
<td>POSTROUTING</td>
<td>进行SNAT</td>
</tr>
<tr>
<td>11</td>
<td> </td>
<td> </td>
<td>到达出口网卡</td>
</tr>
<tr>
<td>12</td>
<td> </td>
<td> </td>
<td>进入网线</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">连接跟踪状态机</span></div>
<p>连接跟踪（Connection tracking ）的目的是，让Netfilter框架能够知晓每个连接的状态。你可以使用<pre class="crayon-plain-tag">--state</pre>来匹配状态。</p>
<p>注意，为了更加准确的跟踪TCP连接，内核还维护一系列的内部状态。这些内部状态不能用来匹配。</p>
<p>在iptables中，每个<strong><span style="background-color: #c0c0c0;">封包都可能和一个被跟踪的连接相关联</span></strong>。连接可以处于4种不同的状态：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NEW</td>
<td>
<p>说明看到的封包是连接的第一个封包。导致此状态的例子：</p>
<ol>
<li>TCP协议的SYN包</li>
</ol>
</td>
</tr>
<tr>
<td>ESTABLISHED</td>
<td>
<p>当双向封包都监测到之后，进入此状态。导致此状态的例子：</p>
<ol>
<li>TCP协议的应答SYN/ACK</li>
</ol>
</td>
</tr>
<tr>
<td>RELATED</td>
<td>
<p>如果连接和另外一个ESTABLISHED状态的连接相关，则它进入此状态</p>
<p>如果主连接产生一个新连接，这个新连接就是RELATED的。例如FTP数据连接是相关于FTP控制连接的 </p>
</td>
</tr>
<tr>
<td>INVALID</td>
<td>无法识别的状态，可能由于系统内存耗尽、不响应已知连接的ICMP报文导致 </td>
</tr>
</tbody>
</table>
<p>在内核中，连接跟踪由conntrack模块负责。</p>
<p>除了<strong><span style="background-color: #c0c0c0;">本地生成的包，在OUTPUT中跟踪</span></strong>之外，所有<span style="background-color: #c0c0c0;"><strong>连接跟踪都在PREROUTING链中进行</strong></span>。例如：</p>
<ol>
<li>本地发起一个连接的初始封包，则在OUTPUT链中，连接变为NEW</li>
<li>当接收到上述封包的应答包后，在PREROUTING链中，连接变为ESTABLISHED</li>
</ol>
<p>如果外部发起初始封包，则NEW状态是在PREROUTING中产生的。</p>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">iptables [-t table] {-A|-C|-D} chain rule-specification
ip6tables [-t table] {-A|-C|-D} chain rule-specification
iptables [-t table] -I chain [rulenum] rule-specification
iptables [-t table] -R chain rulenum rule-specification
iptables [-t table] -D chain rulenum
iptables [-t table] -S [chain [rulenum]]
iptables [-t table] {-F|-L|-Z} [chain [rulenum]] [options...]
iptables [-t table] -N chain
iptables [-t table] -X [chain]
iptables [-t table] -P chain target
iptables [-t table] -E old-chain-name new-chain-name

其中：
rule-specification = [matches...] [target]
match = -m matchname [per-match-options]
target = -j targetname [per-target-options]</pre>
<div class="blog_h2"><span class="graybg">输出</span></div>
<p>带-n参数时，0.0.0.0/0表示匹配任何地址（anywhere）<br />不带-n参数时，所有保留私有地址都会显示为bogon</p>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p>选项被分为若干组：</p>
<div class="blog_h3"><span class="graybg">指定表</span></div>
<p>-t --table table 指定命令操控的表格的名称，如果内核被配置为自动模块加载方式，并且相应模块不存在，将尝试自动加载适当的模块</p>
<div class="blog_h3"><span class="graybg">子命令</span></div>
<p>该组选项指定期望执行的动作，仅其中一个可以在命令行中指定<br />-A　--append chain rule-specification 附加一个或者多个规则到目标规则链。如果源/目标名称解析到多于一个地址，那么规则将会添加到每个可能的地址组合<br />-C　--check chain rule-specification 检查目标规则链是否具有匹配的规则，该命令与-D的逻辑类似，但是不会修改当前的iptables配置，使用其退出码来指示是否成功<br />-D　--delete chain rule-specification 从目标规则链中删除一个或者多个规则，需要指定规则的匹配规则<br />-D　--delete chain rulenum 从目标规则链中删除一个或者多个规则，需要指定从1开始的个数<br />-I　--insert chain [rulenum] rule-specification 插入一个或者多个规则到目标规则链的指定位置，如果rulenum是1则插到最前面，举例：<pre class="crayon-plain-tag">-I FORWARD 1</pre> <br />-R　--replace chain rulenum rule-specification 替换目标规则链中的一个或者多个规则。如果源/目标名称解析到多个地址，该命令将失败<br />-L　--list [chain] 列出选中规则链中的所有规则，如果不指定规则链，则列出全部规则链，与其他命令不同，该命令仅应用到特定的表格，因此如果想列出NAT表的全部规则，需要使用：iptables -t nat -n -L。该命令一般与-n联用，以避免耗时的DNS查找，指定-Z选项也是合法的<br />-S　--list-rules [chain] 打印选定规则链中的所有规则，该命令仅应用到特定的表格<br />-F　--flush [chain] 刷空选定规则链中的所有规则（如果不指定规则链，则应用到表格中全部的规则链），等价于一条条删除规则<br />-Z　--zero [chain [rulenum]] 将封包、字节计数器置零<br />-N　--new-chain chain 创建一个用户自定义的规则链，名称为chain，chain必须不是任何已经存在的target<br />-X　--delete-chain [chain] 删除可选的、用户定义的规则链<br />-P　--policy chain target 设置<strong><span style="background-color: #c0c0c0;">某个规则链的策略（没有规则匹配封包时的默认行为）</span></strong>为target。仅内置规则链可以设置策略，策略的目标不得是任何规则链，举例：</p>
<pre class="crayon-plain-tag">iptables --policy INPUT DROP  # 设置INPUT的规则链策略为DROP</pre>
<p>-E　--rename-chain old-chain new-chain 重命名用户定义的规则链</p>
<div class="blog_h3"><span class="graybg">参数</span></div>
<p>以下的参数用来组成一个rule specification<br />[!]　-p, --protocol protocol 用来检查的规则或者封包的协议。支持的值包括：tcp, udp, udplite, icmp, icmpv6,esp, ah, sctp, all。<span style="background-color: #c0c0c0;">叹号用来反转测试</span>，默认all<br />[!]　-s, --source address[/mask][,...] 封包来源规则，address可以是网络名、主机名、IP地址（附加/掩码）、或者普通IP地址。主机名仅仅会在规则提交到内核前解析一次。叹号用来反转地址规则。指定多个地址是可以的，但是会被分解为多个规则，或者导致多个规则被删除（-D）<br />[!]　-d, --destination address[/mask][,...] 封包目标规则，类似来源规则<br />-m　--match match 指定使用的match，所谓match是用来<span style="background-color: #c0c0c0;">测试特定属性的扩展模块</span><br />-j　--jump target 指定规则的目标，即：当规则匹配的时候要做什么。目标可以是用户定义的规则链、一个<span style="background-color: #c0c0c0;">特殊的内置目标（立即决定封包命运）</span>、或者一个扩展。如果某个规则<span style="background-color: #c0c0c0;">不设置该选项，那么匹配后将对封包不构成任何影响，但是规则的计数器会增加</span><br />-g　--goto chain 仅仅用在用户定义的规则链中，指示重定向到另外一个链继续处理。和-j不同，<span style="background-color: #c0c0c0;">-j后子链完成匹配后会跳转到父链，-g则不会跳转</span>，如果子链没有匹配的规则则<span style="background-color: #c0c0c0;">按子链的默认策略处理</span><br />[!]　-i, --in-interface name 仅用于进入INPUT, FORWARD, PREROUTING链的封包。限制接收到封包的网络接口名称，如果名称后面跟着+号，所有以该名称开头的接口都被匹配<br />[!]　-o, --out-interface name 仅用于进入FORWARD, OUTPUT, POSTROUTING链的封包。限制封包将被送出的网络接口名称，如果名称后面跟着+号，所有以该名称开头的接口都被匹配<br />[!]　-f, --fragment 对于分片的IP数据报，该规则将仅仅用于第二个或者以后的的分片。仅用于IPv4 　<br />-c　--set-counters packets bytes 在INSERT, APPEND, REPLACE规则时，用于初始化封包、字节计数器</p>
<div class="blog_h3"><span class="graybg">其他选项</span></div>
<p>-v　--verbose 冗长输出，list命令将输出接口名称、规则选项、TOS掩码、包以及字节计数器。对于附加、插入、删除、替换规则的操作，该选项导致规则的详细信息被打印<br />-w　--wait 等待xtables锁被解开，为了防止多个进程同时修改，iptables具有锁定机制<br />-n　--numeric 数字化输出，IP地址和端口将使用数字格式<br />-x　--exact 仅与-L命令有关，显示精确的封包、字节计数，使用1024而不是1000作为倍率<br />--line-numbers 列出规则时，显示行号<br />--modprobe=command 当添加、删除规则时，使用命令command加载必要的模块（目标、匹配-m扩展等）</p>
<div class="blog_h2"><span class="graybg">应用举例</span></div>
<div class="blog_h3"><span class="graybg">透明代理</span></div>
<pre class="crayon-plain-tag"># 发送任何目的端口为25的封包给在127.0.0.1:10025上监听的进程
# 并且给封包以标记 1 
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY \
  --tproxy-mark 0x1/0x1 --on-port 10025 --on-ip 127.0.0.1


# PREROUTING链之后，转发之前，检查路由表
# 如果发现封包被标记为 1，那么查找100号表
ip rule add fwmark 1 lookup 100

# 100号路由表，将封包通过lo发出。注意出口为lo的封包不会真正路由，而是交给本地进程处理
ip route add local 0.0.0.0/0 dev lo table 100</pre>
<div class="blog_h3"><span class="graybg">网关</span></div>
<pre class="crayon-plain-tag"># 对于准备从wlan0出去的封包，进行源地址映射
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

# 允许转发来自eth0、发往wlan0的封包
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT

# 对于来自wlan0、发往wlan0的封包，如果状态为RELATED,ESTABLISHED则允许通过
# ESTABLISHED，封包属于一个已经建立的连接
# RELATED，封包创建了新连接，但是此连接和一个已经存在的连接相关，例如FTP数据传输器、ICMP错误
iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT</pre>
<div class="blog_h3"><span class="graybg">防火墙</span></div>
<pre class="crayon-plain-tag"># 列出filter表（可用于实现防火墙）内容。注意默认网卡信息不显示，需要加 -v
iptables -t filter -L -n

# 禁止向目标地址发送网络包
iptables -A OUTPUT -j DROP -d 10.5.39.223
# 撤销
iptables -D OUTPUT -j DROP -d 10.5.39.223

# 禁止PING
iptables -I INPUT -p icmp --icmp-type 8 -j DROP
# 撤销
iptables -D INPUT -p icmp --icmp-type 8 -j DROP

# 禁止向目标网络转发包
iptables -A FORWARD -d 172.27.0.0/16 -j DROP
iptables -D FORWARD -d 172.27.0.0/16 -j DROP

# 取消Docker的网络隔离
iptables -I DOCKER-USER 1 -j ACCEPT
# 或者                                 两个Docker网络的网段
iptables -t filter -I DOCKER-USER   -s 172.17.0.0/16 -j ACCEPT
iptables -t filter -I DOCKER-USER   -s 172.21.0.0/16 -j ACCEPT</pre>
<div class="blog_h3"><span class="graybg">优化输出</span></div>
<pre class="crayon-plain-tag"># 显示Filter表的详细信息，格式化输出
sudo iptables -L -nv -t filter | column -t -s " "</pre>
<div class="blog_h3"><span class="graybg">源地址映射</span></div>
<pre class="crayon-plain-tag"># 作为VPN服务器的主机，启用ipv4转发
iptables -t nat -L -v -n
# 输出如下：
# ...
# Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
#  pkts bytes target     prot opt in     out     source               destination
# 66404 4468K MASQUERADE  all  --  any    any     anywhere             anywhere

# 删除既有的IP掩蔽设置，针对所有封包做了端口映射
iptables -t nat -D POSTROUTING -j MASQUERADE
# 这时再连接到gmem.cc的VPN，发现无法上外网了
# 原因是，没有合适的端口映射，gmem.cc无法将封包发回给VPN客户端

# 修改后的IP掩蔽配置，限制了源IP地址的范围、出口网卡
iptables -t nat -A POSTROUTING -m iprange --src-range 172.21.0.100-172.21.0.120 -o eth0 -j MASQUERADE</pre>
<div class="blog_h3"><span class="graybg">目的地址映射</span></div>
<pre class="crayon-plain-tag"># 把针对本机192.168.0.89的7777端口的请求全部转换为对虚拟机172.16.87.132的80端口的请求
sudo iptables -t nat -A PREROUTING         # 入站后第一个链
              -p tcp --dport 7777 -i eth0  # 如果封包来自eth0且目标端口为7777
              -j DNAT --to-destination 172.16.87.132:80 # 那么修改目标地址为虚拟机的80端口

# 此时，针对192.168.0.89:7777的封包，目的地址已经改为172.16.87.132
# 允许对目的地址为172.16.87.132的封包进行转发，可能发生的情况是：
# 原始数据报 
# 192.168.0.200:23387 =&gt; 192.168.0.89:7777
# 入站后，目标地址被映射：
# 192.168.0.200：23387 =&gt; 172.16.87.132：80
# 转发：由于目的地址不是本机，因此进行转发，ipv4转发功能已经开启过
# 出站前，源地址被映射被映射：
# 172.16.87.1:39480 =&gt; 172.16.87.132：80 
# 这就为本机39480端口和192.168.0.200：23387建立了映射关系
sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j MASQUERADE
# 上面的命令也可以替换为：
sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j SNAT --to-source 172.16.87.1
# 返回的数据报，处理时，自动逆向映射：原先的目标映射用于逆向源映射，原先的源映射用于逆向目标映射
# 原始数据报
# 172.16.87.132：80 =&gt; 172.16.87.1:39480
# 入站后，目标地址被映射
# 172.16.87.132：80 =&gt; 192.168.0.200：23387
# 出站前，源地址被映射
# 192.168.0.89:7777 =&gt; 192.168.0.200：23387

# ✭✭✭ 一个重要的原则是，仅仅去匹配初始发起连接的包，做好NAT后，返回的包自动映射</pre>
<div class="blog_h1"><span class="graybg">编写规则</span></div>
<p>很多iptables命令都要求提供一个规则的规格（rule-specification，后面简称规则）。规则实际上就是一系列iptables命令选项，大部分选项都用来说明如何匹配封包的，<pre class="crayon-plain-tag">-j</pre>用于指定目标。</p>
<div class="blog_h2"><span class="graybg">添加注释</span></div>
<p>要为规则添加注释，参考：</p>
<pre class="crayon-plain-tag">iptables -m comment --comment "comment here"
iptables -A INPUT -i eth1 -m comment --comment "my LAN - " -j DROP</pre>
<p>可以看到，注释以匹配扩展的形式被支持。</p>
<p>注释最多256字符，执行<pre class="crayon-plain-tag">iptables -L</pre>命令时会以<pre class="crayon-plain-tag">/* */</pre>打印在规则尾部。 </p>
<div class="blog_h2"><span class="graybg">一般匹配</span></div>
<p>这些匹配选项的值，前面都可以加<pre class="crayon-plain-tag">!</pre>表示<span style="background-color: #c0c0c0;">反向匹配</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-p, --protocol</td>
<td>匹配协议，可选值 TCP, UDP, ICMP等</td>
</tr>
<tr>
<td>-s, --src, --source</td>
<td>匹配源地址。地址形式192.168.0.0/24</td>
</tr>
<tr>
<td>-d, --dst, --destination</td>
<td>匹配目的地址。地址形式192.168.0.0/24</td>
</tr>
<tr>
<td>-i, --in-interface</td>
<td>匹配入站网络接口，仅用于INPUT, FORWARD, PREROUTING链。eth+表示匹配任何以太网卡</td>
</tr>
<tr>
<td>-o, --out-interface</td>
<td>匹配出站网络接口，OUTPUT, FORWARD, POSTROUTING链。eth+表示匹配任何以太网卡</td>
</tr>
<tr>
<td>-f, --fragment</td>
<td>用于匹配分片的封包的非首分片。取反需要<pre class="crayon-plain-tag">! -f</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">隐式匹配</span></div>
<div class="blog_h3"><span class="graybg">用于TCP的匹配</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--sport, --source-port</td>
<td>源端口。支持端口范围：<pre class="crayon-plain-tag">--source-port 22:80</pre></td>
</tr>
<tr>
<td>--dport, --destination-port</td>
<td>目的端口。支持端口范围</td>
</tr>
<tr>
<td>--tcp-flags</td>
<td>匹配TCP标记，可用的标记SYN, ACK, FIN, RST, URG, PSH，还有两个特殊的值ALL, NONE分别匹配具有任何标记的封包、没有任何标记的封包</td>
</tr>
<tr>
<td>--syn</td>
<td>匹配设置了SYN位，但是没有设置ACK,RST位的封包</td>
</tr>
<tr>
<td>--tcp-option</td>
<td>基于TCP选项来匹配</td>
</tr>
</tbody>
</table>
<p>举例：</p>
<pre class="crayon-plain-tag"># 匹配SYN标记被设置，ACK,FIN,RST没有被设置的封包
iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST SYN</pre>
<div class="blog_h3"><span class="graybg">用于UDP的匹配</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--sport, --source-port</td>
<td>源端口匹配</td>
</tr>
<tr>
<td>--dport, --destination-port</td>
<td>目的端口匹配</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">用于ICMP的匹配</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--icmp-type</td>
<td>匹配ICMP报文类型</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">显式匹配</span></div>
<p>所谓显式匹配，是指必须明确通过<pre class="crayon-plain-tag">-m</pre>或<pre class="crayon-plain-tag">--match</pre>选项加载的匹配。</p>
<div class="blog_h3"><span class="graybg">conntrack</span></div>
<p>内核中基于netfilter框架实现的连接跟踪模块即conntrack。在DNAT时，conntrack使用状态机来跟踪连接的状态，记住目的地址被从什么改成了什么，这样才能将回程包的源地址改回来</p>
<p>基于连接跟踪的匹配。使用<pre class="crayon-plain-tag">-m conntrack</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--ctstate</td>
<td>
<p>匹配封包状态，可选值：</p>
<p>INVALID  匹配无法识别或没有任何状态的数据报<br />ESTABLISED  匹配连接请求的响应，以及后续的包。此时conntrack知道封包属于哪个连接<br />NEW  匹配连接的第一个包，conntrack此时对连接一无所知，通常发生在SYN时<br />RELATED 当一个连接和另外一个ESTABLISHED状态的连接相关时，则前一个连接为此状态<br />SNAT 已被SNAT<br />DNAT 已被DNAT</p>
<p>要匹配多个状态，用逗号分隔：<pre class="crayon-plain-tag">-m conntrack --ctstate ESTABLISHED,RELATED</pre></p>
<p>取反需要在匹配模块名之后：<pre class="crayon-plain-tag">-m conntrack ! --ctstate ESTABLISHED,RELATED</pre></p>
</td>
</tr>
<tr>
<td>--ctproto</td>
<td>匹配封包协议。示例<pre class="crayon-plain-tag">-m conntrack ! --ctproto TCP</pre></td>
</tr>
<tr>
<td>--ctorigsrc</td>
<td>匹配封包源地址。示例<pre class="crayon-plain-tag">-m conntrack --ctorigsrc 192.168.0.0/24</pre></td>
</tr>
<tr>
<td>--ctorigdst</td>
<td>匹配封包目的地址</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">iprange</span></div>
<p>使用IP地址范围进行匹配。使用<pre class="crayon-plain-tag">-m iprange</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--src-range</td>
<td>源地址范围。示例<pre class="crayon-plain-tag">-m iprange --src-range 192.168.1.13-192.168.2.19</pre></td>
</tr>
<tr>
<td>--dst-range</td>
<td>目的地址范围。示例<pre class="crayon-plain-tag">-m iprange --dst-range 192.168.1.13-192.168.2.19</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">length</span></div>
<p>第三层载荷匹配。该扩展用于根据网络层载荷，即传输层封包的长度进行匹配</p>
<p>用法：<pre class="crayon-plain-tag">-m length [!] --length length[:length] TCP/UDP包的长度</pre></p>
<div class="blog_h3"><span class="graybg">limit</span></div>
<p>可以用于限制特定规则的日志记录的频度。使用<pre class="crayon-plain-tag">-m limit</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--limit</td>
<td>
<p>指定平均匹配速率。如果指定单位时间内匹配的封包超过数量，则不再匹配</p>
<p>示例：<pre class="crayon-plain-tag">-m limit --limit 3/hour</pre>。每小时最多匹配3次</p>
<p>可用单位：/second /minute /hour /day</p>
</td>
</tr>
<tr>
<td>--limit-burst</td>
<td>指定最大匹配速率</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">mac</span></div>
<p>根据源地址的MAC地址进行匹配，只能用在PREROUTING, FORWARD,INPUT 规则链。</p>
<p>示例： <pre class="crayon-plain-tag">-m mac --mac-source 00:00:00:00:00:01</pre></p>
<div class="blog_h3"><span class="graybg">mark</span></div>
<p>根据关联当前封包的netfilter标记进行匹配。<span style="background-color: #c0c0c0;">mark是一个特殊字段，无符号整数（unsigned int），仅仅在内核中维护（<strong>不会嵌入在封包里</strong>）</span>。封包在通过主机时，标记可以和它关联。</p>
<p>很多内核例程都会使用mark，从而完成流量塑形（Traffic Shaping）、过滤等工作。</p>
<p>要设置一个mark，可以使用iptables的MARK目标。由于以前在ipchains的FWMARK目标中设置，现在<span style="background-color: #c0c0c0;">仍然会使用FWMARK这个术语</span>。</p>
<p>示例：<pre class="crayon-plain-tag">-m mark --mark 1986</pre>，匹配具有1986标记的封包。你还可以指定标记的掩码，也就是<pre class="crayon-plain-tag">-m mark --mark 1/1</pre>这种形式，这样 / 后面的数字，会与netfilter标记先进行逻辑与，得到的值再和  / 前面的值进行比较 —— 这可以仅仅关注fwmark的某个位</p>
<div class="blog_h3"><span class="graybg">multiport</span></div>
<p>多端口匹配。该扩展只能用于以下协议：tcp, udp, udplite, dccp, sctp。最多指定15个端口</p>
<p>指定端口的语法：<pre class="crayon-plain-tag">port[,port|,port:port]</pre></p>
<p>使用<pre class="crayon-plain-tag">-m multiport</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--source-port</td>
<td>根据源端口匹配，示例<pre class="crayon-plain-tag">-m multiport --source-port 22,53,80,110</pre></td>
</tr>
<tr>
<td>--destination-port</td>
<td>根据目的端口匹配</td>
</tr>
<tr>
<td>--port</td>
<td>根据源或目的端口匹配</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">owner</span></div>
<p>根据创建封包的进程的身份来匹配封包，很明显，只能用于本机产生的封包上，你只能在OUTPUT链中使用该匹配。</p>
<p>使用<pre class="crayon-plain-tag">-m owner</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--uid-owner</td>
<td>
<p>如果封包是由指定的User ID创建，则匹配</p>
<p>可能的应用场景：仅仅允许Apache通过80端口发出数据</p>
</td>
</tr>
<tr>
<td>--gid-owner</td>
<td>如果封包是由指定的Group ID中的用户创建，则匹配</td>
</tr>
<tr>
<td>--pid-owner</td>
<td>如果创建封包的进程具有指定的PID，则匹配</td>
</tr>
<tr>
<td>--sid-owner</td>
<td>如果创建封包的进程具有指定的Session，则匹配</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">pkttype</span></div>
<p>根据链路层类型匹配。用法 <pre class="crayon-plain-tag">-m pkttype [!] --pkt-type {unicast|broadcast|multicast}</pre></p>
<div class="blog_h3"><span class="graybg">state</span></div>
<p>和内核中的连接跟踪代码配合工作。此匹配会从连接跟踪状态机中读取封包的状态。</p>
<p>示例：<pre class="crayon-plain-tag">-m state --state RELATED,ESTABLISHED</pre></p>
<div class="blog_h3"><span class="graybg">tcpmss</span></div>
<p>根据TCP的Maximum Segment Size来匹配，仅仅针对SYN或SYN/ACK包。</p>
<p>示例：<pre class="crayon-plain-tag">iptables -A INPUT -p tcp --tcp-flags SYN,ACK,RST SYN -m tcpmss --mss 2000:2500</pre></p>
<div class="blog_h3"><span class="graybg">tos</span></div>
<p>根据Type Of Service来匹配。示例：<pre class="crayon-plain-tag">iptables -A INPUT -p tcp -m tos --tos 0x16</pre></p>
<div class="blog_h3"><span class="graybg">ttl</span></div>
<p>根据Time To Live来匹配。示例：<pre class="crayon-plain-tag">iptables -A OUTPUT -m ttl --ttl 60</pre></p>
<div class="blog_h3"><span class="graybg">addrtype</span></div>
<p>通过封包的地址类型进行匹配，可用的地址类型包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">地址类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>UNSPEC</td>
<td>未指定的地址，例如0.0.0.0</td>
</tr>
<tr>
<td>UNICAST</td>
<td>单播地址</td>
</tr>
<tr>
<td>LOCAL</td>
<td>本地地址（任何分配到任一本地网络接口的地址，包括loopback地址、ipvs虚拟地址） </td>
</tr>
<tr>
<td>BROADCAST</td>
<td>广播地址</td>
</tr>
<tr>
<td>ANYCAST</td>
<td>选播地址</td>
</tr>
<tr>
<td>MULTICAST</td>
<td>黑洞地址</td>
</tr>
<tr>
<td>UNREACHABLE</td>
<td>不可达地址</td>
</tr>
<tr>
<td>PROHIBIT</td>
<td>禁止地址</td>
</tr>
</tbody>
</table>
<p>使用<pre class="crayon-plain-tag">-m addrtype</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>[!] --src-type type</td>
<td>
<p>根据源地址类型匹配</p>
<p>地址类型：</p>
<p style="padding-left: 30px;">LOCAL：不是指loopback地址，而是指<span style="background-color: #c0c0c0;">任何分配给本机某个网络接口的IP地址</span>（包括loopback地址、ipvs虚拟地址）</p>
</td>
</tr>
<tr>
<td>[!] --dst-type type</td>
<td>根据目标地址类型匹配</td>
</tr>
<tr>
<td>--limit-iface-in</td>
<td>限制检查的入站网络接口，仅支持PREROUTING, INPUT, FORWARD规则链</td>
</tr>
<tr>
<td>--limit-iface-out</td>
<td>限制检查的出站网络接口，仅支持POSTROUTING, OUTPUT, FORWARD规则链</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">connbytes</span></div>
<p>根据<span style="background-color: #c0c0c0;">某个连接到目前为止</span>发送的字节数、封包数，或者每封包平均字节数进行匹配，计数器64位长。</p>
<p>该扩展可以用来检测维持了很长时间的下载，并将其安排到低优先级，以进行带宽控制。</p>
<p>使用<pre class="crayon-plain-tag">-m connbytes</pre>加载，可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>[!] --connbytes from[:to]</td>
<td>指定匹配的封包/字节数范围</td>
</tr>
<tr>
<td>--connbytes-dir {original|reply|both}</td>
<td>统计哪种类型的封包</td>
</tr>
<tr>
<td>--connbytes-mode {packets|bytes|avgpkt}</td>
<td>统计模式，封包数、字节数、平均每封包的字节数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">connlimit</span></div>
<p>可用于限制一个客户端可以连接到某个服务的并发连接数。可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--connlimit-upto n</td>
<td>如果连接数低于或者等于n则匹配</td>
</tr>
<tr>
<td>--connlimit-above n</td>
<td>如果连接数大于n则匹配</td>
</tr>
<tr>
<td>--connlimit-mask prefix_length</td>
<td>统计哪些IP的总和连接数，prefire_length指定分组的IP前缀</td>
</tr>
<tr>
<td>--connlimit-saddr</td>
<td>限制源地址组，默认值</td>
</tr>
<tr>
<td>--connlimit-daddr</td>
<td>限制目的地址组</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag"># 允许每个客户端主机开启两个telnet连接
iptables -A INPUT -p tcp --syn --dport 23  -m connlimit --connlimit-above 2 -j REJECT
# 限制每个C类地址网段（例如192.168.0.*）最多总计建立16个HTTP连接 
iptables -p tcp --syn --dport 80 -m connlimit   --connlimit-above 16 --connlimit-mask 24 -j REJECT</pre>
<div class="blog_h3"><span class="graybg">connmark</span></div>
<p>基于关联到连接的netfilter mark字段进行匹配，此mark由CONNMARK目标来设置。</p>
<p>用法：<pre class="crayon-plain-tag">-m connmark [!] --mark value[/mask]</pre></p>
<p>示例：</p>
<pre class="crayon-plain-tag">#                            匹配所属连接被标记为123的封包
#                                                   将连接的标记拷贝到封包上
iptables -t mangle -I OUTPUT -m connmark --mark 123 -j CONNMARK --restore-mark</pre>
<div class="blog_h3"><span class="graybg">connlabel</span></div>
<p>类似于connmark，但是标签是按位的（每个标签占一个bit），全系统支持最多128个标签</p>
<div class="blog_h3"><span class="graybg">socket</span></div>
<p>匹配条件：</p>
<ol>
<li>针对包进行套接字查找（Socket lookup），如果发现存在打开的TCP/UDP套接字，也就是说SRC_IP:SRC_PORT:DST_IP:DST_PORT作为已建立的（established）套接字存在于本地TCP/IP栈，则匹配</li>
<li>存在established的监听套接字，则匹配</li>
<li>存在非零绑定的监听套接字（non-zero bound listening socket），甚至在非本地地址上监听，则匹配。所谓zero bound应该就是指绑定到0.0.0.0</li>
</ol>
<p>简而言之，就是<span style="background-color: #c0c0c0;">匹配应当由本地TCP/IP栈处理（而非转发）的封包</span>。进行套接字查找时，使用packet元组，或者嵌入在 ICMP/ICPMv6错误报文中的原始TCP/UDP头</p>
<p>可以接额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--transparent</td>
<td>
<p>仅仅匹配透明套接字。透明套接字意味着地址不是本机的任何网络接口上配置的地址</p>
</td>
</tr>
<tr>
<td>--nowildcard</td>
<td>
<p>不忽略绑定到ANY地址（0.0.0.0）的套接字</p>
<p>默认情况下不匹配zero-bound的监听套接字，其目的是让本地服务能够监听请求而不是统统被转发</p>
</td>
</tr>
</tbody>
</table>
<p>这个匹配扩展+MARK+策略路由，就能够实现全功能的non-locally bound套接字。常常用于配合优化<a href="/network-faq#transparent-proxy">透明代理</a>。通常是匹配后进行MARK，然后具有此MARK的封包路由设置到lo。</p>
<p>从各种文档中看到关于socket match的说明，都不是非常明朗，导致理解起TPROXY拦截模式下Istio的iptables规则有些困难：</p>
<pre class="crayon-plain-tag"># mangle表

target     prot opt source               destination         
ISTIO_INBOUND  tcp  --  0.0.0.0/0            0.0.0.0/0           

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain ISTIO_DIVERT (1 references)
target     prot opt source               destination         
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK set 0x539
LOG        tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-socket-matched: "
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_INBOUND (1 references)
target     prot opt source               destination         
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
RETURN     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
ISTIO_DIVERT  tcp  --  0.0.0.0/0            0.0.0.0/0            socket
ISTIO_TPROXY  tcp  --  0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_TPROXY (1 references)
target     prot opt source               destination         
LOG        tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-before-tproxy: "
TPROXY     tcp  --  0.0.0.0/0           !127.0.0.1            TPROXY redirect 0.0.0.0:15001 mark 0x539/0xffffffff</pre>
<p>这是一个启用了TPROXY拦截模式的Pod的iptables，LOG目标是我添加用于分析socket match的行为的。此Pod的地址是172.27.121.156。可以看到它在监听：</p>
<pre class="crayon-plain-tag"># netstat -nlt
# Active Internet connections (only servers)
# Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN   </pre>
<p>现在我们从另一个客户端 <pre class="crayon-plain-tag">172.27.155.65</pre>上访问<pre class="crayon-plain-tag">curl http://172.27.121.156</pre>。</p>
<p>这会发起一个TCP连接，它的SYN包，是否匹配socket扩展呢？我们回顾一下它的匹配条件：</p>
<ol>
<li>套接字查找成功，这个不满足。因为是新连接</li>
<li>已经建立的监听套接字，这个不满足，监听套接字还没有收到SYN，谈不上建立</li>
<li>存在non-zero bound listening socket。这个说的让人费解。但是肯定隐含着在SYN包的目的地址上监听。此SYN包的目的地址是172.27.121.156:80，我们知道Pod在0.0.0.0:80上监听了，那么172.27.121.156作为它的本地地址，自然也在其上监听。那么，这个监听套接字，是不是non-zero bound的呢？网上很难找到相关信息，我的理解，<span style="background-color: #c0c0c0;">zero bound就是指绑定到0.0.0.0</span>。在这种猜测之下，是不匹配socket的</li>
</ol>
<p>根据iptables日志来验证一下。客户端curl之后，在Pod的内核看到一下日志：</p>
<pre class="crayon-plain-tag"># 客户端发来的SYN，可以看到不匹配socket，直接走到TPROXY，这证明了上述猜测
dst-80-before-tproxy: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=60 SYN URGP=0 
# 客户端发来的ACK，注意反方向报文没有日志
# 这此匹配socket了，因为满足了1，套接字查找成功
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 ACK URGP=0 MARK=0x539 
# 这次应该是客户端发送HTTP请求报文了
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=130 ACK PSH URGP=0 MARK=0x539 
# 这里是Envoy发给80端口进程，首个报文，但是不会TPROXY，因为目的地址不满足要求
dst-80-before-tproxy: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00 SYN URGP=0 
# 套接字查找成功，都不再走TPROXY了
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=322 TOS=0x00 PREC=0x00 ACK PSH URGP=0 MARK=0x539 
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539 
# 80进程处理完，报文返回Envoy，Envoy又返回给客户端。客户端发起后续报文，包括FIN
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK FIN URGP=0 MARK=0x539 
dst-80-socket-matched: IN=eth0 OUT=  SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539 
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK FIN URGP=0 MARK=0x539</pre>
<div class="blog_h3"><span class="graybg">set </span></div>
<p>IPSet扩展，可以匹配一组IP。使用<pre class="crayon-plain-tag">-m set</pre>加载，额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--match-set ipsetname</td>
<td>
<p>匹配的IP Set，<pre class="crayon-plain-tag">[!] --match-set setname flag[,flag]...</pre>形式</p>
<p>此选项必须，flag是IPSet的字段</p>
<p><pre class="crayon-plain-tag">iptables -A FORWARD -m set --match-set test src,dst</pre>表示：</p>
<ol>
<li>如果test是ipportmap，则当封包的源地址、目的端口对可以在test中发现时，匹配</li>
<li>如果test是ipmap，则当封包的源地址可以在test中发现时，匹配</li>
</ol>
</td>
</tr>
<tr>
<td>--return-nomatch</td>
<td>如果指定此选项并且目标IPSet的类型支持nomatch，则反转匹配结果</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">bpf</span></div>
<p>使用Linux Socket Filter来过滤，需要提供十进制形式的（由nfbpf_compile生成）BPF字节码。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">iptables -A OUTPUT -m bpf --bytecode '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' -j ACCEPT
# 等价写法
iptables -A OUTPUT -m bpf --bytecode "`nfbpf_compile RAW 'ip proto 6'`" -j ACCEPT</pre>
<div class="blog_h3"><span class="graybg">cpu</span></div>
<p>根据处理封包的CPU来匹配。示例：</p>
<pre class="crayon-plain-tag"># 如果是第一个核心处理了封包，则重定位到8080端口
iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 0 -j REDIRECT --to-port 8080
# 如果是第二个核心处理了封包，则重定位到8081端口
iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 1 -j REDIRECT --to-port 8081</pre>
<div class="blog_h3"><span class="graybg">time</span></div>
<p>根据封包到达时间来匹配。 </p>
<div class="blog_h2"><span class="graybg">目标</span></div>
<p>使用选项<pre class="crayon-plain-tag">-j</pre>来指定目标，除了本节要介绍的各种目标外，该选项的值还可以是：</p>
<ol>
<li>自定义链的名称，这导致跳转到自定义的链继续执行</li>
</ol>
<div class="blog_h3"><span class="graybg">ACCEPT</span></div>
<p>接收封包，不再遍历当前链的其它规则、也不会在遍历当前表中的任何规则。但是，在一个链中被接收的封包，可能<span style="background-color: #c0c0c0;">仍然需要经过其它表中的链</span>，仍然可能被DROP。</p>
<p>在子链中ACCEPT的效果，等价于在父链中被ACCEPT。</p>
<div class="blog_h3"><span class="graybg">RETURN</span></div>
<p>停止遍历当前链：</p>
<ol>
<li>如果当前位于子链，则回到父链，继续执行后面的规则</li>
<li>如果当前位于主链，则执行默认策略</li>
</ol>
<div class="blog_h3"><span class="graybg">DROP</span></div>
<p>静默的丢弃封包，不会在对它进行任何后续处理。</p>
<p>可能会导致对方主机上出现一个僵尸套接字，最好用REJECT代替。</p>
<div class="blog_h3"><span class="graybg">REJECT</span></div>
<p>用于发送一个错误报文给匹配封包的发送者，其它方面，该目标的行为与DROP一致：丢弃封包，终止规则遍历。</p>
<p>该目标仅用于INPUT, FORWARD, OUTPUT规则链中，或者从这两规则链调用的用户自定义规则链。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--reject-with type</td>
<td>错误报文的类型：<br />icmp-net-unreachable<br />icmp-host-unreachabl<br />icmp-port-unreachable（默认）<br />icmp-proto-unreachable<br />icmp-net-prohibited<br />icmp-host-prohibite<br />icmp-admin-prohibited</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AUDIT</span></div>
<p>允许创建命中目标的封包的审计记录，用来记录被接受的、丢弃的、拒绝的封包。</p>
<p>用法：<pre class="crayon-plain-tag">-j AUDIT --type {accept|drop|reject}</pre></p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 添加一个规则链到默认表格
iptables -N AUDIT_DROP
# 审计丢弃包的动作
iptables -A AUDIT_DROP -j AUDIT --type drop
# 丢弃包
iptables -A AUDIT_DROP -j DROP</pre>
<div class="blog_h3"><span class="graybg">LOG</span></div>
<p>这是一个非终止性目标，后续规则总是会被执行。如果你希望LOG后DROP，在此规则后面紧跟着一个相同匹配的目标为DROP的规则。</p>
<p>让内核记录匹配的封包。内核将会打印匹配的封包的信息，包括大部分IP头字段。这些日志可以在dmesg/syslog中看到。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--log-level level</td>
<td>日志级别：emerg, alert, crit, error, warning, notice, info, debug</td>
</tr>
<tr>
<td>--log-prefix prefix</td>
<td>日志前缀，29个字符最多</td>
</tr>
<tr>
<td>--log-tcp-sequence</td>
<td>记录TCP序列号</td>
</tr>
<tr>
<td>--log-tcp-options</td>
<td>记录TCP选项</td>
</tr>
<tr>
<td>--log-ip-options</td>
<td>记录IP选项</td>
</tr>
<tr>
<td>--log-uid</td>
<td>记录生成封包的UID</td>
</tr>
</tbody>
</table>
<p>关于容器中的iptables logging：</p>
<ol>
<li>默认情况下，来自容器（非全局网络命名空间）的iptable logging被忽略，你无法在<pre class="crayon-plain-tag">dmesg -w</pre>中看到</li>
<li>要改变此行为，你需要较新版本的内核，并且设置：<span style="color: #242729;"><pre class="crayon-plain-tag">echo 1 &gt; /proc/sys/net/netfilter/nf_log_all_netns</pre></span></li>
</ol>
<div class="blog_h3"><span class="graybg">DNAT</span></div>
<p>仅用于nat表的PREROUTING,OUTPUT规则链，或者从这两规则链调用的用户自定义规则链。</p>
<p>该目标指定封包的目的地址应当被修改（且该连接上所有以后的封包应当执行同样的操作），规则应当停止检查。</p>
<p>应用场景：HTTP服务放置在内网主机上，前端有个防火墙配置公网IP，互联网用户通过此公网IP访问内网主机上的HTTP服务，这种情况需要DNAT</p>
<p>额外选项： </p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--to-destination</td>
<td>
<p>--to-destination [ipaddr[-ipaddr]][:port[-port]]</p>
<p>指定单个目的IP地址或者一个地址范围。如果指定了以下协议：tcp, udp, dccp, sctp，则可以指定一个端口范围。如果不指定端口，那么目的端口不会被改写。如果不指定IP地址，则仅仅会修改端口</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>--random</td>
<td>随机化端口映射</td>
</tr>
<tr>
<td>--persistent</td>
<td>对于一个客户端连接，给予相同的映射</td>
</tr>
</tbody>
</table>
<p>举例：</p>
<pre class="crayon-plain-tag"># 如果公网客户端访问本防火墙的80端口
# 那么将其目标地址改写为192.168.0.200，目标端口不变
iptables -t nat 
         -A PREROUTING 
         -d 106.185.46.7
         --dport 80
         -j DNAT --to-destination 192.168.0.200</pre>
<div class="blog_h3"><span class="graybg">SNAT</span></div>
<p>仅用于nat表的POSTROUTING,INPUT规则链，或者从这两规则链调用的用户自定义规则链。</p>
<p>该目标指定封包的源地址应当被修改（且该连接上所有以后的封包应当执行同样的操作），规则应当停止检查。</p>
<p>应用场景：局域网前端有个防火墙配置公网IP，内网主机通过该IP访问公网，为了内网主机能够访问外网，需要SNAT。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--to-source</td>
<td>
<p>--to-source [ipaddr[-ipaddr]][:port[-port]]</p>
<p>指定单个源IP地址或者一个地址范围。如果指定了以下协议：tcp, udp, dccp, sctp，则可以指定一个端口范围。如果不指定端口范围，那么小于512的源端口映射到512以下；512-1023之间的源端口映射到1024以下；其它源端口映射到1024以上。如果可能，则不去修改端口</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>--random</td>
<td>随机化端口映射</td>
</tr>
<tr>
<td>--persistent</td>
<td>对于一个客户端连接，给予相同的映射</td>
</tr>
</tbody>
</table>
<p>举例：</p>
<pre class="crayon-plain-tag"># 对于来自192.168.0.0网段的所有出站请求
# 如果这些封包将通过eth0接口（外网网卡）传出去
# 那么，将封包源地址改写为106.185.46.7（即eth0的IP地址）
iptables -t nat -A POSTROUTING 
         -s 192.168.0.0/255.255.255.0 
         -o eth0 
         -j SNAT --to-source 106.185.46.7</pre>
<div class="blog_h3"><span class="graybg">MASQUERADE</span></div>
<p>仅用于nat表的POSTROUTING规则链。应当用于动态分配IP的网络连接，静态网络连接应当使用SNAT目标。该目标会自动从网络接口上获取当前IP地址，用来做源地址的SNAT</p>
<p>应用场景：本主机具有动态分配的公网地址，有客户端通过PPP连接（VPN）到本主机，并通过本主机上网。这时需要将客户端封包的源地址（PPP对端地址）转换为公网地址，但是由于本主机的公网地址是动态获取的，因此不能使用SNAT，只能使用MASQUERADE</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--to-ports port[-port]</td>
<td>指定一组转换后使用的源端口。仅当协议指定为tcp, udp, dccp, sctp时可用</td>
</tr>
<tr>
<td>--random</td>
<td>随机化端口映射</td>
</tr>
</tbody>
</table>
<p>举例：</p>
<pre class="crayon-plain-tag">iptables -t nat -A POSTROUTING 
         -s 192.168.0.0/255.255.255.0 
         -o eth0 
         -j MASQUERADE</pre>
<div class="blog_h3"><span class="graybg">TPROXY</span></div>
<p>仅可用在mangle的PREROUTING链（或者从该链调用的用户自定义链）。此目标能够<strong><span style="background-color: #c0c0c0;">重定向包到一个本地套接字，但却不修改包头的任何信息</span></strong>。 它还能够改变封包的mark，进而辅助后续的策略路由。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--on-port port</td>
<td>重定向到的本地套接字的目标端口，0表示新的目标端口与原始端口相同</td>
</tr>
<tr>
<td>--on-ip address</td>
<td>重定向到的本地套接字的目标IP地址，默认情况下是入口网卡的地址。仅仅当规则指定了-p tcp或-p udp时可用</td>
</tr>
<tr>
<td>--tproxy-mark value[/mask]</td>
<td>为封包添加标记，以便选择正确的路由表，这里设置的fwmark可以用于高级路由。透明代理必须要此选项正确设置，否则封包将被转发走</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">REDIRECT</span></div>
<p>可以认为是一种DNAT。</p>
<p>仅用于nat表的PREROUTING,OUTPUT规则链，或者从这两规则链调用的用户自定义规则链。该目标重定向封包到当前主机本身，通过<strong><span style="background-color: #c0c0c0;">把目的地址改写</span></strong>为入站网卡的主地址（本地生成的封包映射到localhost地址，对于IPv4即127.0.0.1）。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--to-ports port[-port]</td>
<td>
<p>重定向到的目的端口，或者目的端口的范围</p>
<p>如果不指定此选项，则目的端口不变</p>
</td>
</tr>
<tr>
<td>--random</td>
<td>随机化端口映射</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CONNMARK</span></div>
<p>在连接上设置netfilter标记。标记为32bit。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--set-xmark</td>
<td>
<p>--set-xmark value[/mask]</p>
<p>如果指定mask，则将mask提供的bit归零，并和value 异或，得到最终的ctmark</p>
</td>
</tr>
<tr>
<td>--save-mark</td>
<td>
<p>--save-mark [--nfmask nfmask] [--ctmask ctmask]</p>
<p>拷贝封包标记（nfmark）为连接标记（ctmark）。算法：</p>
<p style="padding-left: 30px;">ctmark = (ctmark &amp; ~ctmask) ^ (nfmark &amp; nfmask)</p>
</td>
</tr>
<tr>
<td>--restore-mark</td>
<td>
<p>--restore-mark [--nfmask nfmask] [--ctmask ctmask]</p>
<p>拷贝连接标记为封包标记。算法：</p>
<p style="padding-left: 30px;">nfmark = (nfmark &amp; ~nfmask) ^ (ctmark &amp; ctmask) </p>
<p>此选项仅能用于mangle表</p>
</td>
</tr>
<tr>
<td>--and-mark bits</td>
<td>等价于 --set-xmark 0/invbits，invbits是bits取反</td>
</tr>
<tr>
<td>--or-mark bits</td>
<td>等价于--set-xmark bits/bits</td>
</tr>
<tr>
<td>--xor-mark bits</td>
<td>等价于--set-xmark bits/0</td>
</tr>
<tr>
<td>--set-mark value[/mask]</td>
<td>仅仅mask中设置的那些bit，对应value中的bit的设置</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">MARK</span></div>
<p>在封包上设置netfilter标记。标记为32bit。</p>
<p>常常和基于fwmark的路由（需要iproute2）联用，这种情况下你需要在PREROUTING链mangle表中使用，以影响路由策略。</p>
<p>额外选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">子选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--set-xmark value[/mask]</td>
<td>如果指定mask，则将当前nfmark中mask提供的bit归零，并和value 异或，得到最终的nfmark</td>
</tr>
<tr>
<td>--set-mark value[/mask]</td>
<td>如果指定mask，则将当前nfmark中mask提供的bit归零，并和value或，得到最终的nfmark</td>
</tr>
<tr>
<td>--and-mark bits</td>
<td>和nfmark进行二进制与</td>
</tr>
<tr>
<td>--or-mark bits</td>
<td>和nfmark进行二进制或</td>
</tr>
<tr>
<td>--xor-mark bits</td>
<td>和nfmark进行二进制异或</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CHECKSUM</span></div>
<p>只能用于mangle表，可以为封包添加缺失的校验和</p>
<div class="blog_h3"><span class="graybg">IDLETIMER </span></div>
<p>可以用于识别某个网络接口已经空闲一定的时间</p>
<div class="blog_h3"><span class="graybg">NETMAP</span></div>
<p>允许静态的把整个网络的IP地址映射到另外一个网络的IP地址</p>
<div class="blog_h3"><span class="graybg">NFLOG</span></div>
<p>用于记录匹配包的日志</p>
<div class="blog_h3"><span class="graybg">NOTRACK</span></div>
<p>禁止匹配规则的封包的连接跟踪。你无法通过conntrack命令看到这样的连接</p>
<div class="blog_h3"><span class="graybg">TCPMSS</span></div>
<p>修改TCP的SYN封包的MSS值，用来控制连接的最大单个段的大小，一般限制到出站网络接口MTU-40</p>
<div class="blog_h3"><span class="graybg">TTL</span></div>
<p>该目标用于修改封包的TTL</p>
<div class="blog_h1"><span class="graybg">高级</span></div>
<div class="blog_h2"><span class="graybg">conntrack和NAT</span></div>
<p>假设内网客户端机器IP地址为10.0.0.1；NAT网关内网地址10.0.0.2，外网地址10.4.80.14；外网服务器地址为9.135.102.75。</p>
<p>我们可以通过conntrack命令，在NAT网关上查看相关事件：</p>
<pre class="crayon-plain-tag">conntrack -E --proto tcp -d 9.135.102.75

    [NEW] tcp      6 120 SYN_SENT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                   [UNREPLIED] src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736
 [UPDATE] tcp      6 60 SYN_RECV src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736
 [UPDATE] tcp      6 432000 ESTABLISHED src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
 [UPDATE] tcp      6 120 FIN_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
 [UPDATE] tcp      6 60 CLOSE_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
 [UPDATE] tcp      6 30 LAST_ACK src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
 [UPDATE] tcp      6 120 TIME_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
 # 过了2MSL
 [DESTROY] tcp      6 src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000 
                               src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]</pre>
<p>NAT表的特殊之处在于，<a href="https://www.netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html#ss3.2">仅新连接的第一个封包会经过此表</a>。第一个封包的地址转换结果，会应用到连接的所有后续封包。</p>
<p>NAT网关接收到新连接后，创建conntrack条目：</p>
<p style="padding-left: 30px;">10.0.0.1:56736-&gt;9.135.102.75:36000, 9.135.102.75:36000-&gt; 10.0.0.1:56736</p>
<p>在iptables中，NAT网关的POSTROUTING钩子中的SNAT规则，会修改conntrack的应答目的地址</p>
<p style="padding-left: 30px;">10.0.0.1:56736-&gt;9.135.102.75:36000, 9.135.102.75:36000-&gt;<strong>10.4.80.14:56736</strong></p>
<p>这时，就到达上面命令输出的[NEW]状态了。请求/应答报文进行地址转换所需的信息已经完备。</p>
<p>iptables规则所做的事情，仅仅是修改conntrack规则而已。所有其它事情，包括请求封包的源地址修改，是由内核中的conntrack模块（nf_conntrack、nf_conntrack_ipv4）以及nat模块（nf_nat、nf_nat_ipv4）等负责的。</p>
<p>当NAT网关接收到应答报文后，会检索conntrack中的条目，并将匹配的条目关联到该封包（<pre class="crayon-plain-tag">skb-&gt;_nfct</pre>）。这个条目将用于目的地址的还原。</p>
<p>注意以下几点：</p>
<ol>
<li>iptables不是netfilter本身，它是netfilter的用户。而conntrack和nat模块是netfilter的一部分</li>
<li>无法在POSTROUTING钩子+NAT表看到检查到应答报文，因为NAT表仅仅对新连接调用一次</li>
<li>连接跟踪、NAT转换的大部分工作由相应内核模块，而不是iptables完成</li>
</ol>
<div class="blog_h2"><span class="graybg">关于xtables锁</span></div>
<div class="blog_h3"><span class="graybg">实现方式</span></div>
<p>在iptables新老版本中，xtables锁的实现不同。在1.4.x中，锁基于abstract unix domain socket实现；1.6.x中则基于普通文件。</p>
<pre class="crayon-plain-tag">func grabIptablesLocks(lockfilePath string) (iptablesLocker, error) {
	var err error
	var success bool

	l := &amp;locker{}
	defer func(l *locker) {
		// Clean up immediately on failure
		if !success {
			l.Close()
		}
	}(l)

	// iptables 1.6.x和1.4.x版本的锁是不一样的
	// 1.6.x的锁文件是：/run/xtables.lock
	// 下面的逻辑是1.6.x xtables_lock() 函数的粗略的重复实现
	l.lock16, err = os.OpenFile(lockfilePath, os.O_CREATE, 0600)
	if err != nil {
		return nil, fmt.Errorf("failed to open iptables lock %s: %v", lockfilePath, err)
	}


	// 尝试加锁（1.6.x版本）
	if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) {
		if err := grabIptablesFileLock(l.lock16); err != nil {
			return false, nil
		}
		return true, nil
	}); err != nil {
		return nil, fmt.Errorf("failed to acquire new iptables lock: %v", err)
	}

	// 下面的逻辑是1.4.x xtables_lock() 函数的粗略的重复实现
	if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) {
		// 1.4.x的锁是抽象Unidx Domain socket
		// 前缀的@表示套接字位于抽象命名空间（abstract namespace），不体现为文件系统中的文件
		l.lock14, err = net.ListenUnix("unix", &amp;net.UnixAddr{Name: "@xtables", Net: "unix"})
		if err != nil {
			return false, nil
		}
		return true, nil
	}); err != nil {
		return nil, fmt.Errorf("failed to acquire old iptables lock: %v", err)
	}

	success = true
	return l, nil
}

// 加1.6锁
func grabIptablesFileLock(f *os.File) error {
	//                             独占         非阻塞
	return unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)
}</pre>
<div class="blog_h3"><span class="graybg">加锁失败时的错误</span></div>
<p>尝试修改iptables时，可能会因为锁被占用导致报错。这种报错没有专门的退出码，因此只能比对stderr输出来判定。</p>
<p>正常情况下，出现锁争用的概率比较低，可以使用下面的命令显式占用xtables锁：</p>
<pre class="crayon-plain-tag"># 1.4.x加锁
socat ABSTRACT-LISTEN:xtables -

# 1.6.x加锁
flock -x /run/xtables.lock sleep 365d</pre>
<p>保持运行上述两个命令的Terminal打开，然后插入iptables规则。可以看到报错信息如下：</p>
<ol>
<li>v1.4.21版本：
<ol>
<li>不带-w参数：Another app is currently holding the xtables lock. Perhaps you want to use the -w option?</li>
<li>带有-w参数：Another app is currently holding the xtables lock; still 21s 0us time ahead to have a chance to grab the lock... Another app is currently holding the xtables lock. Stopped waiting after 30s.</li>
</ol>
</li>
<li>v1.6.0版本：报错同上</li>
</ol>
<p>实际上，K8S中就是基于上述输出信息进行判断的：</p>
<pre class="crayon-plain-tag">func IsProxyLocked(err error) bool {
	return strings.Contains(err.Error(), "holding the xtables lock")
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/iptables">重温iptables</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/iptables/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>基于BCC进行性能追踪</title>
		<link>https://blog.gmem.cc/bcc</link>
		<comments>https://blog.gmem.cc/bcc#comments</comments>
		<pubDate>Fri, 07 Jun 2019 07:33:11 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[eBPF]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37315</guid>
		<description><![CDATA[<p>简介 BCC是一个工具包，用来创建高效的、在内核中运行的性能追踪程序。它首先是一个编译器，能够将你写的代码编译为eBPF程序，此外，它还提供了大量有用的工具和样例 BCC的基础是eBPF，因此其大部分功能需要内核版本在4.1+才能使用，其提供的某些根据甚至需要4.4或4.6版本的内核。要编译和运行BCC程序，你除了需要安装bcc之外，还需要llvm/clang，以及内核头文件。 BCC的支持内核中多种追踪机制，包括kprobe、uprobe、tracepoint、USDT（User-level statically defined tracing probes ），能够监控块设备IO、TCP函数、文件系统操作、任何syscall、Node.js/Java/MySQL/libc等多种常用软件/平台定义的USDT。 在网络监控方面，BCC提供了分布式网桥、HTTP过滤器、快速封包丢弃、隧道监控等样例代码。 BCC通过以下途径让eBPF开发变得简单： 在C中使用kernel instrumentation，提供了LLVM的一个C wrapper 提供Python和Lua的前端 安装 内核配置 要使用BCC，你通常需要4.1或者更新版本的内核，并且启用以下内核配置： [crayon-69e2a15489a8f021295652/] 安装软件包 [crayon-69e2a15489a94459328392/] 从源码构建 <a class="read-more" href="https://blog.gmem.cc/bcc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/bcc">基于BCC进行性能追踪</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>BCC是一个工具包，用来创建高效的、在内核中运行的性能追踪程序。它首先是一个编译器，能够将你写的代码编译为eBPF程序，此外，它还提供了大量有用的工具和样例</p>
<p>BCC的基础是eBPF，因此其大部分功能需要内核版本在4.1+才能使用，其提供的某些根据甚至需要4.4或4.6版本的内核。要编译和运行BCC程序，你除了需要安装bcc之外，还需要llvm/clang，以及内核头文件。</p>
<p>BCC的支持内核中多种追踪机制，包括kprobe、uprobe、tracepoint、USDT（User-level statically defined tracing probes ），能够监控块设备IO、TCP函数、文件系统操作、任何syscall、Node.js/Java/MySQL/libc等多种常用软件/平台定义的USDT。</p>
<p>在网络监控方面，BCC提供了分布式网桥、HTTP过滤器、快速封包丢弃、隧道监控等样例代码。</p>
<p>BCC通过以下途径让eBPF开发变得简单：</p>
<ol>
<li>在C中使用kernel instrumentation，提供了LLVM的一个C wrapper</li>
<li>提供Python和Lua的前端</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">内核配置</span></div>
<p>要使用BCC，你通常需要4.1或者更新版本的内核，并且启用以下内核配置：</p>
<pre class="crayon-plain-tag">CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
# [optional, for tc filters]
CONFIG_NET_CLS_BPF=m
# [optional, for tc actions]
CONFIG_NET_ACT_BPF=m
CONFIG_BPF_JIT=y
# [for Linux kernel versions 4.1 through 4.6]
CONFIG_HAVE_BPF_JIT=y
# [for Linux kernel versions 4.7 and later]
CONFIG_HAVE_EBPF_JIT=y
# [optional, for kprobes]
CONFIG_BPF_EVENTS=y
# Need kernel headers through /sys/kernel/kheaders.tar.xz
CONFIG_IKHEADERS=y</pre>
<div class="blog_h2"><span class="graybg">安装软件包</span></div>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
# CentOS
yum install bcc-tools</pre>
<div class="blog_h2"><span class="graybg">从源码构建</span></div>
<p>从0.10.0开始，BCC依赖libbpf，后者提供对BPF系统调用的包装函数，提供bpf.h / btf.h的UAPI头文件。</p>
<p>安装构建BCC需要的依赖，以及相关工具：</p>
<pre class="crayon-plain-tag">apt install -y bison build-essential cmake flex git libedit-dev \
    python3-distutils  llvm clang llvm-dev libclang-dev python zlib1g-dev libelf-dev libfl-dev

apt install arping netperf iperf3
ln -s `which python3` /usr/bin/python</pre>
<p>编译并安装BCC：</p>
<pre class="crayon-plain-tag">git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install


cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
make
sudo make install</pre>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<p>BCC自带的工具位于<pre class="crayon-plain-tag">/usr/share/bcc/tools</pre>下，需要将其加入到PATH环境变量。</p>
<div class="blog_h1"><span class="graybg">工具</span></div>
<p>我们可以先了解以下BCC提供的各种工具，这些工具可以直接用来解决性能追踪等问题，也是发明新轮子的重要参考。</p>
<p>下图显示了工作在系统各层次的工具列表：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/06/bcc_tracing_tools_2019.png"><img class="size-large wp-image-37455 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2019/06/bcc_tracing_tools_2019-1024x613.png" alt="bcc_tracing_tools_2019" width="710" height="425" /></a></p>
<div class="blog_h2"><span class="graybg">非BCC工具</span></div>
<p>在使用BCC之前，我们有必要回顾以下常用的Linux性能分析工具。</p>
<div class="blog_h3"><span class="graybg">uptime</span></div>
<pre class="crayon-plain-tag">uptime 
23:51:26 up 21:31, 1 user, load average: 30.02, 26.43, 19.02</pre>
<p>这个命令可以查看系统启动了多久，还可以快速查看工作负载情况。最后的三个数字分别是最近1/5/15分钟的、基于指数衰减移动和平均（exponentially damped moving sum averages）算法的工作负载信息。如果你发现第一个数字比第二个小很多，可能提示你错过了高负载时刻。</p>
<p>工作负载提示了有多少进程想要运行（去获得CPU）：包括积极等待被调度的进程，以及被不可中断睡眠（通常是块IO）阻塞的进程。</p>
<div class="blog_h3"><span class="graybg">dmesg</span></div>
<p>内核在重大事件发生后，都会输出信息到日志环：</p>
<pre class="crayon-plain-tag">dmesg | tail
# [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0
# [1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child
# [1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB
# [2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request.  Check SNMP counters.</pre>
<p>系统工作不正常是，有必要观察这些日志。</p>
<div class="blog_h3"><span class="graybg">vmstat</span></div>
<pre class="crayon-plain-tag">vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  1  0      0 35469380 1681776 43056984    0    0   673   552  174  706 17  8 74  1  0
#  4  0      0 35468148 1681776 43059452    0    0    12  3480 21357 44556  8  3 88  0  0
# 10  0      0 35467088 1681784 43059824    0    0    20  3212 23257 45228 14  5 81  0  0</pre>
<p>这个工具给出系统的各种宏观统计信息：</p>
<ol>
<li><pre class="crayon-plain-tag">r</pre>等待获得运行机会的CPU数量，比起load average，更明确的显示了CPU饱和度，因为它没有包含I/O，如果数量大于CPU核心，说明饱和了</li>
<li><pre class="crayon-plain-tag">free</pre> 空闲内存KB</li>
<li><pre class="crayon-plain-tag">si</pre>/<pre class="crayon-plain-tag">so</pre> 换入/换出的页数量，如果非零提示内存不足</li>
<li><pre class="crayon-plain-tag">us</pre>, <pre class="crayon-plain-tag">sy</pre>, <pre class="crayon-plain-tag">id</pre>, <pre class="crayon-plain-tag">wa</pre>, <pre class="crayon-plain-tag">st</pre> 处理器时间的分解：用户空间耗时，内核空间耗时，空闲时间，等待IO时间，被偷走的时间（虚拟化环境下被其它客户机偷走使用的）
<ol>
<li>如果us+sy高提示CPU繁忙、wa高则提示IO瓶颈</li>
<li>系统需要消耗sy时间来处理IO，如果sy时间总是很高，例如占比20%+，可能提示内核处理IO效率较差</li>
<li>CPU利用90%+不一定说明CPU饱和，查看r列更靠谱</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">mpstat</span></div>
<p>这个命令可以分别查看每个CPU核心的使用情况：</p>
<pre class="crayon-plain-tag">mpstat -P ALL 1                                                                                                                                                                          
# Linux 5.8.0-55-generic (zircon)         06/11/2021      _x86_64_        (16 CPU)
# 09:14:03 PM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
# 09:14:04 PM  all    2.95    0.00   10.39    0.80    0.00    0.00    0.00    0.68    0.00   85.18
# 09:14:04 PM    0    5.00    0.00    5.00    0.00    0.00    0.00    0.00    3.00    0.00   87.00
# 09:14:04 PM    1    6.06    0.00    5.05    0.00    0.00    0.00    0.00    0.00    0.00   88.89</pre>
<p>能够用来发现CPU负载不均衡的情况。单个热点CPU，提示存在一个忙的单线程应用程序。</p>
<div class="blog_h3"><span class="graybg">pidstat</span></div>
<pre class="crayon-plain-tag">pidstat 1
# Linux 5.8.0-55-generic (zircon)         06/11/2021      _x86_64_        (16 CPU)

# 09:17:58 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
# 09:17:59 PM  1000      2736    4.95    1.98    0.00    0.00    6.93     6  gnome-shell
# 09:17:59 PM  1000      6916    0.99    0.00    0.00    0.00    0.99    11  gnome-terminal-
# 09:17:59 PM  1000      7380    8.91   51.49    0.00    0.00   60.40     4  vmware-vmx</pre>
<p>类似于top，但是滚动更新，有助于录制和事后分析或者发现特定的pattern。</p>
<div class="blog_h3"><span class="graybg"> iostat</span></div>
<pre class="crayon-plain-tag">iostat -x 1</pre>
<p>用于了解块设备的工作情况：</p>
<ol>
<li> <pre class="crayon-plain-tag">r/s</pre>, <pre class="crayon-plain-tag">w/s</pre>, <pre class="crayon-plain-tag">rkB/s</pre>, <pre class="crayon-plain-tag">wkB/s</pre>：读写的次数、读写的字节数</li>
<li><pre class="crayon-plain-tag">await</pre>：等待IO完成的平均ms</li>
<li><pre class="crayon-plain-tag">avgqu-sz</pre>：平均发送给目标设备的请求数量，数量大于1提示饱和（尽管设备通常能够并行处理请求，特别是由多个后端磁盘组成的虚拟设备）</li>
<li><pre class="crayon-plain-tag">%util</pre>：提示设备有多少时间百分比是忙着的。尽管取决于设备，大于60%通常会导致低性能。接近100%通常提示设备饱和。但是，对于多个后端磁盘组成的虚拟设备，100%仅仅提示所有时间都有后端磁盘在工作，但是可能远远没有饱和。</li>
</ol>
<p>需要注意，很多应用程序都使用异步IO技术，这样即使设备饱和，应用程序也不会出现直接的（可被预读、缓冲写缓和）延迟。</p>
<div class="blog_h3"><span class="graybg">free</span></div>
<pre class="crayon-plain-tag">free -m
#               total        used        free      shared  buff/cache   available
# Mem:         128677        6601       77687       12528       44389      108334
# Swap:         62499           0       62499</pre>
<div class="blog_h3"><span class="graybg">sar</span></div>
<p>需要安装下面的软件：</p>
<pre class="crayon-plain-tag">apt install sysstat</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">ENABLED="true"</pre>
<p>并重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart sysstat.service</pre>
<p>查看网络接口的吞吐情况：</p>
<pre class="crayon-plain-tag">sar -n DEV 1

# 接口     收包数    发包数      收KB      发KB                                   网卡利用率
  IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
  lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00</pre>
<p>查看TCP统计信息：</p>
<pre class="crayon-plain-tag">sar -n TCP,ETCP 1

#            本地主动发起连接数量
#                     本地被动accept连接数量
#                                  
09:38:04 PM  active/s passive/s    iseg/s    oseg/s
09:38:05 PM      0.00      0.00      5.00      5.00

#                               重传次数
09:39:41 PM  atmptf/s  estres/s retrans/s isegerr/s   orsts/s
09:39:42 PM      0.00      0.00      0.00      0.00      0.00</pre>
<div class="blog_h2"><span class="graybg">可观察性工具</span></div>
<div class="blog_h3"><span class="graybg">execsnoop</span></div>
<p>这个工具可以探测新<pre class="crayon-plain-tag">exec()</pre>的进程，注意它不会追踪<pre class="crayon-plain-tag">fork()</pre>因而不能看到所有的进程。</p>
<p>该工具可用于发现短暂存在的、消耗CPU的进程，这些进程可能难以在周期性的、快照式的监控工具（例如top）中出现。</p>
<div class="blog_h3"><span class="graybg">opensnoop</span></div>
<p>这个工具可以探测<pre class="crayon-plain-tag">open()</pre>系统调用，从而推测应用程序的工作方式（会使用哪些数据文件、配置文件、日志文件）。某些时候，应用程序因为反复尝试打开不存在的文件，或者反复写入大量数据到文件，而性能低下，该工具也可以发现这类场景。</p>
<div class="blog_h3"><span class="graybg">ext4slower</span></div>
<p>类似的工具还包括<pre class="crayon-plain-tag">btrfsslower</pre>, <pre class="crayon-plain-tag">xfsslower</pre>, <pre class="crayon-plain-tag">zfsslower</pre>。</p>
<p>这些工具可以探测单个的、缓慢的文件系统操作。由于磁盘是异步处理IO的，难以将块设备层的延迟和应用程序的延迟关联起来。在VFS/文件系统接口层进行探测，更加接近应用程序，延迟相关的可能性也更大。</p>
<div class="blog_h3"><span class="graybg">biolatency</span></div>
<p>跟踪此片IO延迟（从向设备发起请求到处理完毕），退出此工具时会绘制直方图。</p>
<p>iostat之类工具能给出平均延迟，此工具则通过直方图显示延迟的分布情况。</p>
<div class="blog_h3"><span class="graybg">biosnoop</span></div>
<p>按进程和延迟探测块IO操作。</p>
<div class="blog_h3"><span class="graybg">biotop</span></div>
<p>按进程显示块IO的汇总信息。</p>
<div class="blog_h3"><span class="graybg">cachestat</span></div>
<p>每行打印统计周期内（默认1s）文件系统缓存命中、丢失、读/写操作缓存命中率。</p>
<div class="blog_h3"><span class="graybg">bindsnoop</span></div>
<p>追踪IPv4/IPv6的<pre class="crayon-plain-tag">bind()</pre>系统调用。</p>
<div class="blog_h3"><span class="graybg">tcpconnect</span></div>
<p>该工具监控每一个新发起的主动TCP连接（通过<pre class="crayon-plain-tag">connect()</pre>系统调用）。</p>
<div class="blog_h3"><span class="graybg">tcpaccept</span></div>
<p>该工具监控每一个新发起的被动TCP连接（通过<pre class="crayon-plain-tag">accept()</pre>系统调用）。</p>
<div class="blog_h3"><span class="graybg">tcpretrans</span></div>
<p>每当出现一个TCP重传包时，该工具打印一行内容。</p>
<p>TCP重传会导致延迟和吞吐量问题。</p>
<p>对于ESTABLISHED状态连接的重传，需要检查网络patterns；对于SYN_SENT状态连接的重传，可能提示目标内核CPU饱和以及内核丢包。</p>
<div class="blog_h3"><span class="graybg">runqlat</span></div>
<p>显示线程在CPU运行队列上等待的时间的直方图。</p>
<p>在CPU饱和的情况下，该工具可以度量等待CPU消耗的时间。</p>
<div class="blog_h3"><span class="graybg">profile</span></div>
<p>以特定的周期，获取栈追踪采样，并且在结束时，打印每个独特的栈追踪的数量。</p>
<p>该工具可以辅助理解哪些代码路径消耗CPU资源，因为消耗资源越多的代码路径，被采样到的概率越大。</p>
<div class="blog_h3"><span class="graybg">trace</span></div>
<p>能够探测你指定的函数，并且在满足条件的时候打印指定格式的追踪消息。</p>
<p>命令格式：</p>
<pre class="crayon-plain-tag">trace [-h] [-b BUFFER_PAGES] [-p PID] [-L TID] [-v] [-Z STRING_SIZE] [-S] [-M MAX_EVENTS] [-t] 
      [-u] [-T] [-C] [-c CGROUP_PATH] [-n NAME] [-f MSG_FILTER] [-B] [-s SYM_FILE_LIST] [-K] [-U] [-a]
             [-I header]
             # 探针规格定义
             probe [probe ...]</pre>
<p>选项：</p>
<p style="padding-left: 30px;">perf_events 环缓冲占用的页数量，默认64<br />-p PID  追踪特定的进程<br />-L TID 追踪特定的线程<br />-v 执行前打印生成的BPF代码<br />-S 包含trace程序自身的栈追踪，默认排除<br />-M 退出前允许的最大事件数量<br />-t 打印时间戳信息<br />-t -u 打印UNIX时间戳<br />-T 打印时间<br />-c  cgroup路径<br />-n 仅仅打印进程名包含该选项指定字符串的进程<br />-f 仅仅打印消息包含该选项指定字符串的事件<br />-B 运行对二进制值调用STRCMP<br />-s 逗号分割的符号文件，用于栈的符号解析<br />-K 输出内核栈追踪<br />-U 输出用户栈追踪<br />-a 在栈中打印虚拟地址<br />-I 额外包含在BPF程序中的头文件地址</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 追踪内核的发生的do_sys_open函数调用
trace do_sys_open

# 追踪open系统调用，并打印打开的文件名
#     ::do_sys_open (没有前缀)表示你需要一个entry probe
#                  格式化为字符串
#                        打印第二个参数
trace 'do_sys_open "%s", arg2'

# 追踪进程名包含main的进程的do_sys_open调用
trace 'do_sys_open "%s", arg2' -n main

# 追踪打开文件名包含config的进程的do_sys_open调用
trace 'do_sys_open "%s", arg2' -f config

# 追踪do_sys_open调用，并打印返回值
# 前缀 r 用来定义一个return probe，关键字retval代表函数返回值
trace 'r::do_sys_open "%llx", retval'

# 追踪内核函数kfree_skb的0x12偏移量的指令
trace kfree_skb+0x12

# 追踪读取超过20000bytes的sys_read系统调用
# 注意：在4.17+，系统调用函数发生了重命名，例如 sys_read被重命名为 __x64_sys_read
trace 'sys_read (arg3 &gt; 20000) "read %d bytes", arg3'

#        中间这个bash表示包含目标函数的库或者二进制文件的名字。如果不指定二进制
#        文件的绝对路径，则它必须位于PATH中
trace 'r:bash:readline "%s", retval'

# 追踪共享库的导出函数，或者二进制文件导入的函数
trace'r:/usr/lib64/libtinfo.so:curses_version "Version=%s", retval'

# 追踪标记参数为42的、由libc发起的open调用
#      指定库名时不需要前缀lib
trace 'c:open (arg2 == 42) "%s %d", arg1, arg2'

# 追踪malloc调用，打印分配的内存大小
trace 'c:malloc "size = %d", arg1'

# 追踪通过write调用写到stdout的字节数
trace 'p:c:write (arg1 == 1) "writing %d bytes to STDOUT", arg3'

# 追踪失败的__kmalloc调用，也就是返回值为0的
trace 'r::__kmalloc (retval == 0) "kmalloc failed!"'

# 多个探针可以在单个命令中执行
#               返回值必须cast为int然后再和0比较，因为argN和retval的默认类型是64bit整数
#               可能溢出为负数了
trace 'r:c:read ((int)retval &lt; 0) "read failed: %d", retval' \
        'r:c:write ((int)retval &lt; 0) "write failed: %d", retval' -T

# 该工具也对内核tracepoint提供了基本的支持
# 前缀 t 用来定义一个tracepoint
trace 't:block:block_rq_complete "sectors=%d", args-&gt;nr_sector' -T
# 要想知道tracepoint的结构的格式，也就是       args 指针的格式，可以利用工具tplist
tplist -v block:block_rq_complete
#   block:block_rq_complete
#       dev_t dev;
#       sector_t sector;
#       unsigned int nr_sector;
#       int errors;
#       char rwbs[8];

# 越来越多的高层次的库，启用了基于USDT探针的内省支持。这些探针的用法就像内核的tracepoint
# 前缀 u 用来定义一个USDT
#                                %U表示将arg3解析为用户空间符号，类似的%K则解析为内核空间符号
trace 'u:pthread:pthread_create "%U", arg3' -T -C

# Ruby OpenJDK Node等平台，也都支持USDT
# 下面的例子追踪Ruby方法调用，需要Ruby构建时启用配置--enable-dtrace
#                                    类名   方法名
trace 'u:ruby:method__entry "%s.%s", arg1, arg2' -p $(pidof irb) -T

# 根据字符串来过滤
#                打开文件名中包含test.txt
trace 'p:c:open (STRCMP("test.txt", arg1)) "opening %s", arg1' -T

# 通过提供函数签名，来提升可读性
trace 'p:c:open(char *filename) "opening %s", filename'</pre>
<div class="blog_h3"><span class="graybg">argdist</span></div>
<p>追踪一个函数，并且打印它的参数汇总信息。</p>
<div class="blog_h3"><span class="graybg">funccount</span></div>
<p>对函数、追踪点、USDT探针进行计数。</p>
<div class="blog_h3"><span class="graybg">bpflist</span></div>
<p>显示使用了BPF程序和Map的进程。</p>
<div class="blog_h3"><span class="graybg">bashreadline</span></div>
<p>打印全局范围内，在bash中执行的所有命令。</p>
<div class="blog_h3"><span class="graybg">capable</span></div>
<p>追踪对内核函数<pre class="crayon-plain-tag">cap_capable()</pre>的调用，此函数负责进行security capability的检查。</p>
<div class="blog_h1"><span class="graybg">开发</span></div>
<p>完整的开发文档参考：<a href="https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md">https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md</a></p>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<p>要基于BCC开发BPF程序，你可以使用多种编程语言，例如Python、Lua或者CPP。不过，你通常都要在这些编程语言代码中，用字符串嵌入C语言的BPF程序片段。</p>
<div class="blog_h3"><span class="graybg">C++</span></div>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;fstream&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;

#include "bcc_version.h"
#include "BPF.h"

const std::string BPF_PROGRAM = R"(
int on_sys_clone(void *ctx) {
  bpf_trace_printk("Hello, World! Here I did a sys_clone call!\n");
  return 0;
}
)";

int main() {
  // 此类提供BPF各种操作接口
  ebpf::BPF bpf;
  // 初始化C编写的BPF程序片段
  auto init_res = bpf.init(BPF_PROGRAM);
  if (init_res.code() != 0) {
    std::cerr &lt;&lt; init_res.msg() &lt;&lt; std::endl;
    return 1;
  }
  // 打开此输入流用于查看BPF程序打印的日志
  std::ifstream pipe("/sys/kernel/debug/tracing/trace_pipe");
  std::string line;
  // 获取系统调用的函数名，4.17+给系统调用函数名加了前缀__x64_，这个函数会自动添加前缀
  std::string clone_fnname = bpf.get_syscall_fnname("clone");

  //                挂钩探针            到此函数        探针的BPF函数名
  auto attach_res = bpf.attach_kprobe(clone_fnname, "on_sys_clone");
  if (attach_res.code() != 0) {
    std::cerr &lt;&lt; attach_res.msg() &lt;&lt; std::endl;
    return 1;
  }

  std::cout &lt;&lt; "Starting HelloWorld with BCC " &lt;&lt; LIBBCC_VERSION &lt;&lt; std::endl;

  while (true) {
    if (std::getline(pipe, line)) {
      std::cout &lt;&lt; line &lt;&lt; std::endl;
      // 当pipe中没有更多数据可读，卸载探针
      auto detach_res = bpf.detach_kprobe(clone_fnname);
      if (detach_res.code() != 0) {
        std::cerr &lt;&lt; detach_res.msg() &lt;&lt; std::endl;
        return 1;
      }
      break;
    } else {
      std::cout &lt;&lt; "Waiting for a sys_clone event" &lt;&lt; std::endl;
      sleep(1);
    }
  }

  return 0;
}</pre>
<div class="blog_h3"><span class="graybg">python</span></div>
<pre class="crayon-plain-tag">from bcc import BPF

# This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()</pre>
<p>类似C++的ebpf::BPF类，Python中的BPF类提供了BPF操作的各种接口。</p>
<p>text参数是需要执行的C语言编写的BPF程序。</p>
<p><pre class="crayon-plain-tag">kprobe__sys_clone()</pre>这种命名约定，用于快捷的定义Kprobe，前缀<pre class="crayon-plain-tag">kprobe__</pre>表示这是一个Kprobe，后面的则是需要instrument的内核函数的名字。</p>
<p>Kprobe接收一个<pre class="crayon-plain-tag">ctx</pre>参数，这个例子中访问该参数，因此将其转型为<pre class="crayon-plain-tag">void*</pre>。</p>
<p>函数bpf_trace_printk()打开公共的内核调试追踪管道（kernel debug trace pipe）并写入消息，也就是/sys/kernel/debug/tracing/trace_pipe。此管道是公共的，也就是所有BPF程序都可能写入消息，导致输出混乱。此外此管道性能较差，因此可以考虑BPF_PERF_OUTPUT()。</p>
<p><pre class="crayon-plain-tag">trace_print()</pre>函数读取trace_pipe中的消息并打印。</p>
<div class="blog_h2"><span class="graybg">常用编程接口</span></div>
<div class="blog_h3"><span class="graybg">读取追踪管道字段</span></div>
<pre class="crayon-plain-tag">b = BPF(text=prog)
# ...
# 获得字段元组，而非字符串
(task, pid, cpu, flags, ts, msg) = b.trace_fields()</pre>
<div class="blog_h3"><span class="graybg">使用Map</span></div>
<pre class="crayon-plain-tag">// b = BPF(text="""
#include &lt;uapi/linux/ptrace.h&gt;

// 定义一个哈希表，使用默认的键值类型u64
BPF_HASH(last);

int do_trace(struct pt_regs *ctx) {
    u64 ts, *tsp, delta, key = 0;

    // 查找键值
    tsp = last.lookup(&amp;key);
    if (tsp != NULL) {
        //      此助手函数返回当前纳秒数
        delta = bpf_ktime_get_ns() - *tsp;
        if (delta &lt; 1000000000) {
            // output if time is less than 1 second
            bpf_trace_printk("%d\\n", delta / 1000000);
        }
        // 删除键值
        last.delete(&amp;key);
    }

    ts = bpf_ktime_get_ns();
    // 更新键值
    last.update(&amp;key, &amp;ts);
    return 0;
}
// """)
// b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")</pre>
<div class="blog_h3"><span class="graybg">使用perfbuf</span></div>
<p>由于bpf_trace_printk()性能较差，不适合在生产环境中使用。perfbuf可以作为其代替品。perfbuf本质上是BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。</p>
<p>使用BCC时，可以直接调用<pre class="crayon-plain-tag">BPF_PERF_OUTPUT()</pre>以使用perfbuf。</p>
<pre class="crayon-plain-tag">from bcc import BPF
# prog = """</pre><br />
<pre class="crayon-plain-tag">#include &lt;linux/sched.h&gt;

// 定义一个结构，作为输出的“事件”
struct data_t {
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
};
// 定义名为events的perfbuf
BPF_PERF_OUTPUT(events);

int hello(struct pt_regs *ctx) {
    struct data_t data = {};
    // 当前进程ID（内核角度的PID，对应用户空间的Thread ID），此ID存放在低32bit
    // 当前线程组ID（内核角度的Thread Group ID，对应用户空间的PID），此ID存放在高32bit
    // 这里直接赋值给u32，导致高32bit丢弃，存储线程ID
    // 对于多线程程序，它的所有线程共享TGID，如果需要区分不同线程，应当使用PID
    data.pid = bpf_get_current_pid_tgid();
    // 时间戳
    data.ts = bpf_ktime_get_ns();
    // 当前进程名
    bpf_get_current_comm(&amp;data.comm, sizeof(data.comm));

    // 提交事件到用户空间
    events.perf_submit(ctx, &amp;data, sizeof(data));

    return 0;
} </pre><br />
<pre class="crayon-plain-tag"># """

# 加载并挂钩BPF程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")

# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))

start = 0
# 处理事件的回调函数
def print_event(cpu, data, size):
    global start
    #       访问perfbuf 转换事件（data）为Python对象
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid,
        "Hello, perf_output!"))

# 打开perfbuf，设置回调函数
b["events"].open_perf_buffer(print_event)
while 1:
    # 阻塞式的拉取事件
    b.perf_buffer_poll()</pre>
<div class="blog_h3"><span class="graybg">生成直方图</span></div>
<pre class="crayon-plain-tag">from __future__ import print_function
from bcc import BPF
from time import sleep

# b = BPF(text="""</pre><br />
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blkdev.h&gt;

// 声明一个直方图Map
BPF_HISTOGRAM(dist);
//  kprobe__开头的，表示是Kprobe，后面的blk_account_io_done就是目标函数
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
	// 让直方图的指定索引上的计数增加
	// 将块IO数据长度取2为底的对数，作为索引值。这表示进行2^的桶分布
	// 打个比方，当前操作字节数为64KB，取对数得到6，因此增加下面的第6个桶的计数

	//      kbytes          : count     distribution
	//        0 -&gt; 1        : 3        |                                      |
	//        2 -&gt; 3        : 0        |                                      |
	//        4 -&gt; 7        : 211      |**********                            |
	//        8 -&gt; 15       : 0        |                                      |
	//       16 -&gt; 31       : 0        |                                      |
	//       32 -&gt; 63       : 0        |                                      |
	//       64 -&gt; 127      : 1        |                                      |
	//      128 -&gt; 255      : 800      |**************************************|
	dist.increment(bpf_log2l(req-&gt;__data_len / 1024));
	return 0;
}</pre><br />
<pre class="crayon-plain-tag"># """)

# header
print("Tracing... Hit Ctrl-C to end.")

# trace until Ctrl-C
try:
	sleep(99999999)
except KeyboardInterrupt:
	print()

# output
b["dist"].print_log2_hist("kbytes")</pre>
<div class="blog_h3"><span class="graybg">从文件加载BPF程序</span></div>
<pre class="crayon-plain-tag">b = BPF(src_file = "vfsreadlat.c")</pre>
<div class="blog_h3"><span class="graybg">tracepoint</span></div>
<p>要运行tracepoint BPF程序，你需要Linux内核4.7+版本。</p>
<pre class="crayon-plain-tag">// instrument内核中追踪带年random:urandom_read。追踪点具有稳定的API，应当尽可能使用追踪点
// 而不是kprobes
TRACEPOINT_PROBE(random, urandom_read) {
    // args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
    bpf_trace_printk("%d\\n", args-&gt;got_bits);
    return 0;
}</pre>
<p>所有可用的追踪点可以通过下面的命令获得：</p>
<pre class="crayon-plain-tag">tplist
# 或者
perf list</pre>
<p>追踪点可用参数可以通过下面的命令获得：</p>
<pre class="crayon-plain-tag">tplist -v random:urandom_read
# random:urandom_read
#     int got_bits;
#     int pool_left;
#     int input_left;</pre>
<div class="blog_h3"><span class="graybg">kprobe</span></div>
<p>下面的例子，追踪内核函数：</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/sched.h&gt;

struct key_t {
    u32 prev_pid;
    u32 curr_pid;
};

BPF_HASH(stats, struct key_t, u64, 1024);
//                                   第二个参数开始，对应被探测的内核函数的参数
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
    struct key_t key = {};
    u64 zero = 0, *val;

    key.curr_pid = bpf_get_current_pid_tgid();
    key.prev_pid = prev-&gt;pid;

    // 等价于 stats.increment(key)
    val = stats.lookup_or_try_init(&amp;key, &amp;zero);
    if (val) {
      (*val)++;
    }
    return 0;
}</pre>
<p>注册探针：</p>
<pre class="crayon-plain-tag">b.attach_kprobe(event="finish_task_switch", fn_name="count_sched")</pre>
<p>需要注意的一点是，你可以在探针的参数列表中，将被探测函数的参数列出来，并访问。 finish_task_switch的签名：</p>
<pre class="crayon-plain-tag">static struct rq *finish_task_switch(struct task_struct *prev)
	__releases(rq-&gt;lock);</pre>
<p>因此在探针中我们可以访问prev参数，代表被切换出去的进程。</p>
<div class="blog_h3"><span class="graybg">uprobe</span></div>
<p>下面的例子追踪用户空间函数strlen()。</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;

struct key_t {
    char c[80];
};
BPF_HASH(counts, struct key_t);

int count(struct pt_regs *ctx) {
    // 从上下文中读取strlen()的第一个参数
    if (!PT_REGS_PARM1(ctx))
        return 0;

    struct key_t key = {};
    u64 zero = 0, *val;

    // 将参数存入结构
    bpf_probe_read_user(&amp;key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
    // 将结构存入Map，如果没有对应的key则初始化
    val = counts.lookup_or_try_init(&amp;key, &amp;zero);
    // 增加计数
    if (val) {
      (*val)++;
    }
    return 0;
};</pre><br />
<pre class="crayon-plain-tag"># 挂钩到         库c      的strlen函数   钩子函数为count
b.attach_uprobe(name="c", sym="strlen", fn_name="count")</pre>
<div class="blog_h3"><span class="graybg">USDT </span></div>
<p>USDT即用户静态定义的追踪点（user statically-defined tracing point），相当于tracepoint的用户态版本。很多流行的软件、框架都预置了USDT。</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
int do_trace(struct pt_regs *ctx) {
    uint64_t addr;
    char path[128]={0};
    // 读取USDT探针的第6个参数，到addr变量
    bpf_usdt_readarg(6, ctx, &amp;addr);
    // 将addr作为指针看待，并且从它所在位置读取最多128字节的字符串
    bpf_probe_read_user(&amp;path, sizeof(path), (void *)addr);
    bpf_trace_printk("path:%s\\n", path);
    return 0;
};</pre><br />
<pre class="crayon-plain-tag"># 为指定的PID初始化USDT追踪
u = USDT(pid=int(pid))
# 挂钩do_trace到追踪点http__server__request
u.enable_probe(probe="http__server__request", fn_name="do_trace")

# 初始化BPF
b = BPF(text=bpf_text, usdt_contexts=[u])</pre>
<div class="blog_h2"><span class="graybg">工具源码分析</span></div>
<div class="blog_h3"><span class="graybg">disksnoop</span></div>
<pre class="crayon-plain-tag"># 在Python程序中定义一个内核常量，不需要给出头文件
REQ_WRITE = 1		# from include/linux/blk_types.h

# 加载BPF程序
# b = BPF(text=""" </pre><br />
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blkdev.h&gt;

// 哈希表        键的类型（值类型是默认的u64）
BPF_HASH(start, struct request *);

// KProbe的参数pt_regs，包含registers、BPF上下文
//                                    struct request *req是目标函数blk_start_request的第一个参数
void trace_start(struct pt_regs *ctx, struct request *req) {
	u64 ts = bpf_ktime_get_ns();
        // 直接使用请求参数的指针作为键，在追踪程序中这很常见。因为任何两个结构都不会有相同的地址
        // 可以安全的作为唯一键（注意内存被释放并重用的情况）
	start.update(&amp;req, &amp;ts);
}

void trace_completion(struct pt_regs *ctx, struct request *req) {
	u64 *tsp, delta;
        // 查找先前记录的时间戳
	tsp = start.lookup(&amp;req);
	if (tsp != 0) {
		// 获取消耗的时间
		delta = bpf_ktime_get_ns() - *tsp;
		// 打印本次块IO操作的信息
		//                           直接访问结构字段，BCC会自动将其转换为bpf_probe_read_kernel()调用
		//                           某些情况下BCC无法处理复杂的解引用操作，你可以手工调用该助手函数
		bpf_trace_printk("%d %x %d\\n", req-&gt;__data_len,
		    req-&gt;cmd_flags, delta / 1000);
		start.delete(&amp;req);
	}
}
 </pre><br />
<pre class="crayon-plain-tag"># """)
# 函数 void blk_start_request ( struct request * req); 开始请求驱动来处理块IO请求
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
# 函数 void blk_mq_start_request(struct request *rq); 高IOPS下Linux的块设备层形成了瓶颈
# 因此多核心系统下，3.13+添加的Multi queue特性可以减少块设备层的性能影响。传统块设备层有能力
# 处理万级别的IOPS，但是随着SSD这样的高性能设备的出现，其出现能力瓶颈。Multi queue的主要思想
# 是，为每个CPU核心设置一个请求队列，均衡核之间的负载，减少对请求队列的锁竞争
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
# 函数 void blk_account_io_done(struct request *req, u64 now); 在块设备IO完成后调用
# 此函数优于blk_account_io_completion()，后者可能在请求部分完成的情况下调用，调用可能发生多次，
# 这种现象会发生在loop设备/老的SCSI上
b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion")</pre>
<p>可以使用具有稳定接口的追踪点block:block_rq_issue、block:block_rq_complete代替上述Kprobe。</p>
<div class="blog_h3"><span class="graybg">vfsreadlat</span></div>
<p>所有统计延迟的监控程序，都是选择适当的键，然后在entry/return钩子中进行计时：</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;

BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);

int do_entry(struct pt_regs *ctx)
{
	u32 pid;
	u64 ts, *val;

	pid = bpf_get_current_pid_tgid();
	ts = bpf_ktime_get_ns();
	// 记录开始时间
	start.update(&amp;pid, &amp;ts);
	return 0;
}

int do_return(struct pt_regs *ctx)
{
	u32 pid;
	u64 *tsp, delta;

	pid = bpf_get_current_pid_tgid();
	tsp = start.lookup(&amp;pid);

	if (tsp != 0) {
		// 计算耗时
		delta = bpf_ktime_get_ns() - *tsp;
		// 更新桶计数
		dist.increment(bpf_log2l(delta / 1000));
		start.delete(&amp;pid);
	}

	return 0;
}</pre><br />
<pre class="crayon-plain-tag"># 从文件中加载BPF程序
b = BPF(src_file = "vfsreadlat.c")
# 挂钩的内核函数是vfs_read
b.attach_kprobe(event="vfs_read", fn_name="do_entry")
b.attach_kretprobe(event="vfs_read", fn_name="do_return")</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/bcc">基于BCC进行性能追踪</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/bcc/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Sysdig进行系统性能分析</title>
		<link>https://blog.gmem.cc/sysdig</link>
		<comments>https://blog.gmem.cc/sysdig#comments</comments>
		<pubDate>Sat, 19 Jan 2019 09:47:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=36135</guid>
		<description><![CDATA[<p>sysdig 简介 Sysdig是一个开源的系统性能分析工具，可以实现strace、tcpdump、lsof、top、iftop等工具所具有的功能。 如果需要源代码级别的、通用剖析工具，可以参考：利用perf剖析Linux应用程序。 安装 [crayon-69e2a1548aa33829300962/] 选项 -c 运行指定的chisel，如果chisel需要参数，则必须用--chisel=chiselname chiselargs形式-cl 列出可用的chisel，从./chisels, ~/.chisels,/usr/share/sysdig/chisels搜索-M 在指定秒数之后停止收集-n 在获取指定数量的事件之后停止收集-S 在捕获结束，例如列出top事件之后，打印汇总摘要-s 捕获IO缓冲的前N字节，默认80 基本用法 不加任何参数调用，每行显示一个捕获的事件，不断刷新。输出格式： [crayon-69e2a1548aa37851932275/] 可以将结果输出到文件而非控制台：[crayon-69e2a1548aa3a463592853-i/]，随后你可以读取先前保存的文件：[crayon-69e2a1548aa3c723699243-i/] 过滤器 可以使用过滤器对sysdig输出进行过滤，过滤器支持 <a class="read-more" href="https://blog.gmem.cc/sysdig">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/sysdig">使用Sysdig进行系统性能分析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">sysdig</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Sysdig是一个开源的系统性能分析工具，可以实现strace、tcpdump、lsof、top、iftop等工具所具有的功能。</p>
<p>如果需要源代码级别的、通用剖析工具，可以参考：<a href="/perf">利用perf剖析Linux应用程序</a>。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag"># CentOS 7
rpm --import https://s3.amazonaws.com/download.draios.com/DRAIOS-GPG-KEY.public
curl -s -o /etc/yum.repos.d/draios.repo http://download.draios.com/stable/rpm/draios.repo
# yum update
yum -y install epel-release

yum install kernel-devel-$(uname -r)
yum install sysdig
/usr/lib/dkms/dkms_autoinstaller start
sysdig-probe-loader</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;">-c 运行指定的chisel，如果chisel需要参数，则必须用--chisel=chiselname chiselargs形式<br />-cl 列出可用的chisel，从./chisels, ~/.chisels,/usr/share/sysdig/chisels搜索<br />-M 在指定秒数之后停止收集<br />-n 在获取指定数量的事件之后停止收集<br />-S 在捕获结束，例如列出top事件之后，打印汇总摘要<br />-s 捕获IO缓冲的前N字节，默认80</p>
<div class="blog_h2"><span class="graybg">基本用法</span></div>
<p>不加任何参数调用，每行显示一个捕获的事件，不断刷新。输出格式：</p>
<pre class="crayon-plain-tag"># 事件号 发生事件          CPU号   进程名(PID） 事件方向
#                                             事件类型 事件参数
1462218 16:40:38.068514659 3 &lt;NA&gt; (2941270) &gt; switch next=2941697(sysdig) pgft_maj=0 pgft_min=0 vm_size=0 vm_rss=0 vm_swap=0                                                             
1462220 16:40:38.068515560 6 java (2556857) &gt; futex addr=7F3B3888C598 op=129(FUTEX_PRIVATE_FLAG|FUTEX_WAKE) val=1                                                                        
1462222 16:40:38.068515997 5 java (2557004) &gt; futex addr=7F3AEC01C894 op=137(FUTEX_PRIVATE_FLAG|FUTEX_WAIT_BITSET) val=1                                                                 
1462224 16:40:38.068516460 6 java (2556857) &lt; futex res=0</pre>
<p>可以将结果输出到文件而非控制台：<pre class="crayon-plain-tag">sysdig -w result.dump</pre>，随后你可以读取先前保存的文件：<pre class="crayon-plain-tag">sysdig -r result.dump</pre></p>
<div class="blog_h2"><span class="graybg">过滤器</span></div>
<p>可以使用过滤器对sysdig输出进行过滤，过滤器支持 =、!=、contains等操作符</p>
<p>下面的命令列出可用的过滤器：</p>
<pre class="crayon-plain-tag">sysdig -l

----------------------
Field Class: fd

fd.num          # 文件描述符号
fd.type         # 文件描述符类型，支持的值：'file', 'directory', 'ipv4', 'ipv6', 'unix',
                # 'pipe', 'event', 'signalfd', 'eventpoll', 'inotify', 'signal fd'
fd.typechar     # 文件描述符类型简写，支持的值：f                 4       6       u
                #  p      e         s           l            i
                # o表示未知
fd.name         # 文件描述符全名，对于文件，即完整路径；对于套接字，即连接元组
fd.directory    # 文件描述符是文件，且它必须位于此目录下
fd.filename     # 文件描述符是文件，且它的basename是此值
fd.ip           # 匹配客户端或服务器IP地址
fd.cip          # 匹配客户端IP地址
fd.sip          # 匹配服务器IP地址
fd.lip          # 匹配本地地址
fd.rip          # 匹配远程地址
fd.port         # 匹配客户端或服务器端口
fd.cport        # 对于TCP/UDP，客户端端口
fd.sport        # 对于TCP/UDP，客户端端口
fd.lport        # 对于TCP/UDP，本地端口
fd.rport        # 对于TCP/UDP，远程端口
fd.l4proto      # 套接字协议，可选 'tcp', 'udp', 'icmp' or 'raw'
fd.sockfamily   # 套接字家族，可选 'ip' or 'unix'
fd.is_server    # 指定为true则用于套接字的进程必须是服务器端
fd.uid          # FD的唯一标识
fd.containername
                # container ID + FD name
fd.containerdirectory
                # container ID + directory name
fd.proto        # FD的协议
fd.cproto       # 对于TCP/UDP，客户端协议
fd.sproto       # 对于TCP/UDP，服务器协议
fd.lproto       # 对于TCP/UDP，本地协议
fd.rproto       # 对于TCP/UDP，远程协议
fd.net          # IP网络
fd.cnet         
fd.snet         
fd.lnet         
fd.rnet         
fd.connected    # 对于TCP/UDP，设置为true则仅仅匹配已连接的套接字

----------------------
Field Class: process

proc.pid        # 生成事件的进程的ID
proc.exe        # 第一个命令行参数，通常是可执行文件名
proc.name       # 生成事件的可执行文件的名称
proc.args       # 生成事件的进程的命令行参数
proc.env        # 环境变量 
proc.cmdline    # 完整命令行，也就是 proc.name + proc.args
proc.exeline    # 完整命令行，也就是 proc.exe + proc.args
proc.cwd        # 当前工作目录
proc.nthreads   # 进程包括的线程数量
proc.nchilds    # 进程包括的子线程数量（除去主线程）
proc.ppid       # 父进程ID
proc.pname      # 父进程名称
proc.pcmdline   # 父进程的cmdline
proc.apid       # 某个祖先进程的ID
proc.aname      # 某个祖先进程的名称
proc.loginshellid
                # 登陆Shell ID
proc.duration   # 进程已经启动的纳秒
proc.fdopencount
                # 进程打开的文件描述符数量
proc.fdlimit    # 进程能够打开的文件描述符数量
proc.fdusage    # 打开/最大文件描述符比率
proc.vmsize     # 进程虚拟内存大小，单位KB
proc.vmrss      # 驻留内存大小
proc.vmswap     # 交换到磁盘的内存大小
thread.pfmajor  # 进程启动以来主页面错误次数
thread.pfminor  # 进程启动以来次页面错误次数
thread.tid      # 线程ID
thread.ismain   # 如果指定为true，生成事件的线程必须是主线程
thread.exectime # 最后被调度的线程消耗的CPU纳秒数
thread.totexectime
                # 从捕获开始，当前线程使用的CPU纳秒总数
thread.cgroups  # 线程所属的Cgroup，聚合为单个字符串
thread.cgroup   # 线程所属的某个Cgroup子系统
thread.vtid     # 从产生事件的线程自己的PID命名空间看到的，自己的PID
proc.vpid       # 从产生事件的进程自己的PID命名空间看到的，自己的PID
thread.cpu      # 最近一秒消耗的CPU时间
thread.cpu.user # 最近一秒消耗的CPU时间（用户态）
thread.cpu.system
                # 最近一秒消耗的CPU时间（内核态）
thread.vmsize   # 对于主线程，即虚拟内存大小，对于非主线程，为0
proc.sid        # 会话ID
proc.sname      # 会话名称
proc.tty        # 进程的控制终端，0为无控制终端的进程
proc.exepath    # 可执行文件的完整路径
proc.vpgid      # 从产生事件的进程自己的PID命名空间看到的，自己的进程组ID

----------------------
Field Class: evt

evt.num         # 事件编号
evt.time        # 事件时间戳，包含纳秒部分
evt.time.s      # 事件时间戳，不包含纳秒
evt.datetime    # 事件时间戳，包含日期部分
evt.rawtime     # 原始时间戳，19700101到现在的纳秒数
evt.rawtime.s   # 原始时间戳，19700101到现在的秒数
evt.rawtime.ns  # 原始时间戳，小数部分（纳秒部分）
evt.reltime     # 从捕获到发生事件时流逝的时间
evt.reltime.s  
evt.reltime.ns  
evt.latency     # 配对的enter/exit事件的延迟，纳秒
evt.latency.s   
evt.latency.ns  
evt.latency.human
                # 可读格式，例如10.3ms
evt.deltatime   # 当前事件和上一个事件之间的延迟
evt.deltatime.s 
evt.deltatime.ns
evt.dir         # 事件方向，&gt;为enter事件，&lt;为exit事件
evt.type        # 事件（类型的）名称，例如open
evt.type.is     # evt.type.is.open匹配所有open事件
syscall.type    # 对于系统调用事件，指定系统调用名称，例如open
evt.category    # 事件分类，例如file表示文件操作包括open/close，net表示网络操作
                # memory表示mmap/brk等操作
evt.cpu         # 事件所在的CPU序号
evt.args        # 聚合为字符串的事件参数
evt.arg         # 单个事件参数，使用参数名或序号，例如evt.arg.fd或'evt.arg[0]
evt.res         # 事件结果字符串，如果成功SUCCESS，否则是错误码字符串，例如ENOENT 
evt.failed      # 如果true匹配失败事件
evt.is_io       # 匹配读写FD的事件，例如 read(), send, recvfrom()
evt.is_io_read  
evt.is_io_write 
evt.io_dir      # IO方向，r或w
evt.is_wait     # 如果true则匹配那些导致线程等待的事件，例如sleep(), select(), poll()
evt.wait_latency
                # 等待事件返回（exit）消耗的时间阈值
evt.is_syslog   # 事件是否是写入syslog
evt.is_open_read
                # 如果为true，匹配打开以进行读的open/openat事件
evt.is_open_write
                # 如果为true，匹配打开以进行写的open/openat事件

----------------------
Field Class: user

user.uid        # 用户ID
user.name       # 用户名称
user.homedir    # 家目录
user.shell      # 用户的Shell
user.loginuid   # 审计用户ID
user.loginname  # 审计用户名

----------------------
Field Class: group

group.gid       # 组ID
group.name      # 组名称

----------------------
Field Class: container

container.id    # 容器ID
container.name  # 容器名称
container.image # 容器镜像名称
container.image.id
                # 容器镜像ID
container.type  # 容器类型，例如docker, rkt
container.privileged
                # true则匹配运行在特权模式的容器
container.image.repository
container.image.tag
container.image.digest

----------------------
Field Class: k8s

k8s.pod.name    # Pod名称
k8s.pod.id      # Pod标识符
k8s.pod.label   # Pod的标签，例如k8s.pod.label.foo
k8s.pod.labels  # 逗号分隔的多个Pod标签
k8s.rc.name     # 复制控制器
k8s.rc.id       
k8s.rc.label    
k8s.rc.labels   
k8s.svc.name    # 服务
k8s.svc.id      
k8s.svc.label   
k8s.svc.labels  
k8s.ns.name     # 命名空间
k8s.ns.id       
k8s.ns.label    
k8s.ns.labels
k8s.rs.name     # 复制集
k8s.rs.id      
k8s.rs.label   
k8s.rs.labels   
                
k8s.deployment.name
                # Deployment
k8s.deployment.id
k8s.deployment.label
k8s.deployment.labels</pre>
<p>过滤特定进程产生的事件： </p>
<pre class="crayon-plain-tag"># 仅仅捕获sshd产生的事件
sysdig proc.name=sshd

# 读取Dump，仅仅显示mysqld产生的事件
sysdig -r result.dump proc.name=mysqld</pre>
<div class="blog_h2"><span class="graybg">Chisels</span></div>
<p>Sysdig中的chisels是一小段Lua脚本，用于分析sysdig事件流。Chisels支持：</p>
<ol>
<li>实时分析：每秒刷新一次</li>
<li>离线分析：会打印总体的汇总信息</li>
</ol>
<p>执行下面的命令列出可用的Chisels：<br /> </p>
<pre class="crayon-plain-tag">sysdig -cl

Category: Application
---------------------
httplog         # HTTP请求日志
httptop         # HTTP请求TOP
memcachelog     # memcache请求日志

Category: CPU Usage
-------------------
spectrogram     # OS延迟的可视化直方图
subsecoffset    # 
topcontainers_cpu
                # CPU占用最高的容器
topprocs_cpu    # CPU占用最高的进程

Category: Errors
----------------
topcontainers_error
                # 错误最多的容器
topfiles_errors # 错误最多的文件
topprocs_errors # 错误最多的进程

Category: I/O
-------------
echo_fds        # 打印进程读写的文件
fdbytes_by      # IO字节数，根据一个任意的过滤器字段进行聚合
fdcount_by      # FD计数，根据一个任意的过滤器字段进行聚合
fdtime_by       # FD时间
iobytes         # 任何类型的FD的IO字节数总和
iobytes_file    # 文件IO字节数总和
spy_file        # 回响任何进程对所有文件进行的任何读写，可选的，你可以提供一个文件名，这样只会拦截
                # 单个文件上发生的读写
stderr          # 打印进程的标准错误
stdin           # 打印进程的标准输入
stdout          # 打印进程的标准输出
topcontainers_file
                # 读写磁盘字节数最多的容器
topfiles_bytes  # 被读写字节数最多的文件
topfiles_time   # 被访问时间最长的文件
topprocs_file   # 读写磁盘字节数最多的进程

Category: Logs
--------------
spy_logs        # 回响任何进程对任何日志文件执行的写入
spy_syslog      # 打印任何写入到syslog的消息

Category: Misc
--------------
around          # 在指定过滤器匹配时，输出指定事件范围附近的事件

Category: Net
-------------
iobytes_net     # 显示总和网络IO字节数
spy_ip          # 显示和指定IP地址的数据交换情况
spy_port        # 显示和指定端口的数据交换情况
topconns        # IO字节数最多的网络链接
topcontainers_net
                # 导致网络IO最多的容器
topports_server # 读写字节数最多的TCP/UDP端口
topprocs_net    # 导致网络IO最多的进程

Category: Performance
---------------------
bottlenecks     # 最缓慢的系统调用
fileslower      # 跟踪缓慢的文件IO
netlower        # 跟踪缓慢的网络IO
proc_exec_time  # 显示进程执行时间
scallslower     # 跟踪缓慢的系统调用
topscalls       # 调用次数最多的系统调用
topscalls_time  # 消耗时间最多的系统调用

Category: Security
------------------
list_login_shells
                # 列出登陆Shell的ID
shellshock_detect
                # 打印 shellshock攻击
spy_users       # 显示交互式的用户活动

Category: System State
----------------------
lscontainers    # 列出运行中的荣iq
lsof            # 列出（可过滤）打开的文件描述符
netstat         # 列出（可过滤）打开的网络链接
ps              # 列出（可过滤）运行中的进程</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">网络分析</span></div>
<p>显示使用网络带宽最多的进程： </p>
<pre class="crayon-plain-tag">sysdig -c topprocs_net</pre>
<p>查看网络IO最高的进程： </p>
<pre class="crayon-plain-tag">sysdig -c topprocs_net                                                                                                                                         
# Bytes               Process             PID                                                                                                                                              
# --------------------------------------------------------------------------------                                                                                                         
# 38.48KB             msgr-worker-1       198488                                                                                                                                           
# 24.36KB             msgr-worker-0       198488                                                                                                                                           
# 8.12KB              msgr-worker-2       198488                                                                                                                                           
# 76B                 bird                3951084                                                                                                                                          
# 52B                 zabbix_agentd       5930                                                                                                                                             
 #31B                 zabbix_agentd       5931</pre>
<p>打印和目标主机之间的数据交换： </p>
<pre class="crayon-plain-tag"># 以二进制形式打印
sysdig -s2000 -X -c echo_fds fd.cip=10.5.39.41
# 以ASCII形式打印
sysdig -s2000 -A -c echo_fds fd.cip=192.168.0.1</pre>
<p>最繁忙服务器端口，按当前已建立连接数：</p>
<pre class="crayon-plain-tag"># 按服务器端口分组
sysdig -c fdcount_by fd.sport "evt.type=accept"</pre>
<p>最繁忙客户端地址，按收发字节数：</p>
<pre class="crayon-plain-tag">sysdig -c fdbytes_by fd.cip</pre>
<p>列出不是发往Apache的入站请求： </p>
<pre class="crayon-plain-tag">sysdig -p "%proc.name %fd.name" "evt.type=accept and proc.name!=httpd"</pre>
<div class="blog_h3"><span class="graybg">打开文件</span></div>
<p>捕获打开特定文件的进程： </p>
<pre class="crayon-plain-tag"># 针对/var/log文件的事件
sysdig fd.name=/var/log
# 事件参数fd中包含/var/log的事件
sysdig fd.name contains /var/log
# 7227884 16:52:37.116806685 4 svlogd (5880) &gt; write fd=6(&lt;f&gt;/var/log/calico/libnetwork/current) size=50 
# 7227885 16:52:37.116816800 4 svlogd (5880) &lt; write res=50 data=./run: exec: line 3: libnetwork-plugin: not found.</pre>
<div class="blog_h3"><span class="graybg">IO分析</span></div>
<p>下面的例子，显示被读写字节数最多的文件（瞬时）：</p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes

# Bytes               Filename                                                                                                                                                             
# --------------------------------------------------------------------------------                                                                                                         
# 2.53KB              /etc/hosts                                                                                                                                                           
# 585B                /sys/fs/cgroup/memory/kubepods/besteffort/pod56e20dda-38bf-11e9-99bd-44a8421fdcc8/memory.stat                                                                        
# 563B                /sys/fs/cgroup/memory/kubepods/besteffort/memory.stat                                                                                                                
# 116B                /sys/fs/cgroup/cpu,cpuacct/kubepods/besteffort/cpuacct.usage_percpu</pre>
<p>联用过滤器，仅仅显示/root下读写字节数最多的文件（瞬时）：</p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes "fd.name contains /root"</pre>
<p>联用过滤器，仅仅显示admin用户读写字节数最多的文件（瞬时）： </p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes "user.name=admin" </pre>
<p>显示延迟超过1ms的IO调用： </p>
<pre class="crayon-plain-tag">sysdig -c fileslower 1
# evt.datetime            proc.name    evt.type LATENCY(ms)  fd.name
# ----------------------- ------------ -------- ------------ -----------------------------------------
# 2019-03-25 11:15:54.119 node_exporte read               75 /host/sys/fs/xfs/sda2/stats/stats</pre>
<p>根据FD类型汇总IO数据量：</p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.type

# Bytes     fd.type
# ------------------------------
# 485.00M   file
# 4.63M     unix
# 509.92KB  pipe
# 99.83KB   ipv4
# 53.79KB   event
# 3.08KB    inotify</pre>
<p>根据目录汇总IO数据量，仅仅考虑文件IO： </p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.directory "fd.type=file"

# Bytes     fd.directory
# ------------------------------
# 101.89M   /tmp/
# 90.26M    /root/.ccache/tmp/
# 85.75M    /usr/include/c++/4.7.2/bits/
# 24.74M    /usr/include/c++/4.7.2/</pre>
<p>在特定目录下，汇总每个文件的IO数据量：  </p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.filename "fd.directory=/tmp/"

# Bytes     fd.filename
# ------------------------------
# 13.64M    ccJvb4Mi.s
# 9.33M     cc4pbJkV.s
# 6.72M     ccx9zir6.s</pre>
<p>发现导致特定目录下文件IO的进程： </p>
<pre class="crayon-plain-tag"># 根据进程名聚合
                                         # 逻辑与：IO的目标是/tmp/*.s文件
sysdig -r t.scap -c fdbytes_by proc.name "fd.directory=/tmp/ and fd.filename contains .s"

# Bytes     proc.name
# ------------------------------
# 50.57M    as
# 49.03M    cc1plus
# 1.54M     cc1
# 1B        httpd</pre>
<p>发现针对特定文件IO的调用栈： </p>
<pre class="crayon-plain-tag">sysdig -A -r t.scap -c echo_fds "fd.filename=ccJvb4Mi.s"

# ------ Write 4.00KB to /tmp/ccJvb4Mi.s
# .file"chisel.cpp"
# .text
# .Ltext0:
# .section.text._ZNSt9exceptionC2Ev,"axG",@p </pre>
<div class="blog_h3"><span class="graybg">容器</span></div>
<pre class="crayon-plain-tag"># CPU用量最大的进程
sysdig -pc -c topprocs_cpu
# 网络带宽用量最大的进程
sysdig -pc -c topprocs_net 
# 文件IO字节数最大
sysdig -pc -c topfiles_bytes
# 网络连接数最高
sysdig -pc -c topconns </pre>
<div class="blog_h1"><span class="graybg">csysdig</span></div>
<p>sysdig的图形化用户接口，类似于top或htop，但是功能要强大的多：</p>
<ol>
<li>支持实时分析，也支持分析sysdig跟踪文件。跟踪文件可以来自其它机器</li>
<li>支持CPU、内存、磁盘IO、网络IO等多方面指标的详细分析</li>
<li>支持跟踪进程、文件、网络连接的IO活动</li>
<li>可以钻取到进程、文件、网络连接</li>
<li>支持sysdig的过滤语法</li>
<li>支持容器和K8S</li>
</ol>
<p>csysdig基于view，每个view是一小段Lua脚本，负责收集指标并展示在屏幕上</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">apt-get install sysdig</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;"> -d 刷新间隔，默认2000<br />-pc  容器支持，显示额外的容器相关的字段<br />-k 启用K8S支持，指定API Server的URL，可用环境变量SYSDIG_K8S_API代替<br />-K 使用指定的凭证来访问K8S，可用环境变量SYSDIG_K8S_API_CERT代替<br />-N 不把端口号转换为名称<br />-r 读取跟踪文件</p>
<div class="blog_h2"><span class="graybg">用法</span></div>
<div class="blog_h3"><span class="graybg">基本用法</span></div>
<ol>
<li>不使用参数调用该命令，执行实时分析；使用-r来分析跟踪文件</li>
<li>按F2切换view</li>
<li>Enter可以钻取，Backspace可以退回</li>
<li>按F5可以显示选中条目的IO活动</li>
<li>按F6可以看到选中条目的sysdig事件</li>
</ol>
<div class="blog_h3"><span class="graybg">钻取</span></div>
<p>你可以钻取、上卷，快捷键分别为Enter、Backspace。例如你可以从进程钻取到线程，在不同的钻取级别，你都可以切换、查看其它view，非常强大</p>
<div class="blog_h3"><span class="graybg">动作</span></div>
<ol>
<li>进程视图：k杀死进程</li>
<li>容器视图：b打开Shell</li>
</ol>
<div class="blog_h3"><span class="graybg">光谱图</span></div>
<p>以系统调用为例，颜色越靠近红色，说明那个耗时区间的系统调用越多： </p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2008/04/csysdig-spectrograms.png"><img class="aligncenter size-full wp-image-22601" src="https://blog.gmem.cc/wp-content/uploads/2008/04/csysdig-spectrograms.png" alt="csysdig-spectrograms" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">输出字段</span></div>
<p>F2选择视图时，右侧会给出每个字段的含义</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/sysdig">使用Sysdig进行系统性能分析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/sysdig/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>利用perf剖析Linux应用程序</title>
		<link>https://blog.gmem.cc/perf</link>
		<comments>https://blog.gmem.cc/perf#comments</comments>
		<pubDate>Fri, 18 Jan 2019 02:07:43 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24443</guid>
		<description><![CDATA[<p>简介 剖析（Profiling）是一种有效的、细粒度的软件性能检查手段，大部分编程语言的生态圈都有各种性能剖析工具。本文的讨论内容与具体编程语言无关，而关注在Linux系统上对应用程序的性能进行剖析。 Linux内核实现了非常有价值的性能分析基础设施（perf infrastructure），可以用于剖析各种CPU或软件事件。这一功能由perf_events接口暴露，并提供[crayon-69e2a1548b368095395589-i/]这一用户空间工具，通过此工具，你可以发现以下问题的答案： 为什么内核使用太多的CPU，哪些代码使用了这些CPU时间 什么代码导致CPU二级缓存不命中 CPU是否因内存IO而卡顿 什么代码在分配内存，分配了多少 什么触发了TCP重传 某个内核函数是否被频繁调用 线程离开CPU的原因  perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中，需要安装才能使用。要想从剖析中获得更多内核相关信息，你需要符号（Symbol）和栈追踪，这可能需要安装额外的包，甚至使用特定选项重新编译你的内核。剖析用户空间代码时，也要求目标应用程序的调试信息（符号表）被保留。 工作模式 perf_events有三种工作模式： counting，在内核上下文对各种指标进行计数 sampling，对事件进行采样，并将性能数据存放到内核缓冲，然后异步的写入到perf.data bpf，4.4版本内核引入的新特性，允许在内核中执行一段用户自定义的代码，以执行高效的过滤、汇总 基于事件的采样 perf_events的采样是基于事件进行的。采样的周期以事件的数量来表示，而非基于时间。当目标事件计数溢出指定的数值，则产生一个采样。 样本中包含的信息取决于用户和工具指定的度量类型，但是最重要的信息是指令指针（instruction pointer），也就是程序被中断时所处的位置。 这种基于中断的采样，在现代处理器上存在刹车效应。也就是说，样本中记录的指针，和程序被中断以处理PMU事件时的指令指针，可能相隔数十个指令。 <a class="read-more" href="https://blog.gmem.cc/perf">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/perf">利用perf剖析Linux应用程序</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>剖析（Profiling）是一种有效的、细粒度的软件性能检查手段，大部分编程语言的生态圈都有各种性能剖析工具。本文的讨论内容与具体编程语言无关，而关注在Linux系统上对应用程序的性能进行剖析。</p>
<p>Linux内核实现了非常有价值的性能分析基础设施（perf infrastructure），可以用于剖析各种CPU或软件事件。这一功能由perf_events接口暴露，并提供<pre class="crayon-plain-tag">perf</pre>这一用户空间工具，通过此工具，你可以发现以下问题的答案：</p>
<ol>
<li>为什么内核使用太多的CPU，哪些代码使用了这些CPU时间</li>
<li>什么代码导致CPU二级缓存不命中</li>
<li>CPU是否因内存IO而卡顿</li>
<li>什么代码在分配内存，分配了多少</li>
<li>什么触发了TCP重传</li>
<li>某个内核函数是否被频繁调用</li>
<li>线程离开CPU的原因 </li>
</ol>
<p>perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中，需要安装才能使用。要想从剖析中获得更多内核相关信息，你需要符号（Symbol）和栈追踪，这可能需要安装额外的包，甚至使用特定选项重新编译你的内核。剖析用户空间代码时，也要求目标应用程序的调试信息（符号表）被保留。</p>
<div class="blog_h2"><span class="graybg">工作模式</span></div>
<p>perf_events有三种工作模式：</p>
<ol>
<li>counting，在内核上下文对各种指标进行计数</li>
<li>sampling，对事件进行采样，并将性能数据存放到内核缓冲，然后异步的写入到perf.data</li>
<li>bpf，4.4版本内核引入的新特性，允许在内核中执行一段用户自定义的代码，以执行高效的过滤、汇总</li>
</ol>
<div class="blog_h2"><span class="graybg">基于事件的采样</span></div>
<p>perf_events的采样是基于事件进行的。<span style="background-color: #c0c0c0;">采样的周期以事件的数量来表示</span>，而非基于时间。当目标事件计数<span style="background-color: #c0c0c0;">溢出指定的数值，则产生一个采样</span>。</p>
<p>样本中包含的信息取决于用户和工具指定的度量类型，但是最重要的信息是指令指针（instruction pointer），也就是程序被中断时所处的位置。</p>
<p>这种基于中断的采样，在现代处理器上存在<span style="background-color: #c0c0c0;">刹车效应</span>。也就是说，样本中记录的指针，和程序被中断以处理PMU事件时的指令指针，可能相隔数十个指令。</p>
<p>record子命令默认使用cycle事件，类似于定期采样。</p>
<div class="blog_h2"><span class="graybg">事件类型</span></div>
<p>perf_events可以捕获的事件类型包括：</p>
<ol>
<li>硬件事件，来自CPU自己或CPU的PMU（Performance Monitoring Unit，性能监控单元），包含一系列微架构事件例如时钟周期、L1缓存丢失等。具体支持的事件类型取决于CPU型号</li>
<li>软件事件，基于内核计数器的低级事件，例如CPU迁移、上下文切换、Minor Faults、Major Faults（页面错误）</li>
<li>追踪点事件，由内核中的ftrace实现，包括：
<ol>
<li>内核追踪点事件，静态的、内核级的追踪点，硬编码到内核</li>
<li>用户静态定义追踪（USDT），用户态应用程序硬编码的追踪点</li>
<li>动态追踪，软件可以被动态instrumented，在任何位置创建事件。对于内核软件，使用kprobes框架，对于用户软件，使用uprobes</li>
<li>定时追踪，以任意频率抓取快照，主要用于CPU剖析，工作机制是定期引发中断</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">动态追踪</span></div>
<p>要启用内核动态追踪，需要使用内核编译参数CONFIG_KPROBES=y、CONFIG_KPROBE_EVENTS=y。要追踪基于帧指针的内核栈，需要内核编译参数CONFIG_FRAME_POINTER=y。</p>
<p>要启用用户动态追踪，需要使用内核编译参数CONFIG_UPROBES=y、CONFIG_UPROBE_EVENTS=y</p>
<div class="blog_h3"><span class="graybg">事件限定符</span></div>
<p>事件有多种表示方式，最简单的是它的字符串表示。引用事件时，可以指定限定符：</p>
<pre class="crayon-plain-tag"># 剖析用户级、内核级的cycle事件
-e cycles
-e cycles:uk
# 仅用户级
-e cycles:u</pre>
<p>限定符包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 80px; text-align: center;">限定符</td>
<td style="text-align: center;">说明</td>
<td style="text-align: center;">示例</td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="color: #000000;">u</span></td>
<td>监控特权级别3, 2, 1 ，也就是用户态</td>
<td><span style="color: #000000;">event:u</span></td>
</tr>
<tr>
<td><span style="color: #000000;">k</span></td>
<td>监控特权级别0，也就是内核态</td>
<td>event:k</td>
</tr>
<tr>
<td>h</td>
<td>在虚拟化环境下监控Hypervisor事件</td>
<td>event:h</td>
</tr>
<tr>
<td>H</td>
<td>在虚拟化环境下监控宿主机事件</td>
<td>event:H</td>
</tr>
<tr>
<td>G</td>
<td>在虚拟化环境下监控客户机事件</td>
<td>event:G</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监控范围</span></div>
<p>perf可以在不同的范围收集性能数据：</p>
<ol>
<li>单线程（per-thread）：仅仅监控选择的线程。如果线程被调度失去CPU，对它的性能计数暂停，如果线程被调度到其它CPU，计数会存储并恢复，继续在新CPU上进行计数</li>
<li>单进程（ per-process）：进程的所有线程都被监控，计数被汇总</li>
<li>单CPU（ per-cpu）：仅仅监控单颗、或者选定的几颗CPU上的事件</li>
<li>全局（system-wide）：监控所有CPU上的事件</li>
</ol>
<div class="blog_h2"><span class="graybg">调试信息</span></div>
<div class="blog_h3"><span class="graybg">符号</span></div>
<p>和其它调试工具一样，perf需要符号信息，符号用于将内存地址转换为可读的函数、变量名称。没有符号信息，你会看到0x7ff3c1cddf29这样的HEX代码，无法阅读。</p>
<p>如果需要被剖析的软件通过包管理器安装，你需要找到并安装对应的调试包（可能以-dbgsym为后缀）。如果找不到调试包，可以考虑手工编译。</p>
<div class="blog_h3"><span class="graybg">JIT符号</span></div>
<p>Java、Node.js等基于虚拟机的语言，在自己的虚拟CPU上执行代码，因而具有自己管理栈和执行函数的方法。</p>
<p>为了通过perf来剖析这类语言的程序，你需要：</p>
<ol>
<li>对于Java，使用<a href="https://github.com/jvm-profiling-tools/perf-map-agent">perf-map-agent</a></li>
<li>对于Node.js，使用命令行选项<pre class="crayon-plain-tag">--perf_basic_prof</pre></li>
</ol>
<p>你可能看不到完整的Java栈，这是因为X86上的Hotspot忽略了帧指针（Frame pointer）。在8u60+版本以后，可以使用-XX:+PreserveFramePointer修复这一问题。</p>
<div class="blog_h3"><span class="graybg">栈追踪</span></div>
<p>为了方便剖析，编译时尽量保留帧指针，编译器的优化行为可能导致难以调试，但是很多情况下优化掉帧指针是默认行为。</p>
<p>缺少帧指针会导致无法看到完整的调用栈，解决此问题的方法有几种：</p>
<ol>
<li>使用DWARF（ELF文件通常使用的调试信息格式）信息来Unwind栈</li>
<li>使用LBR（Last Branch Record），这是处理器特性，因此可能不可用</li>
<li>保留帧指针</li>
</ol>
<p>下面是默认情况下编译（<span style="background-color: #c0c0c0;">-O2，忽略帧指针</span>）得到的OpenSSH的剖析结果：</p>
<pre class="crayon-plain-tag">57.14%     sshd  libc-2.15.so        [.] connect           
               |
               --- connect
                  |          
                  |--25.00%-- 0x7ff3c1cddf29
                  |          
                  |--25.00%-- 0x7ff3bfe82761
                  |          0x7ff3bfe82b7c
                  |          
                  |--25.00%-- 0x7ff3bfe82dfc
                   --25.00%-- [...]</pre>
<p>以<pre class="crayon-plain-tag">-fno-omit-frame-pointer</pre>重新编译后：</p>
<pre class="crayon-plain-tag">100.00%     sshd  libc-2.15.so   [.] __GI___connect_internal
               |
               --- __GI___connect_internal
                  |          
                  |--30.00%-- add_one_listen_addr.isra.0
                  |          add_listen_addr
                  |          fill_default_server_options
                  |          main
                  |          __libc_start_main
                  |          
                  |--20.00%-- __nscd_get_mapping
                  |          __nscd_get_map_ref
                  |          
                  |--20.00%-- __nscd_open_socket
                   --30.00%-- [...]</pre>
<p>可以看到符号都显示出来了，另外add_one_listen_addr的祖先帧也显示出来了。 </p>
<div class="blog_h3"><span class="graybg">关于内核</span></div>
<p>你需要使用<pre class="crayon-plain-tag">CONFIG_FRAME_POINTER=y</pre>来编译内核，以保留栈指针信息。</p>
<div class="blog_h3"><span class="graybg">DWARF</span></div>
<p>从内核3.9版本开始，缺失栈指针的问题有个变通的解决方案（对于用户空间的栈）——使用libunwind。libunwind基于dwarf，调用perf时传入--call-graph dwarf即可。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<p>在Ubuntu上可以这样安装：</p>
<pre class="crayon-plain-tag">apt install linux-tools-common</pre>
<p>系统可能会提示你安装其它的包，安装即可。</p>
<div class="blog_h1"><span class="graybg">子命令列表</span></div>
<p>perf支持一系列的子命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>annotate</td>
<td>读取perf.data并显示被注解的代码</td>
</tr>
<tr>
<td>bench</td>
<td>基准测试的框架</td>
</tr>
<tr>
<td>config</td>
<td>在配置文件中读写配置项</td>
</tr>
<tr>
<td>diff</td>
<td>读取perf.data并显示剖析差异</td>
</tr>
<tr>
<td>evlist</td>
<td>列出perf.data中的事件名称</td>
</tr>
<tr>
<td>inject</td>
<td>用于增强事件流的过滤器</td>
</tr>
<tr>
<td>kmem</td>
<td>跟踪/度量内核内存属性</td>
</tr>
<tr>
<td>kvm</td>
<td>跟踪/度量KVM客户机系统</td>
</tr>
<tr>
<td>list</td>
<td>显示符号化的事件列表</td>
</tr>
<tr>
<td>lock</td>
<td>分析锁事件</td>
</tr>
<tr>
<td>mem</td>
<td>分析内存访问</td>
</tr>
<tr>
<td>record</td>
<td>执行剖析</td>
</tr>
<tr>
<td>report</td>
<td>显示剖析结果</td>
</tr>
<tr>
<td>sched</td>
<td>分析调度器</td>
</tr>
<tr>
<td>stat</td>
<td>获取性能计数</td>
</tr>
<tr>
<td>top</td>
<td>显示成本最高的操作并动态刷新</td>
</tr>
<tr>
<td>trace</td>
<td>类似于strace的工具</td>
</tr>
<tr>
<td>probe</td>
<td>定义新的动态追踪点</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">stat</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>运行一个命令，并收集性能计数器。</p>
<p>对于任何支持计数的事件，perf都能够在进程执行期间对发生的事件进行计数，计数结果会直接打印在控制台上。</p>
<p>stat子命令可以在线程、进程、CPU、全局级别进行计数。默认情况下进程的所有线程、子进程的计数都被统计在内，可以通过-i选项修改此行为。</p>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p>参考record子命令。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag"># 如果不指定-e，则默认收集所有事件的计数器
perf stat [-e &lt;EVENT&gt; | --event=EVENT] [-a] &lt;command&gt;
perf stat [-e &lt;EVENT&gt; | --event=EVENT] [-a] -- &lt;command&gt; [&lt;options&gt;]</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># CPU性能计数器
perf stat command

# 详细的CPU性能计数器
perf stat -d command

# 收集指定进程的性能计数，直到Ctrl-C
perf stat -p PID

# 收集整个系统的性能计数，持续5秒
perf stat -a sleep 5
# Performance counter stats for 'system wide':
#      20010.623960      cpu-clock (msec)          #    4.000 CPUs utilized          
#            61,947      context-switches          #    0.003 M/sec                  
#             4,153      cpu-migrations            #    0.208 K/sec                  
#                99      page-faults               #    0.005 K/sec                  
#     5,003,068,069      cycles                    #    0.250 GHz                      (49.96%)
#     2,000,087,622      instructions              #    0.40  insn per cycle           (75.00%)
#       393,183,513      branches                  #   19.649 M/sec                    (75.05%)
#        24,391,051      branch-misses             #    6.20% of all branches          (74.99%)

#       5.003219775 seconds time elapsed


# 指定需要收集的几种计数器，持续10秒
perf stat -e cycles,instructions,cache-references,cache-misses,bus-cycles -a sleep 10

# CPU一级缓存计数器
perf stat -e L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores command

# TLB计数器
perf stat -e dTLB-loads,dTLB-load-misses,dTLB-prefetch-misses command

# CPU最后一级缓存计数器
perf stat -e LLC-loads,LLC-load-misses,LLC-stores,LLC-prefetches command

# 统计指定进程的系统调用
perf stat -e 'syscalls:sys_enter_*' -p PID

# 统计全局系统调用
perf stat -e 'syscalls:sys_enter_*' -a sleep 5

# 统计调度器相关事件
perf stat -e 'sched:*' -p PID
perf stat -e 'sched:*' -p PID sleep 10

# 统计Ext4相关事件
perf stat -e 'ext4:*' -a sleep 10

# 统计块IO事件
perf stat -e 'block:*' -a sleep 10

# 收集多种事件
perf stat -e cycles,instructions,cache-misses</pre>
<div class="blog_h1"><span class="graybg">top</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>动态显示执行成本最高的项。</p>
<p>默认情况下以cycles事件采样，样本最高的输出在最顶部。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 动态显示成本最高的地址和符号，不保存到文件
perf top -F 49
# 动态显示成本最高的命令
perf top -F 49 -ns comm,dso</pre>
<div class="blog_h1"><span class="graybg">record</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令可以启动一个命令，并对其进行剖析，然后把剖析数据记录到文件（默认perf.data）。按Ctrl - C可以随时结束剖析。</p>
<p>此命令可以在线程、进程、CPU、全局级别进行剖析。</p>
<p>record, report, annotate是一组相关的命名，通常的使用流程是：</p>
<ol>
<li>在被剖析机器上调用record录制数据</li>
<li>拷贝录制的perf.data，在任意机器上调用report、annotate进行分析</li>
</ol>
<p>该命令不是记录所有事件，还是进行采样。默认情况下采样基于cycle事件，也就是进行定期采样。</p>
<p>perf_events接口允许通过两种方式描述采样周期：</p>
<ol>
<li>period：事件发生的次数</li>
<li>frequency：每秒样本的平均个数</li>
</ol>
<p>perf默认使用第二种，具体来说对应到-F选项。-F 1000表示1000Hz，也就是每秒平均采样1000个。<span style="background-color: #c0c0c0;">内核会动态的调整采样周期</span>，以尽量满足需求。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf record [-e &lt;EVENT&gt; | --event=EVENT] [-l] [-a]    &lt;command&gt;
perf record [-e &lt;EVENT&gt; | --event=EVENT] [-l] [-a] -- &lt;command&gt; [&lt;options&gt;]</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-e</td>
<td>
<p>剖析的事件类型，也就是性能监控单元（Performance Monitoring Unit，PMU）类型。可以指定：</p>
<ol>
<li>事件名称，执行<pre class="crayon-plain-tag">perf list</pre>可以显示支持的事件列表</li>
<li>一组事件，以花括号包围<pre class="crayon-plain-tag">{event1,event2,...}</pre></li>
<li>其它形式</li>
</ol>
</td>
</tr>
<tr>
<td>--filter</td>
<td>事件过滤器，必须和指定了追踪点（tracepoint）事件的-e配合使用</td>
</tr>
<tr>
<td>--exclude-perf</td>
<td>不记录perf自己发起的事件</td>
</tr>
<tr>
<td>-a</td>
<td>
<p>使用Per-CPU模式，如果不指定-C，则相当于全局模式</p>
<p>如果指定-C，则可以选定若干CPU</p>
</td>
</tr>
<tr>
<td>-p</td>
<td>收集指定进程的事件，逗号分割的PID列表</td>
</tr>
<tr>
<td>-t</td>
<td>收集指定线程的事件，逗号分割的线程ID列表</td>
</tr>
<tr>
<td>-u</td>
<td>收集指定用户的进程的事件</td>
</tr>
<tr>
<td>-r</td>
<td>使用RT SCHED_FIFO优先级收集数据</td>
</tr>
<tr>
<td>-c</td>
<td>采样周期</td>
</tr>
<tr>
<td>-o</td>
<td>输出文件的名称</td>
</tr>
<tr>
<td>-i</td>
<td>不包括子任务的事件，不监控子进程、线程、子进程的线程</td>
</tr>
<tr>
<td>-F</td>
<td>以指定的频率剖析</td>
</tr>
<tr>
<td>-g</td>
<td>记录调用栈</td>
</tr>
<tr>
<td>--call-graph</td>
<td>
<p>收集调用栈使用的方法：</p>
<ol>
<li>fp，栈指针</li>
<li>dwarf，DWARF的调用帧信息（CFI）</li>
<li>lbr，Hardware Last Branch Record facility</li>
</ol>
<p>某些系统上，如果应用通过GCC的--fomit-frame-pointer参数构建，fp模式下可能只能看到匿名调用帧，可以用dwarf代替</p>
</td>
</tr>
<tr>
<td>-s</td>
<td>记录每个线程的事件计数器，配合<pre class="crayon-plain-tag">perf report -T</pre>使用</td>
</tr>
<tr>
<td>-d</td>
<td>记录样本地址</td>
</tr>
<tr>
<td>-T</td>
<td>记录样本时间戳</td>
</tr>
<tr>
<td>-P</td>
<td>记录样本周期</td>
</tr>
<tr>
<td>-C</td>
<td>仅仅从指定的CPU上收集样本，示例：<pre class="crayon-plain-tag">-C 0,2-3</pre></td>
</tr>
<tr>
<td>-G</td>
<td>仅仅记录指定控制组的事件</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">性能剖析</span></div>
<pre class="crayon-plain-tag"># 以99HZ的频率剖析指定的命令，默认情况下工作在Per-thread模式
perf record -F 99 command

# 以99HZ的频率剖析指定的PID
perf record -F 99 -p PID
# 以99HZ的频率剖析指定的PID，持续10秒
perf record -F 99 -p PID sleep 10
# 以99HZ的频率剖析整个系统
perf record -F 99 -ag -- sleep 10

# 基于事件发生次数，而非采样频率来指定采样周期
perf record -e retired_instructions:u -c 2000

# 进行栈追踪（通过Frame Pointer）
perf record -F 99 -p PID -g -- sleep 10
# 进行栈追踪（通过DWARF）
perf record -F 99 -p PID --call-graph dwarf sleep 10
# 全局性栈追踪
perf record -F 99 -ag -- sleep 10  # 4.11之前
perf record -F 99 -g -- sleep 10   # 4.11之后
# 追踪某个容器的栈
perf record -F 99 -e cpu-clock --cgroup=docker/1d567f4393190204...etc... -a -- sleep 10

# 每发生10000次L1缓存丢失，进行一次采样
perf record -e L1-dcache-load-misses -c 10000 -ag -- sleep 5
# 每发生以100次最后级别的CPU缓存丢失，进行一次采用
perf record -e LLC-load-misses -c 100 -ag -- sleep 5 

# 采样内核指令
perf record -e cycles:k -a -- sleep 5 
# 采用用户指令
perf record -e cycles:u -a -- sleep 5 
# 精确采样用户指令，基于PEBS
perf record -e cycles:up -a -- sleep 5</pre>
<div class="blog_h3"><span class="graybg">静态追踪</span></div>
<pre class="crayon-plain-tag"># 追踪新进程的创建
perf record -e sched:sched_process_exec -a

# 追踪上下文切换
perf record -e context-switches -a
perf record -e context-switches -c 1 -a
perf record -e context-switches -ag
# 追踪通过sched跟踪点的上下文切换
perf record -e sched:sched_switch -a

# 追踪CPU迁移
perf record -e migrations -a -- sleep 10

# 追踪所有connect调用（出站连接）
perf record -e syscalls:sys_enter_connect -ag
# 追踪所有accepts调用（入站连接）
perf record -e syscalls:sys_enter_accept* -ag

# 追踪所有块设备请求
perf record -e block:block_rq_insert -ag
# 追踪所有块设备发起和完成
perf record -e block:block_rq_issue -e block:block_rq_complete -a
# 追踪100KB（200扇区）以上的块操作完成
perf record -e block:block_rq_complete --filter 'nr_sector &gt; 200'
# 追踪所有同步写操作的完成
perf record -e block:block_rq_complete --filter 'rwbs == "WS"'
# 追踪所有写操作的完成
perf record -e block:block_rq_complete --filter 'rwbs ~ "*W*"'

# 采样Minor faults（RSS尺寸增加）
perf record -e minor-faults -ag
perf record -e minor-faults -c 1 -ag
# 采样页面错误
perf record -e page-faults -ag

# 追踪所有Ext4调用，并把结果写入到非Ext4分区
perf record -e 'ext4:*' -o /tmp/perf.data -a 

# 追踪kswapd唤醒事件
perf record -e vmscan:mm_vmscan_wakeup_kswapd -ag

# 添加Node.js USDT 探针，要求Linux 4.10+
perf buildid-cache --add `which node`
# 跟踪Node.js http__server__request USDT事件
perf record -e sdt_node:http__server__request -a </pre>
<div class="blog_h1"><span class="graybg">report</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令用于分析perf record生成的perf.data文件。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf report [-i &lt;file&gt; | --input=file]</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-i</td>
<td>指定输入文件</td>
</tr>
<tr>
<td>-n</td>
<td>显示每个符号的样本数量</td>
</tr>
<tr>
<td>-T</td>
<td>显示每个线程的事件计数器</td>
</tr>
<tr>
<td>--pid</td>
<td>仅仅显示指定PID的事件</td>
</tr>
<tr>
<td>--tid</td>
<td>仅仅显示指定TID的事件</td>
</tr>
<tr>
<td>-s</td>
<td>
<p>指定直方图的排序方式，逗号分隔多个排序方式，优先级降序</p>
<p style="padding-left: 30px;">comm 任务的命令<br />pid 任务的PID<br />dso 采样时执行的库或者模块的名称<br />symbol 采样时执行的函数<br />srcline,srcfile 代码文件和行数，需要DWARF调试信息<br />overhead  采样的执行成本<br />overhead_sys 采样在内核态的执行成本<br />overhead_us 采样在用户态的执行成本<br />sample 采样数量</p>
</td>
</tr>
<tr>
<td>-F</td>
<td>指定输出字段，逗号分隔多个字段</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">输出</span></div>
<div class="blog_h3"><span class="graybg">默认输出</span></div>
<p>默认情况下，样本根据函数（符号）分类，数量最多的样本位于最前面：</p>
<pre class="crayon-plain-tag">#  Overhead        Command  Shared Object                      Symbol
+  21.70%          swapper  [kernel.kallsyms]              [k] 0xffffffff8104f45a
+   8.89%  qemu-system-x86  [kernel.kallsyms]              [k] 0xffffffff810496f6
+   3.32%             Xorg  [kernel.kallsyms]              [k] 0xffffffff8172da33
+   2.96%       vmware-vmx  [kernel.kallsyms]              [k] 0xffffffff81121880
+   2.23%           chrome  chrome                         [.] 0x0000000004fdb6e6
+   2.17%             java  [kernel.kallsyms]              [k] 0xffffffff8109f02b
+   1.71%           chrome  [kernel.kallsyms]              [k] 0xffffffff8172d982
+   1.56%             Xorg  [nvidia]                       [k] _nv008054rm
+   1.55%             Xorg  [nvidia]                       [k] _nv014095rm</pre>
<p>各列含义如下：</p>
<ol>
<li>Overhead，在对应函数中采样到的样本计数</li>
<li> Command，样本从什么应用程序中采集到</li>
<li>Shared Object，样本来自的ELF镜像。如果：
<ol>
<li>应用程序是动态链接的，这里可能显示共享库的名称</li>
<li>如果样本来自内核空间，则<span style="background-color: #c0c0c0;">一律显示伪ELF镜像名“kernel.kallsyms”</span></li>
</ol>
</li>
<li>第4列是特权级别，点号表示用户级别，k、g、u、H分别表示内核级别、客户机内核级别、客户机用户级别、hypervisor</li>
<li>最后一列是符号名称，如果缺乏调试信息，则显示为0x开头的函数内存地址</li>
</ol>
<div class="blog_h3"><span class="graybg">指定分组规则</span></div>
<p>你可以指定多种分组规则，对样本数进行统计。</p>
<p>下面的例子根据共享对象分组：</p>
<pre class="crayon-plain-tag">perf report --sort=dso

+  48.05%  [kernel.kallsyms]                                                                                                                                                             
+   8.06%  libglib-2.0.so.0.4600.1                                                                                                                                                       
+   5.81%  libc-2.19.so                                                                                                                                                                  
+   4.93%  [nvidia]                                                                                                                                                                      
+   2.83%  libgobject-2.0.so.0.4600.1                                                                                                                                                    
+   2.39%  [kvm_intel]</pre>
<p>下面的例子根据CPU序号分组：</p>
<pre class="crayon-plain-tag">perf report --sort=cpu

+  17.10%  0                                                                                                                                                                             
+  15.21%  2                                                                                                                                                                             
+  14.76%  4                                                                                                                                                                             
+  12.81%  6                                                                                                                                                                             
+  11.98%  3                                                                                                                                                                             
+  10.00%  5                                                                                                                                                                             
+   9.85%  1                                                                                                                                                                             
+   8.28%  7</pre>
<div class="blog_h3"><span class="graybg">内核符号</span></div>
<p>perf不知道如何从压缩内核（vmlinuz）中抽取符号，如果你的内核保留了调试符号，则可以用-k来指出未压缩内核的位置：</p>
<pre class="crayon-plain-tag">perf report -k /tmp/vmlinux</pre>
<div class="blog_h3"><span class="graybg">Children/Self</span></div>
<p>如果在record时收集了调用链，则Overhead可以在Children、Self两个列中显示。Children显示子代函数的样本计数、Self显示函数自己的样本计数</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 报告perf.data
perf report

# 显示样本数量
perf report -n

# 树状结构显示，展开，可以追踪到高成本的执行路径
perf report --stdio
# 每行显示一个帧，扁平化显示，按成本降序排列
perf report --stdio -n -g folded</pre>
<div class="blog_h1"><span class="graybg">annotate</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令用于源码级别的分析。</p>
<div class="blog_h2"><span class="graybg">输出</span></div>
<p>所有被采样到的函数都会被反汇编，每个指令占据采样的比例会被输出。</p>
<div class="blog_h3"><span class="graybg">源码信息</span></div>
<p>如果应用程序以-ggdb编译，annotate还能够生成源码级别信息：</p>
<pre class="crayon-plain-tag">------------------------------------------------
 Percent |   Source code &amp; Disassembly of noploop
------------------------------------------------
         :
         :
         :
         :   Disassembly of section .text:
         :
         :   08048484 &lt;main&gt;:
         :   #include &lt;string.h&gt;
         :   #include &lt;unistd.h&gt;
         :   #include &lt;sys/time.h&gt;
         :
         :   int main(int argc, char **argv)
         :   {
    0.00 :    8048484:       55                      push   %ebp
    0.00 :    8048485:       89 e5                   mov    %esp,%ebp
[...]
    0.00 :    8048530:       eb 0b                   jmp    804853d &lt;main+0xb9&gt;
         :                           count++;
   14.22 :    8048532:       8b 44 24 2c             mov    0x2c(%esp),%eax
    0.00 :    8048536:       83 c0 01                add    $0x1,%eax
   14.78 :    8048539:       89 44 24 2c             mov    %eax,0x2c(%esp)
         :           memcpy(&amp;tv_end, &amp;tv_now, sizeof(tv_now));
         :           tv_end.tv_sec += strtol(argv[1], NULL, 10);
         :           while (tv_now.tv_sec &lt; tv_end.tv_sec ||
         :                  tv_now.tv_usec &lt; tv_end.tv_usec) {
         :                   count = 0;
         :                   while (count &lt; 100000000UL)
   14.78 :    804853d:       8b 44 24 2c             mov    0x2c(%esp),%eax
   56.23 :    8048541:       3d ff e0 f5 05          cmp    $0x5f5e0ff,%eax
    0.00 :    8048546:       76 ea                   jbe    8048532 &lt;main+0xae&gt;
[...]</pre>
<p>再次强调一下刹车现象，百分比信息可能不准确。</p>
<div class="blog_h3"><span class="graybg">内核符号</span></div>
<p>对于压缩内核，你可以指定非压缩版本内核的位置，以解析符号：</p>
<pre class="crayon-plain-tag">perf annotate -k /tmp/vmlinux -d symbol </pre>
<div class="blog_h1"><span class="graybg">list</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>显示支持的事件列表。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 显示所有事件
perf list

# 显示跟踪点sched下的事件
perf list 'sched:*'</pre>
<div class="blog_h1"><span class="graybg">probe</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>用于定义新的追踪点，实现动态追踪。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf probe [options] --add=PROBE [...]
perf probe [options] PROBE
perf probe [options] --del=[GROUP:]EVENT [...]
perf probe --list[=[GROUP:]EVENT]
perf probe [options] --line=LINE
perf probe [options] --vars=PROBEPOINT
perf probe [options] --funcs</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">动态追踪</span></div>
<pre class="crayon-plain-tag"># 为tcp_sendmsg函数添加进入追踪点，--add可以省略
perf probe --add tcp_sendmsg

# 移除tcp_sendmsg进入追踪点
perf probe -d tcp_sendmsg

# 为tcp_sendmsg函数添加退出追踪点
perf probe 'tcp_sendmsg%return'

# 显示tcp_sendmsg的可用变量，需要debuginfo
perf probe -V tcp_sendmsg
# 显示tcp_sendmsg的可用变量和外部变量
perf probe -V tcp_sendmsg --externs

# 显示tcp_sendmsg可用的行探针
perf probe -L tcp_sendmsg

# 显示tcp_sendmsg的81行可用的探针
perf probe -V tcp_sendmsg:81

# 为tcp_sendmsg添加进入进入追踪点，使用3个参数寄存器（依赖于平台）
perf probe 'tcp_sendmsg %ax %dx %cx'

# 为tcp_sendmsg添加进入进入追踪点，且将cx寄存器设置别名bytes（依赖于平台）
perf probe 'tcp_sendmsg bytes=%cx'

# 追踪tcp_sendmsg，要求%cx变量大于100
perf record -e probe:tcp_sendmsg --filter 'bytes &gt; 100'

# 捕获返回值
perf probe 'tcp_sendmsg%return $retval'

# 为tcp_sendmsg和它的入口参数size添加追踪点，需要debuginfo，但是不依赖平台
perf probe 'tcp_sendmsg size'

# 为tcp_sendmsg和size、Socket状态添加追踪点
perf probe 'tcp_sendmsg size sk-&gt;__sk_common.skc_state'

# size大于0并且套接字状态不为TCP_ESTABLISHED时追踪tcp_sendmsg
perf record -e probe:tcp_sendmsg --filter 'size &gt; 0 &amp;&amp; skc_state != 1' -a

# 在tcp_sendmsg的81行添加追踪点，使用局部变量seglen
perf probe 'tcp_sendmsg:81 seglen'

# 为libc的用户态malloc函数添加探针
perf probe -x /lib64/libc.so.6 malloc

# 列出可用的动态探针
perf probe -l?</pre>
<div class="blog_h1"><span class="graybg">inject</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>该命令使用额外的信息来增强事件流。</p>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-b</td>
<td>注入build-ids</td>
</tr>
<tr>
<td>-v</td>
<td>冗长输出</td>
</tr>
<tr>
<td>-i</td>
<td>指定输入文件</td>
</tr>
<tr>
<td>-o</td>
<td>指定输出文件</td>
</tr>
<tr>
<td>-s</td>
<td>合并sched_stat、sched_switch，以获得任务在何处睡眠、睡眠了多久</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">剖析示例</span></div>
<div class="blog_h2"><span class="graybg">零散示例</span></div>
<div class="blog_h3"><span class="graybg">全局剖析</span></div>
<p>进行10秒钟的剖析：</p>
<pre class="crayon-plain-tag">perf record -a -g sleep 10</pre>
<p>根据应用程序的名称进行分组，整体上根据样本的执行成本进行排序：</p>
<pre class="crayon-plain-tag">perf report --sort comm,dso</pre>
<p>可以展开执行成本高的项，追踪到CPU时间都消耗在哪里了：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png"><img class="aligncenter size-full wp-image-24461" src="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png" alt="perf" width="1004" height="348" /></a><a href="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png"><br /></a>对于上面这个例子，可以看到，kubelet消耗的资源主要用于Prometheus相关的指标收集，以及grpc的客户端。</p>
<div class="blog_h3"><span class="graybg">CPU性能计数</span></div>
<pre class="crayon-plain-tag">perf stat -- tar xzf ideaIU-2018.3.3.tar.gz 

 Performance counter stats for 'tar xzf ideaIU-2018.3.3.tar.gz':
    # 反映CPU利用率
       9725.627068 task-clock (msec)         #    0.941 CPUs utilized          
    # 上下文切换数量
            45,568 context-switches          #    0.005 M/sec                  
             1,370 cpu-migrations            #    0.141 K/sec        
    # 页面错误数量          
               623 page-faults               #    0.064 K/sec                  
    # 经过的时钟周期的数量
    36,249,427,884 cycles                    #    3.727 GHz                    
    # IPC，每时钟周期指令数（instructions per cycle），通常越高越好，表示工作处理过程是优化的
    # 但是，最好还是检查实际执行的指令，Spin loop就是高指令频率但是实际上什么事都不做的反例
    46,378,956,536 instructions              #    1.28  insns per cycle
     # 分支和分支预测失败
     8,274,686,762 branches                  #  850.813 M/sec                  
       214,332,307 branch-misses             #    2.59% of all branches        

      10.334029429 seconds time elapsed</pre>
<div class="blog_h3"><span class="graybg">定期采样</span></div>
<p>perf_events支持使用固定频率来采样指令指针或栈追踪，进而分析CPU使用。采样频率通过-F选项指定。</p>
<p>频率可以选用99，而非100HZ，以避免采样和某些周期性活动的步调完全一致而导致结果失真。如果需要更高分辨率的采样，可以使用997这样的频率，但是注意频率越高剖析本身的成本也越高。</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 以99HZ频率，全局的采样30秒
perf record -F 99 -a -g -- sleep 30</pre>
<div class="blog_h3"><span class="graybg">硬件计数器触发采样</span></div>
<p>除了定时触发（固定频率采样），采样也可以由特定的CPU硬件计数器触发。使用这些计数器可以分析缓存丢失、memory stall cycles（由于访问内存而导致的时钟周期浪费，内存访问速度相对CPU时钟周期要慢的多）等更低级别的处理器事件。</p>
<p>可用的事件列表：</p>
<pre class="crayon-plain-tag">perf list | grep Hardware
  cpu-cycles OR cycles                               [Hardware event]
  instructions                                       [Hardware event]
  cache-references                                   [Hardware event]
  cache-misses                                       [Hardware event]
  branch-instructions OR branches                    [Hardware event]
  branch-misses                                      [Hardware event]
  bus-cycles                                         [Hardware event]
  ref-cycles                                         [Hardware event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-dcache-store-misses                             [Hardware cache event]
  L1-dcache-prefetch-misses                          [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  LLC-loads                                          [Hardware cache event]
  LLC-stores                                         [Hardware cache event]
  LLC-prefetches                                     [Hardware cache event]
  dTLB-loads                                         [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  dTLB-stores                                        [Hardware cache event]
  dTLB-store-misses                                  [Hardware cache event]
  iTLB-loads                                         [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  mem:&lt;addr&gt;[:access]                                [Hardware breakpoint]</pre>
<p>如果每当发生这些事件，就记录栈追踪，对系统性能有严重的影响。因此，通常使用-c选项，当计数器增加一定数量后，记录一次栈追踪：</p>
<pre class="crayon-plain-tag">perf record -e L1-dcache-load-misses -c 10000 -ag -- sleep 5</pre>
<p>上面这个例子，每当L1缓存丢失10000次，记录一次栈追踪。</p>
<div class="blog_h3"><span class="graybg">统计系统调用</span></div>
<p>下面的命令统计所有调用次数大于0的系统调用的数量：</p>
<pre class="crayon-plain-tag">perf stat -e 'syscalls:sys_enter_*' tar xzf ideaIU-2018.3.3.tar.gz 2&gt;&amp;1 | awk '$1 != 0'
 Performance counter stats for 'tar xzf ideaIU-2018.3.3.tar.gz':

                 2      syscalls:sys_enter_statfs                                   
            12,417      syscalls:sys_enter_utimensat                                   
  ...                                 
            12,417      syscalls:sys_enter_unlinkat                                   
  ...                                
            12,432      syscalls:sys_enter_newfstat                                   
                 1      syscalls:sys_enter_lseek                                    
           184,818      syscalls:sys_enter_read                                     
           192,154      syscalls:sys_enter_write                                    
                12      syscalls:sys_enter_access                                   
            12,417      syscalls:sys_enter_fchmod                                   
            12,417      syscalls:sys_enter_fchown                                   
                26      syscalls:sys_enter_open                                     
            24,834      syscalls:sys_enter_openat                                   
            12,442      syscalls:sys_enter_close                                                                 
  ...
      38.365080478 seconds time elapsed</pre>
<p>可以看到，主要的系统调用都是read/write。</p>
<p>使用strace命令可以获得类似的结果，但是strace的成本要高的多。测试结果显示，perf可能让程序变慢2.5倍，而strace则会让程序变慢高达60倍。</p>
<div class="blog_h3"><span class="graybg">统计新建进程 </span></div>
<pre class="crayon-plain-tag">perf record -e sched:sched_process_exec -a
# Ctrl-C
perf report -n --sort comm --stdio

# Overhead       Samples  Command
# ........  ............  .......
#
    32.14%             9  ionice 
    32.14%             9  nice   
    21.43%             6  du     
    10.71%             3  find   
     3.57%             1  ipset</pre>
<p>追踪点sched:sched_process_exec在某个进程调用exec()产生另外一个进程时生效，这通常是新进程的产生方式。但是这个例子的结果不一定准确，原因是：</p>
<ol>
<li>进程可能fork()出子进程，追踪点sched:sched_process_fork可以用于这种情况</li>
<li>应用程序可以reexec —— 针对自己再次调用exec() —— 这种用法可以清空地址空间，但是不会产生新进程</li>
</ol>
<div class="blog_h3"><span class="graybg">追踪出站连接</span></div>
<p>某些时候你希望了解服务器发起了哪些网络连接，是哪些进程发起的，为何发起。这些网络连接可能是延迟的根源。</p>
<p>追踪connect系统调用，即可了解出站连接：</p>
<pre class="crayon-plain-tag">perf record -e syscalls:sys_enter_connect -ag
perf report --stdio

# Children      Self  Trace output
# ........  ........  ............................................................
#
     5.88%     5.88%  fd: 0x00000005, uservaddr: 0xc4206fa390, addrlen: 0x0000001b
            |
            ---0x894812ebc0312874
               0xc42008e000
               runtime.main
               main.main
               github.com/projectcalico/node/pkg/readiness.Run
               github.com/projectcalico/node/pkg/readiness.checkBIRDReady
               github.com/projectcalico/node/pkg/readiness/bird.GRInProgress
               net.Dial
               net.(*Dialer).Dial
               net.(*Dialer).DialContext
               net.dialSerial
               net.dialSingle
               net.dialUnix
               net.unixSocket
               net.socket
               net.(*netFD).dial
               net.(*netFD).connect
               syscall.Connect
               syscall.Syscall</pre>
<p>可以看到，占比最高的出站连接是由Calico Node容器发起的。</p>
<div class="blog_h3"><span class="graybg">追踪套接字缓冲</span></div>
<p>通过剖析套接字缓冲的使用情况，也是定位到什么代码使用了最多的网络I/O的一种方法：</p>
<pre class="crayon-plain-tag">perf record -e 'skb:consume_skb' -ag</pre>
<div class="blog_h3"><span class="graybg">睡眠时间分析</span></div>
<p>使用perf可以了解应用程序为何休眠、在何处休眠、休眠多久。主要通过收集sched_stat、sched_switch事件实现。</p>
<p>剖析的目标代码如下：</p>
<pre class="crayon-plain-tag">for (i = 0; i &lt;  10; i++) {
        ts1.tv_sec = 0;
        ts1.tv_nsec = 10000000;
        // 休眠
        nanosleep(&amp;ts1, NULL);

        tv1.tv_sec = 0;
        tv1.tv_usec = 40000;
        // 休眠
        select(0, NULL, NULL, NULL,&amp;tv1);
}</pre>
<p>执行下面的命令开始剖析：</p>
<pre class="crayon-plain-tag">./perf record -e sched:sched_stat_sleep -e sched:sched_switch  -e sched:sched_process_exit -g -o perf.data.raw  sleep_test</pre>
<p>合并sched_start、sched_switch事件：</p>
<pre class="crayon-plain-tag">perf inject -v -s -i perf.data.raw -o perf.data</pre>
<p>报告：</p>
<pre class="crayon-plain-tag">perf report --stdio --show-total-period -i perf.data

  100.00%     502408738      foo  [kernel.kallsyms]  [k] __schedule
               |
               --- __schedule
                   schedule
                  |          
                  |--79.85%-- schedule_hrtimeout_range_clock
                  |          schedule_hrtimeout_range
                  |          poll_schedule_timeout
                  |          do_select
                  |          core_sys_select
                  |          sys_select
                  |          system_call_fastpath
                  |          __select
                  |          __libc_start_main
                  |          
                   --20.15%-- do_nanosleep
                             hrtimer_nanosleep
                             sys_nanosleep
                             system_call_fastpath
                             __GI___libc_nanosleep
                             __libc_start_main</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/perf">利用perf剖析Linux应用程序</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/perf/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ubuntu的时钟同步</title>
		<link>https://blog.gmem.cc/ntp-under-ubuntu</link>
		<comments>https://blog.gmem.cc/ntp-under-ubuntu#comments</comments>
		<pubDate>Thu, 27 Sep 2018 05:54:01 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=22793</guid>
		<description><![CDATA[<p>NTP 这是一个网络协议，用于通过网络来同步系统时钟。 在Ubuntu下，你可以使用timedatectl、timedatectl（从16.04开始，用于代替ntpdate / ntp）执行时钟同步，或者安装chrony以成为NTP服务器。 timesyncd默认已经安装，用来代替ntpdate以及chrony的客户端部分。 客户端 timedatectl 要查看当前时间、时间配置，可以执行： [crayon-69e2a1548bd11202034244/] 要启用时钟同步，执行： [crayon-69e2a1548bd15862622284/] 显示可用的时区： [crayon-69e2a1548bd18433554371/] 修改时区： [crayon-69e2a1548bd1a797918483/] timesyncd 这是负责执行时钟同步的后台服务： [crayon-69e2a1548bd1c857126037/] 配置文件位置是 /etc/systemd/timesyncd.conf、/timesyncd.conf.d/ 配置文件示例： <a class="read-more" href="https://blog.gmem.cc/ntp-under-ubuntu">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ntp-under-ubuntu">Ubuntu的时钟同步</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">NTP</span></div>
<p>这是一个网络协议，用于通过网络来同步系统时钟。</p>
<p>在Ubuntu下，你可以使用timedatectl、timedatectl（从16.04开始，用于代替ntpdate / ntp）执行时钟同步，或者安装chrony以成为NTP服务器。</p>
<p>timesyncd默认已经安装，用来代替ntpdate以及chrony的客户端部分。</p>
<div class="blog_h1"><span class="graybg">客户端</span></div>
<div class="blog_h2"><span class="graybg">timedatectl</span></div>
<p>要查看当前时间、时间配置，可以执行：</p>
<pre class="crayon-plain-tag">timedatectl status
      Local time: 四 2018-10-11 13:57:39 CST
  Universal time: 四 2018-10-11 05:57:39 UTC
        RTC time: 四 2018-10-11 05:57:39
       Time zone: Asia/Shanghai (CST, +0800)
# 启用时钟同步后，显示为yes
 Network time on: yes
# 如果和远程NTP服务器成功同步，显示为yes
NTP synchronized: yes
 RTC in local TZ: no</pre>
<p>要启用时钟同步，执行：</p>
<pre class="crayon-plain-tag">timedatectl set-ntp true</pre>
<p>显示可用的时区：</p>
<pre class="crayon-plain-tag">timedatectl list-timezones | grep Shanghai</pre>
<p>修改时区：</p>
<pre class="crayon-plain-tag">timedatectl set-timezone Asia/Shanghai</pre>
<div class="blog_h2"><span class="graybg">timesyncd</span></div>
<p>这是负责执行时钟同步的后台服务：</p>
<pre class="crayon-plain-tag">systemctl status systemd-timesyncd.service</pre>
<p>配置文件位置是 /etc/systemd/timesyncd.conf、/timesyncd.conf.d/</p>
<p>配置文件示例：</p>
<pre class="crayon-plain-tag">[Time]
NTP=0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org
FallbackNTP=ntp.ubuntu.com</pre>
<div class="blog_h3"><span class="graybg">检查状态</span></div>
<pre class="crayon-plain-tag">systemctl status systemd-timesyncd.service 

# 同步失败
10月 11 13:49:56 Carbon systemd-timesyncd[730]: Timed out waiting for reply from 10.0.8.1:123 (ntp.gmem.cc). 

# 同步成功
10月 11 13:55:22 Carbon systemd-timesyncd[44163]: Synchronized to time server 10.0.8.1:123 (ntp.gmem.cc). </pre>
<div class="blog_h1"><span class="graybg">服务器</span></div>
<p>要成为NTP服务器，可以安装chrony、ntpd，或者open-ntp。推荐chrony。</p>
<p><span style="background-color: rgb(192, 192, 192);">Chrony也可以作为NTP客户端，因为它可以从其它NTP服务器同步时间</span>。</p>
<div class="blog_h2"><span class="graybg">安装 </span></div>
<pre class="crayon-plain-tag">apt install -y chrony</pre>
<p>上面的命令会安装两个程序：chronyd为NTP服务守护进程，chronyc为控制chronyd的命令行接口。</p>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>配置文件路径为 /etc/chrony/chrony.conf，示例：</p>
<pre class="crayon-plain-tag"># 上游服务器列表
#                        该参数表示在头四次 NTP 请求以 2s 或者更短的间隔，而不是以 minpoll x 
#                        指定的最小间隔，这样的设置可以让 chronyd 启动时快速进行一次同步
#                        其它参数   minpoll x = 6      maxpoll x = 9
server 0.cn.pool.ntp.org iburst
server 1.cn.pool.ntp.org iburst
server 2.cn.pool.ntp.org iburst
server 3.cn.pool.ntp.org iburst
# 要使用本地NTP服务器，可以将上面的删除，然后添加自己的：
server 10.0.0.1 iburst

# 作为服务器运行
# 允许哪些客户端访问当前NTP服务器
allow
allow 192.168.1.0/24

# 即使当前服务器无法联系到上游，也允许它对外提供时钟服务
local stratum 10


# 启用内核时间与 RTC 时间同步，自动写回硬件
rtcsync</pre>
<p>配置完毕后，需要重启守护程序：</p>
<pre class="crayon-plain-tag">systemctl daemon-reload
systemctl restart chrony.service</pre>
<div class="blog_h2"><span class="graybg">chronyc</span></div>
<pre class="crayon-plain-tag"># 查看 NTP 服务器的在线和离线状态
chronyc activity

# 查看 Chrony 服务的日志
journalctl -u chronyd

# 检查 NTP 访问是否对特定主机可用
chronyc accheck

# 该命令会显示有多少 NTP 源在线/离线
chronyc activity

# 手动添加一台新的 NTP 服务器
chronyc add server

# 在客户端报告已访问到服务器
chronyc clients

# 手动移除 NTP 服务器或对等服务器
chronyc delete

# 手动设置守护进程时间
chronyc settime

# 校准时间服务器，显示系统时间信息
chronyc tracking

# 检查 NTP 访问是否对特定主机可用
chronyc accheck

# 查看时间同步源
chronyc sources -v

# 查看时间同步源状态
chronyc sourcestats -v</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ntp-under-ubuntu">Ubuntu的时钟同步</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ntp-under-ubuntu/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用sysrqd进行远程控制</title>
		<link>https://blog.gmem.cc/remote-ctrl-with-sysrqd</link>
		<comments>https://blog.gmem.cc/remote-ctrl-with-sysrqd#comments</comments>
		<pubDate>Sat, 16 Jun 2018 03:27:48 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=21777</guid>
		<description><![CDATA[<p>简介 sysrqd是一个简单的守护程序，允许你透过网络发送SysRQ快捷键。某些情况下，远程机器网络可以连通，但是SSH无法登录，可以利用sysrqd强制重启。 安装 [crayon-69e2a1548c038051005144/] 使用 sysrqd默认监听4094端口，在客户机上，使用telnet登录： [crayon-69e2a1548c03c367530297/] 根据提示输入密码。然后输入SysRQ快捷键即可。输入q可以退出telnet。 常用SysQR快捷键 按键 说明 b 立即重启，不卸载文件系统、不sync页面缓存 c 触发一次系统崩溃 e 向除了init进程（PID 1）之外的所有进程发送SIGTERM。包括sysrqd i 向除了init进程（PID 1）之外的所有进程发送SIGKILL。包括sysrqd m <a class="read-more" href="https://blog.gmem.cc/remote-ctrl-with-sysrqd">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/remote-ctrl-with-sysrqd">使用sysrqd进行远程控制</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">简介</span></div>
<p><a href="https://github.com/jd/sysrqd">sysrqd</a>是一个简单的守护程序，允许你透过网络发送SysRQ快捷键。某些情况下，远程机器网络可以连通，但是SSH无法登录，可以利用sysrqd强制重启。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">apt install -y sysrqd
echo "password" &gt; /etc/sysrqd.secret
service sysrqd restart</pre>
<div class="blog_h2"><span class="graybg">使用</span></div>
<p>sysrqd默认监听4094端口，在客户机上，使用telnet登录：</p>
<pre class="crayon-plain-tag">telnet remote-ip 4094</pre>
<p>根据提示输入密码。然后输入SysRQ快捷键即可。<span style="background-color: #c0c0c0;">输入q可以退出telnet</span>。</p>
<div class="blog_h2"><span class="graybg">常用SysQR快捷键</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">按键</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>b</td>
<td>立即重启，不卸载文件系统、不sync页面缓存</td>
</tr>
<tr>
<td>c</td>
<td>触发一次系统崩溃</td>
</tr>
<tr>
<td>e</td>
<td>向除了init进程（PID 1）之外的所有进程发送SIGTERM。包括sysrqd</td>
</tr>
<tr>
<td>i</td>
<td>向除了init进程（PID 1）之外的所有进程发送SIGKILL。包括sysrqd</td>
</tr>
<tr>
<td>m</td>
<td>打印当前内存信息</td>
</tr>
<tr>
<td>o</td>
<td>关机</td>
</tr>
<tr>
<td>s</td>
<td>sync所有已经挂载的文件系统</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/remote-ctrl-with-sysrqd">使用sysrqd进行远程控制</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/remote-ctrl-with-sysrqd/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Gitea搭建Git服务器</title>
		<link>https://blog.gmem.cc/gitea-server-install-log</link>
		<comments>https://blog.gmem.cc/gitea-server-install-log#comments</comments>
		<pubDate>Sat, 28 Apr 2018 11:26:40 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=20829</guid>
		<description><![CDATA[<p>安装 Ubuntu [crayon-69e2a1548c218874969247/] 配置 [crayon-69e2a1548c21c376088765/] &#160;</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/gitea-server-install-log">使用Gitea搭建Git服务器</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<pre class="crayon-plain-tag">pushd  /opt/gitea
mkdir -p custom/conf/
mkdir repos

wget -O gitea https://dl.gitea.io/gitea/1.3.2/gitea-1.3.2-linux-amd64
chmod +x gitea

nohup /opt/gitea/gitea web &gt; /dev/null 2&gt;&amp;1 &amp;</pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<pre class="crayon-plain-tag">RUN_USER = root
RUN_MODE = prod
APP_NAME = Gmem Gitea

[repository]
ROOT = /opt/gitea/repos

[server]
PROTOCOL         = https
DOMAIN           = git.gmem.cc
HTTP_ADDR        = 0.0.0.0
HTTP_PORT        = 3443
; 证书需要完整的链
CERT_FILE        = /etc/letsencrypt/live/git.gmem.cc/fullchain.pem
KEY_FILE         = /etc/letsencrypt/live/git.gmem.cc/privkey.pem
SSH_DOMAIN       = git.gmem.cc
ROOT_URL         = https://git.gmem.cc/
DISABLE_SSH      = false
SSH_PORT         = 22
LFS_START_SERVER = true
LFS_CONTENT_PATH = /opt/gitea/data/lfs
LFS_JWT_SECRET   = s52JAPPWiOFpp8CZiPHrlGeHWtwv6WCphZtBG5vNLE8
OFFLINE_MODE     = false


[mailer]
ENABLED = false

[service]
REGISTER_EMAIL_CONFIRM            = false
ENABLE_NOTIFY_MAIL                = false
DISABLE_REGISTRATION              = true
ENABLE_CAPTCHA                    = false
REQUIRE_SIGNIN_VIEW               = false
DEFAULT_KEEP_EMAIL_PRIVATE        = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING       = true
NO_REPLY_ADDRESS                  = noreply@gmem.cc
[picture]
DISABLE_GRAVATAR        = false
ENABLE_FEDERATED_AVATAR = false

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = false

[session]
PROVIDER = file

[log]
MODE      = file
LEVEL     = Info
ROOT_PATH = /opt/gitea/log</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/gitea-server-install-log">使用Gitea搭建Git服务器</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/gitea-server-install-log/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ansible学习笔记</title>
		<link>https://blog.gmem.cc/ansible-study-note</link>
		<comments>https://blog.gmem.cc/ansible-study-note#comments</comments>
		<pubDate>Wed, 18 Apr 2018 03:46:48 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=20275</guid>
		<description><![CDATA[<p>简介 Ansible是一个自动化IT工具，能够配置系统、部署软件、编排复杂的IT任务（例如CD、零停机滚动更新）。 Ansible默认通过 SSH 协议管理，在管理机需要Python2.7环境，在托管机上需要Python 2环境。 安装 Ubuntu [crayon-69e2a1548cb64355036733/] Pip [crayon-69e2a1548cb68342461363/] 配置 Ansible支持从多个位置读取配置选项，包括环境变量、命令行参数、名为ansible.cfg的ini文件。 ini文件的搜索顺序为： 环境变量ANSIBLE_CONFIG指定的位置 当前目录的ansible.cfg ~/.ansible.cfg /etc/ansible/ansible.cfg 配置文件中以#或者;开头的行为注释。 配置项说明 [crayon-69e2a1548cb6a559673446/] 全部可用配置项请参考：https://ansible-tran.readthedocs.io/en/latest/docs/intro_configuration.html <a class="read-more" href="https://blog.gmem.cc/ansible-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ansible-study-note">Ansible学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Ansible是一个自动化IT工具，能够配置系统、部署软件、编排复杂的IT任务（例如CD、零停机滚动更新）。</p>
<p><span style="color: #404040;">Ansible默认通过 SSH 协议管理，在管理机需要Python2.7环境，在托管机上需要Python 2环境。</span></p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<pre class="crayon-plain-tag"># 下面的软件包在老版本Ubuntu上叫做python-software-properties
sudo apt-get install software-properties-common
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install ansible

# 被管理的主机上需要安装Python2
sudo apt-get install python-minimal</pre>
<div class="blog_h2"><span class="graybg">Pip</span></div>
<pre class="crayon-plain-tag">/home/alex/Python/3.5.1/bin/pip install ansible </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>Ansible支持从多个位置读取配置选项，包括环境变量、命令行参数、名为ansible.cfg的ini文件。</p>
<p>ini文件的搜索顺序为：</p>
<ol>
<li>环境变量ANSIBLE_CONFIG指定的位置</li>
<li>当前目录的ansible.cfg</li>
<li>~/.ansible.cfg</li>
<li>/etc/ansible/ansible.cfg</li>
</ol>
<p>配置文件中以#或者;开头的行为注释。</p>
<div class="blog_h2"><span class="graybg">配置项说明</span></div>
<pre class="crayon-plain-tag">[defaults]
; 主机清单文件位置
inventory      = /etc/ansible/hosts
; 远程SSH端口
remote_port    = 22
; SSH超时
timeout = 10
; 远程用户
remote_user = root
; SSH身份验证使用的私钥位置
private_key_file =/home/alex/Documents/puTTY/gmem.key
; 禁止由于不在known_hosts中导致的警告
; 等价于 export ANSIBLE_HOST_KEY_CHECKING=False
host_key_checking = False
; 和远程主机通信时，并行进程的数量
forks = 10
; 角色根目录
roles_path = /app/tops/roles
; 主机清单
inventory = /app/tops/host_vars/all.ini
; 控制Fact（远程系统变量）收集行为：
;   implicit  每次执行剧本，都收集
;   smart  避免不必要的收集，节约时间
gathering = smart
; Fact存储方式
fact_caching = jsonfile
; 连接到Fact存储的方式，对于jsonfile，需要指定一个目录
fact_caching_connection = ./.cache
; Fact缓存过期时间
fact_caching_timeout = 3600
; 剧本的标准输出回调，决定输出的显示方式
;   json
;   debug
;   skippy
stdout_callback = debug
; 额外启用的回调，从2.0开始Ansible的所有回调插件可用，但是默认是禁用状态。该设置让你可以指定哪些回调插件启用
;   timer 这个回调插件可以计算整个 playbook 的运行时间
;   mail 这个回调插件可以实现发送邮件的功能
;   profile_roles 这个插件是在执行中添加耗时
;
callback_whitelist = profile_tasks</pre>
<p>全部可用配置项请参考：<a href="https://ansible-tran.readthedocs.io/en/latest/docs/intro_configuration.html">https://ansible-tran.readthedocs.io/en/latest/docs/intro_configuration.html</a></p>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg"><a id="inventory"></a>主机清单</span></div>
<p>所谓主机清单（Host Inventory）是一个配置文件，来指定需要被管理的主机列表。主机清单的默认位置在/etc/ansible/hosts。</p>
<div class="blog_h3"><span class="graybg">组</span></div>
<p>所有主机都属于<pre class="crayon-plain-tag">all</pre>组，此外可以自定义组：</p>
<pre class="crayon-plain-tag">; 方括号内的是组名，每个主机都可以属于多个组
[k8s]
xenial-100.gmem.cc
xenial-101.gmem.cc
xenial-102.gmem.cc
xenial-103.gmem.cc
xenial-104.gmem.cc
xenial-105.gmem.cc</pre>
<div class="blog_h3"><span class="graybg">组的组</span></div>
<p>一个组，可以作为另外一个组的成员：</p>
<pre class="crayon-plain-tag">; 组1
[atlanta]
host1
host2

; 组2
[raleigh]
host2
host3

; 组的组    该关键字表示定义组的成员
[southeast:children]
atlanta
raleigh

; 父组的变量
[southeast:vars]
some_server=foo.southeast.example.com
halon_system_timeout=30
self_destruct_countdown=60
escape_pods=2 </pre>
<div class="blog_h3"><span class="graybg">通配主机</span></div>
<pre class="crayon-plain-tag">[vms]
xenial-[100-200].gmem.cc
;    1:50等价
www[01:50].example.com 
db-[a:f].example.com</pre>
<div class="blog_h3"><span class="graybg">主机变量</span></div>
<pre class="crayon-plain-tag">; 可以为主机设置别名，并指定主机变量
; 别名  ansible_开头的预定义变量
;                       SSH端口                SSH IP地址   通过SSH连接
jumper ansible_ssh_port=5555 ansible_ssh_host=192.168.1.50 ansible_connection=ssh</pre>
<p>变量可以在Playbook（剧本）中引用。</p>
<div class="blog_h3"><span class="graybg">组变量</span></div>
<p>除了主机变量，还可以定义属于整个组的变量：</p>
<pre class="crayon-plain-tag">[atlanta]
host1
host2

; 组名    这个关键字表示该段定义的是变量
[atlanta:vars]
ntp_server=ntp.atlanta.example.com
proxy=proxy.atlanta.example.com</pre>
<div class="blog_h3"><span class="graybg">独立文件中的变量</span></div>
<p>在清单中定义所有变量不是最佳实践，你可以将它们放在独立的YAML中，这些YAML会和清单文件关联。</p>
<p>假设清单文件<pre class="crayon-plain-tag">/etc/ansible/hosts</pre>中有一个主机xenial-100，它属于两个组master、worker，则以下三个文件中定义的变量可以被该主机使用：</p>
<p style="padding-left: 30px;">/etc/ansible/group_vars/master     组变量<br />/etc/ansible/group_vars/worker     组变量<br />/etc/ansible/host_vars/xenial-100  主机变量</p>
<p>上述三个文件，还可以创建为目录，并在里面定义任意数量的文件，在这些文件里面定义变量。只需要改目录名和组/主机名一致即可：</p>
<p style="padding-left: 30px;">/etc/ansible/group_vars/master/sysctl<br />/etc/ansible/group_vars/master/connection</p>
<p>Ansible 1.2+，group_vars、host_vars可以放在：</p>
<ol>
<li>inventory文件所在目录</li>
<li>playbook文件所在目录</li>
</ol>
<p>如果两个目录下都有，则<span style="background-color: #c0c0c0;">playbook覆盖inventory</span>。</p>
<p>建议将inventory和变量一起纳入版本控制管理。</p>
<div class="blog_h3"><span class="graybg"><a id="internal-magic-vars"></a>内置魔法变量</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ansible_ssh_host</td>
<td>
<p>将要连接的远程主机名.与你想要设定的主机的别名不同的话,可通过此变量设置</p>
</td>
</tr>
<tr>
<td>ansible_ssh_port</td>
<td>ssh端口号</td>
</tr>
<tr>
<td>ansible_ssh_user</td>
<td>ssh 用户名</td>
</tr>
<tr>
<td>ansible_ssh_pass</td>
<td>ssh 密码，不安全，建议使用 --ask-pass 或 SSH 密钥</td>
</tr>
<tr>
<td>ansible_sudo_pass</td>
<td>sudo 密码</td>
</tr>
<tr>
<td>ansible_sudo_exe</td>
<td>sudo 命令路径</td>
</tr>
<tr>
<td>ansible_connection</td>
<td>
<p>与主机的连接类型.比如:local, ssh 或者 paramiko</p>
<p>Ansible 1.2 以前默认使用 paramiko.1.2 以后默认使用 'smart','smart' 方式会根据是否支持 ControlPersist, 来判断'ssh' 方式是否可行</p>
</td>
</tr>
<tr>
<td>ansible_ssh_private_key_file</td>
<td>ssh 使用的私钥文件.适用于有多个密钥,而你不想使用 SSH 代理的情况</td>
</tr>
<tr>
<td>ansible_shell_type</td>
<td>目标系统的shell类型.默认情况下,命令的执行使用 'sh' 语法,可设置为 'csh' 或 'fish'</td>
</tr>
<tr>
<td>ansible_python_interpreter</td>
<td>
<p>目标主机的 python 路径.适用于的情况: 系统中有多个 Python, 或者命令路径不是"/usr/bin/python",比如 \*BSD, 或者 /usr/bin/python<br />不是 2.X 版本的 Python.我们不使用 "/usr/bin/env" 机制,因为这要求远程用户的路径设置正确,且要求 "python" 可执行程序名不可为 python以外的名字(实际有可能名为python26)</p>
</td>
</tr>
<tr>
<td>inventory_hostname</td>
<td>当前（正在Play中迭代的）主机在主机清单中的名字</td>
</tr>
<tr>
<td>group_names</td>
<td>当前主机所属的组名列表</td>
</tr>
<tr>
<td>groups</td>
<td>当前主机清单中所有组的名字，到组包含的主机列表的字典</td>
</tr>
<tr>
<td>hostvars</td>
<td>当前主机清单中所有主机的名字，到主机所有变量的字典</td>
</tr>
<tr>
<td>playbook_dir</td>
<td>传递给ansible-playbook命令行的剧本文件的路径</td>
</tr>
<tr>
<td>role_name</td>
<td>当前正在执行的角色名称</td>
</tr>
<tr>
<td>role_path</td>
<td>当前正在执行的角色所在目录的路径</td>
</tr>
<tr>
<td>ansible_facts</td>
<td>inventory_hostname的所有事实数据，通常又setup模块在play中自动收集，不过任何模块都可以返回事实</td>
</tr>
<tr>
<td>ansible_local</td>
<td>inventory_hostname的所有收集/缓存的本地事实（local fact）数据</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">即席任务</span></div>
<p>使用ansible命令可以群发即席的命令给一群主机：<pre class="crayon-plain-tag">ansible &lt;host-pattern&gt; [options]</pre>，命令示例：</p>
<pre class="crayon-plain-tag">#       匹配目标主机的模式     执行的模块        传递给模块的参数   进一步限制主机范围
ansible &lt;pattern_goes_here&gt; -m &lt;module_name&gt; -a &lt;arguments&gt;  -l &lt;group-or-@file&gt;
# 示例
ansible webservers -m service -a "name=httpd state=restarted"  -l @hosts.txt


#       组   模块    登陆到机器的用户  改变身份                   询问密码
ansible all -m ping -u root        --sudo --sudo-user batman  --ask-sudo-pass

# 在所有主机上执行Shell命令
ansible all -u root -a "/bin/echo hello"

# 拷贝当前主机上的文件/etc/hosts到所有k8s组中的主机
ansible k8s -m copy -a "src=/etc/hosts dest=/tmp/hosts"

# 全部重启                            并发10个进程来执行
ansible -u root lb -a "/sbin/reboot" -f 10

# 额外的角色搜索路径
roles_path = /opt/mysite/roles:/opt/othersite/roles</pre>
<div class="blog_h3" style="color: #000000;"><span class="graybg">主机模式</span></div>
<table class="full-width fixed-word-wrap" style="color: #000000;">
<thead>
<tr>
<td style="text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>all *</td>
<td>匹配所有主机</td>
</tr>
<tr>
<td>192.168.1.*<br />*.example.com</td>
<td>局部通配符</td>
</tr>
<tr>
<td>webservers[0-25]</td>
<td>范围匹配</td>
</tr>
<tr>
<td>~(web|db).*\.example\.com</td>
<td>正则式匹配</td>
</tr>
<tr>
<td>webservers:dbservers</td>
<td>或</td>
</tr>
<tr>
<td>webservers:!phoenix</td>
<td>非</td>
</tr>
<tr>
<td>webservers:dbservers:&amp;staging:!phoenix</td>
<td>更加复杂的逻辑操作</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用剧本</span></div>
<p>为了避免重复输入命令，Ansibel提供了Playbook脚本功能，脚本文件的格式为YAML。调用脚本的语法如下：</p>
<pre class="crayon-plain-tag">#                剧本文件     并发度
ansible-playbook deploy.yml  -f 10</pre>
<p>下面是一个脚本示例：</p>
<pre class="crayon-plain-tag">---
# 主机名、组名、或者all
- hosts: web
  # 定义变量
  vars:
    http_port: 80
    max_clients: 200
  # 远程登陆用户
  remote_user: root
  # 需要在远程主机上执行的任务列表，安装定义的顺序依次执行
  tasks:
  - name: ensure apache is at the latest version
    # 安装软件
    yum: pkg=httpd state=latest
    # 参数也可以作为子元素
    yum:
      pkg: httpd
      state: lastest
  
  - name: Write the configuration file
    # 写入配置文件
    template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
    # 发布事件
    notify:
    - restart apache

  - name: ensure apache is running
    service: name=httpd state=started
  # 事件监听器
  handlers:
    - name: restart apache
      service: name=httpd state=restarted

# 可以针对另外一组主机声明脚本
- hosts: k8s</pre>
<div class="blog_h2"><span class="graybg">使用模块</span></div>
<p>模块就是Ansible的命令。调用模块是可以指定不同的参数，就像调用Bash命令那样。</p>
<div class="blog_h3"><span class="graybg">命令中 </span></div>
<p>使用<pre class="crayon-plain-tag">-m</pre>参数指定module的名字，<pre class="crayon-plain-tag">-a</pre>参数指定module的参数，示例： </p>
<pre class="crayon-plain-tag">#                   参数格式"name1=value1 name2=value2"
ansible all -m copy -a "src=/etc/hosts dest=/tmp/hosts"
ansible web -m yum -a "name=httpd state=present"</pre>
<div class="blog_h3"><span class="graybg">脚本中</span></div>
<p>在Playbook脚本中每个任务（Task的子元素）都对应一个模块调用。</p>
<div class="blog_h2"><span class="graybg">常用模块</span></div>
<p>所有模块的文档参考：<a href="http://docs.ansible.com/ansible/modules_by_category.html">http://docs.ansible.com/ansible/modules_by_category.html</a></p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">模块名称</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">ping</td>
<td>
<p>测试一个节点有没有配置好，执行检查：</p>
<ol>
<li>能不能SSH登陆</li>
<li>python版本是否满足需求</li>
</ol>
<p>没有参数</p>
</td>
</tr>
<tr>
<td class="blog_h3">debug</td>
<td>
<p>打印调试信息，类似于echo。支持变量替换</p>
<p>示例：<pre class="crayon-plain-tag">ansible -m debug -a "var=hostvars[inventory_hostname]"</pre></p>
<p>playbook示例：</p>
<pre class="crayon-plain-tag">---
- hosts: k8s
  tasks:
    - name: test
      debug:
        msg: "{{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
        # 也可以打印变量
        var: hostvars[inventory_hostname]["ansible_default_ipv4"]["gateway"]</pre>
</td>
</tr>
<tr>
<td class="blog_h3">copy</td>
<td>
<p>从当前的机器上复制文件到远程节点上，并且设置合理的文件权限。拷贝文件的时候，会先比较下文件的Checksum，如果相同则不会拷贝。
<p>注意，目标目录必须存在，如果不存在可以预先使用file模块创建</p>
<p>示例：</p>
<pre class="crayon-plain-tag">- copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: 0644
    # 备份被替换的文件
    backup: yes</pre>
</td>
</tr>
<tr>
<td class="blog_h3">synchronize</td>
<td>
<p>同步当前机器上的目录到远程节点：
<p><pre class="crayon-plain-tag">- name: sync .helm directory
  synchronize:
    src:  /home/alex/.helm
    dest: /root/.helm</pre>
</td>
</tr>
<tr>
<td class="blog_h3">template</td>
<td>
<p>从当前的机器上复制文件到远程节点上，并进行变量替换。变量使用<pre class="crayon-plain-tag">{{ }}</pre>包围。示例：</p>
<pre class="crayon-plain-tag">- template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config.j2
    owner: root
    group: root
    mode: '0600'
    # 校验拷贝的文件是否有效
    validate: /usr/sbin/sshd -t %s
    backup: yes</pre>
</td>
</tr>
<tr>
<td class="blog_h3">file</td>
<td>
<p>设置远程值机上的文件、软链接和文件夹的权限，或者创建、删除文件。示例：
<pre class="crayon-plain-tag"># 修改文件模式
- file:
    path: /etc/foo.conf
    owner: foo
    group: foo
    mode: 0644
    mode: "u=rw,g=r,o=r"
    mode: "u+rw,g-wx,o-rwx"

# 创建软链接
- file:
    src: /file/to/link/to
    dest: /path/to/symlink
    owner: foo
    group: foo
    state: link

# 创建新文件
- file:
    path: /etc/foo.conf
    state: touch
    mode: "u=rw,g=r,o=r"

# 创建目录
- file:
    path: /etc/some_directory
    state: directory
    mode: 0755</pre>
</td>
</tr>
<tr>
<td class="blog_h3">user</td>
<td>
<p>增、删、改Linux远程节点的用户账户，并为其设置账户的属性。示例：
<pre class="crayon-plain-tag"># 创建账户
- user:
    name: johnd
    comment: "John Doe"
    uid: 1040
    group: admin
# 删除账户
- user:
    name: johnd
    state: absent
    remove: yes

# 修改属性
- user:
    name: jsmith
    #创建SSH 私钥
    generate_ssh_key: yes
    ssh_key_bits: 2048
    ssh_key_file: .ssh/id_rsa

- user:
    name: james18
    shell: /bin/zsh
    groups: developers
    # 设置账户过期时间
    expires: 1422403387</pre>
</td>
</tr>
<tr>
<td class="blog_h3">yum</td>
<td>
<p>用来管理Redhat系（RHEL，CentOS，Fedora 21-）的Linux上的安装包。示例：
<pre class="crayon-plain-tag">- name: 安装最新版本的包，已经安装则替换老版本
    yum:
      name: httpd
      state: latest

- name: 安传个指定版本
    yum:
      name: httpd-2.2.29-1.4.amzn1
      state: present

- name: 删除软件包
    yum:
      name: httpd
      state: absent
- name: 从本地目录安装
  yum:
    name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm
    state: present </pre>
</td>
</tr>
<tr>
<td class="blog_h3">apt</td>
<td>
<p>用来管理Debain系Linux上的软件包。示例：
<pre class="crayon-plain-tag">- name: 更新仓库缓存并安装软件包foo
  apt:
    name: foo
    update_cache: yes

- name: 安装软件包但是不启动
  apt: name=apache2 state=present
  environment:
    RUNLEVLEL: 1
    http_proxy: http://10.0.0.1:8088
- name: 移除软件包
  apt: name=foo state=absent

- name: 安装指定版本的软件包
  apt:
    name: foo=1.00
    state: present

- name: 更新所有软件包到最新版本
  apt:
    upgrade: dist

- name: 仅仅执行apt-get update
  apt:
    update_cache: yes

- name: 安装Deb包
  apt:
    deb: /tmp/mypackage.deb

- name: 安装foo包的build依赖
  apt:
    pkg: foo
    state: build-dep

- name: 移除无用依赖包
  apt:
    autoremove: yes

- name: 安装多个软件包
  apt: name={{item}} state=installed
  with_items:
    - kubelet
    - kubectl</pre>
</td>
</tr>
<tr>
<td class="blog_h3">package</td>
<td>
<p>通用的包管理器，使用底层的操作系统包管理器来安装、删除、升级软件包
<pre class="crayon-plain-tag"># 确保软件包安装
- name: Install ntpdate
  package:
    name: ntpdate
    state: present

# 确保软件包移除
- name: Remove the apache package
  package:
    name: "{{ apache }}"
    state: absent

# 确保软件包是最新版本
- name: Install the latest version of Apache and MariaDB
  package:
    name:
      - httpd
      - mariadb-server
    state: latest </pre>
</td>
</tr>
<tr>
<td class="blog_h3">apt_key</td>
<td>
<p>添加或者删除APT key。示例：
<pre class="crayon-plain-tag">- name: 从Key服务器添加一个Key
  apt_key:
    keyserver: keyserver.ubuntu.com
    id: 36A1D7869245C8950F966E92D8576A8BA88D21E9

- name: 从URL添加Key
  apt_key:
    url: https://ftp-master.debian.org/keys/archive-key-6.0.asc
    state: present

- name: Add an Apt signing key, will not download if present</pre>
</td>
</tr>
<tr>
<td class="blog_h3">apt_repository</td>
<td>
<p>管理APT仓库。示例：
<pre class="crayon-plain-tag"># 添加指定的仓库到系统源列表
- apt_repository:
    repo: deb http://archive.canonical.com/ubuntu hardy partner
    state: present

# 添加指定的仓库到系统源列表，存放在指定的文件中
- apt_repository:
    repo: deb http://dl.google.com/linux/chrome/deb/ stable main
    state: present
    filename: google-chrome

# 移除指定的仓库
- apt_repository:
    repo: deb http://archive.canonical.com/ubuntu hardy partner
    state: absent

# 从PPA安装仓库
- apt_repository:
    repo: ppa:nginx/stable</pre>
</td>
</tr>
<tr>
<td class="blog_h3">service</td>
<td>
<p>管理远程节点上的服务。示例：
<pre class="crayon-plain-tag"># 开服务
- service:
    name: httpd
    state: started
# 关服务
- service:
    name: httpd
    state: stopped
# 重起服务
- service:
    name: httpd
    state: restarted
# 重载服务
- service:
    name: httpd
    state: reloaded
# 设置开机启动
- service:
    name: httpd
    enabled: yes</pre>
</td>
</tr>
<tr>
<td class="blog_h3">shell</td>
<td>
<p>通过/bin/sh在远程节点上执行命令，支持$HOME和”&lt;”, “&gt;”, “|”, “;” and “&amp;”。示例：
<pre class="crayon-plain-tag">- name: test $home
  shell: echo "Test" &gt; ~/tmp/test


# 多行命令
- hosts: k8s
  tasks:
    - name: Pull images
      shell: |
        docker pull $IMAGE_REPO/kube-apiserver-amd64:v1.10.2
        docker pull $IMAGE_REPO/kube-scheduler-amd64:v1.10.2
      args:
        # 指定工作目录
        chdir: somedir/
        # 指定使用的脚本解析器
        executable: /bin/bash</pre>
</td>
</tr>
<tr>
<td class="blog_h3">command</td>
<td>类似shell， 但是不支持$HOME和”&lt;”, “&gt;”, “|”, “;” and “&amp;” </td>
</tr>
<tr>
<td>git</td>
<td>
<p>下载Git仓库的内容到指定位置：
<pre class="crayon-plain-tag">ansible webservers -m git \
  -a "repo=git://foo.example.org/repo.git dest=/srv/myapp version=HEAD"</pre>
</td>
</tr>
<tr>
<td>set_fact</td>
<td>
<p>使用该模块，可以设置per-host的变量（事实）。这些变量可以被当前ansible-playbook调用的、后续的play使用
<p>如果设置cacheable为yes，则记录在缓存中，允许后续playbook调用使用</p>
<pre class="crayon-plain-tag">- name: "设置多个变量，用空格分隔"
  set_fact: one_fact="something" other_fact="{{ local_var }}"

- name: "另外一种多变量设置风格"
  set_fact:
    one_fact: something
    other_fact: "{{ local_var * 2 }}"
    another_fact: "{{ some_registered_var.results | map(attribute='ansible_facts.some_fact') | list }}"

- name: "允许缓存变量"
  set_fact:
    one_fact: something
    other_fact: "{{ local_var * 2 }}"
    cacheable: yes </pre>
</td>
</tr>
<tr>
<td class="blog_h3">systemd</td>
<td>
<p>控制Systemd服务
<pre class="crayon-plain-tag"># 确保服务启动
- name: Make sure a service is running
  systemd:
    state: started
    name: httpd

# 确保服务停止
- name: Stop service cron on debian, if running
  systemd:
    name: cron
    state: stopped

# 发起daemon-reload并重启服务
- name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes
  systemd:
    state: restarted
    daemon_reload: yes
    name: crond

# 重新载入服务
- name: Reload service httpd, in all cases
  systemd:
    name: httpd
    state: reloaded

# 启用服务
- name: Enable service httpd and ensure it is not masked
  systemd:
    name: httpd
    enabled: yes
    # 被mask的Systemd单元无法启动
    masked: no

# 重新载入配置
- name: Just force systemd to reread configs (2.4 and above)
  systemd:
    daemon_reload: yes</pre>
</td>
</tr>
<tr>
<td>modprobe</td>
<td>
<p>加载或卸载内核模块：
<pre class="crayon-plain-tag">- name: ensure kernel modules
  modprobe:
    name: "{{ item }}"
    # absent 表示卸载
    state: present
  with_items:
    - "br_netfilter"</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">剧本详解</span></div>
<p>剧本比即席任务复杂而强大的多，它可以：
<ol>
<li>编排有序的执行过程，在多组机器间，来回有序的执行特别指定的步骤</li>
<li>可以同步或异步的发起任务</li>
</ol>
<p>Playbook的格式是YAML，它由一个或多个plays组成，它的内容是一个以plays为元素的列表。</p>
<p>在每个Play中，<span style="background-color: #c0c0c0;">一组机器被映射为定义好的角色</span>。</p>
<p>Play中包含Tasks，每个Task典型情况下是对某个Ansible模块的调用。</p>
<p>下面是一个简单的例子：</p>
<pre class="crayon-plain-tag"># 只有一个Play，它包含目标、变量、任务列表信息
---
  # 针对的主机，可以是主机、组的Pattern。所有任务都针对相同的主机
- hosts: webservers
  # 可以定义变量
  vars:
    http_port: 80
    max_clients: 200
  # 变量可以放在外部文件中，例如敏感信息
  vars_files:
  - /vars/external_vars.yml
  # 以什么用户登陆执行
  remote_user: root
  # 支持sudo
  sudo: yes
  sudo_user: postgres
  # 任务列表，注意任务是串行执行的，只有前一个执行完毕（不管有多少主机），后一个才开始执行
  tasks:
    # 每个任务都具有名称
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
    # 每个Task都可以覆盖用户
    remote_user: yourname
  - name: write the apache config file
    # 老版本中，需要写作 action: template，限制直接写模块名即可
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    # 如果发生了变动（对于这个模块来说，就是目标文件内容发生了变化）
    # 则执行
    notify:
    # 需要注意，如果有多个Task都通知需要执行此Handler，只会执行一次
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  - name: run this command and ignore the result
    # 对于shell或command，如果不关心退出码，可以
    shell: /usr/bin/somecommand || /bin/true
    # 或者
    ignore_errors: True
  - name: Copy ansible inventory file to client
    # 参数太长，可以换行编写
    copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts
            owner=root group=root mode=0644
    #                                    引用变量
  - name: create a virtual host file for {{ vhost }}
    template: src=somefile.j2 dest=/etc/httpd/conf.d/{{ vhost }}

  # 供引用的、发生变动后执行的处理器。通常都用来重启服务
  handlers:
    - name: restart apache
      service: name=httpd state=restarted</pre>
<p>需要注意：</p>
<ol>
<li>如果一个 host 执行 task 失败，这个 host 将会从整个 playbook 的 rotation 中移除</li>
<li>每个 task 的目标在于执行一个 moudle, 通常是带有特定的参数来执行.在<span style="background-color: #c0c0c0;">参数中可以使用{{变量}}</span></li>
<li>模块具有幂等性，<span style="background-color: #c0c0c0;">多次执行，它只会做必要的动作，此外当远端系统被人改动时,可以重放 playbooks 达到恢复的目的</span>。对于command、shell这样的模块，多次执行相当于反复调用命令，是否幂等取决于底层命令</li>
</ol>
<div class="blog_h2"><span class="graybg">Tasks</span></div>
<p>注意：很多字段Role也可以用。</p>
<p>剧本、角色中都可以定义Task。Task具有以下通用字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>action</td>
<td>该任务执行的动作，通常翻译为C（模块）或Action插件</td>
</tr>
<tr>
<td>any_errors_fatal</td>
<td>任何主机上的单个未处理的错误被传递到所有主机级别，并导致剧本失败</td>
</tr>
<tr>
<td>args</td>
<td>辅助的为Task增加参数的方式，字典</td>
</tr>
<tr>
<td>async</td>
<td>如果C（动作）支持，异步的执行Task，该字段的值是异步最大运行时间</td>
</tr>
<tr>
<td>poll</td>
<td>异步任务的轮询间隔</td>
</tr>
<tr>
<td>become</td>
<td>执行此任务时是否需要改变运行用户</td>
</tr>
<tr>
<td>become_user</td>
<td>改变为什么用户</td>
</tr>
<tr>
<td>changed_when</td>
<td>用于修改Ansible默认changed状态的判断逻辑的表达式</td>
</tr>
<tr>
<td>check_mode</td>
<td>控制任务是否运行在check模式</td>
</tr>
<tr>
<td>connection</td>
<td>修改在目标主机上执行时使用的connection插件</td>
</tr>
<tr>
<td>debugger</td>
<td>基于任务结果状态进行调试</td>
</tr>
<tr>
<td>delay</td>
<td>重试Task的间隔，必须和until联用</td>
</tr>
<tr>
<td>retries</td>
<td>重试Task的次数，必须和until联用</td>
</tr>
<tr>
<td>delegate_facts</td>
<td>是否将Fact应用到被代理的主机</td>
</tr>
<tr>
<td>delegate_to</td>
<td>在指定的主机上，而非目标主机上执行任务</td>
</tr>
<tr>
<td>local_action</td>
<td>等价于：<pre class="crayon-plain-tag">delegate_to: localhost</pre></td>
</tr>
<tr>
<td>run_once</td>
<td>用于控制禁止遍历主机列表逐个执行任务，如果为True则Task仅在第一个主机上执行</td>
</tr>
<tr>
<td>diff</td>
<td>是否返回diff信息</td>
</tr>
<tr>
<td>environment</td>
<td>提供给Task的环境变量字典</td>
</tr>
<tr>
<td>failed_when</td>
<td>覆盖Ansible默认failed状态判断逻辑</td>
</tr>
<tr>
<td>ignore_errors</td>
<td>任务失败时是否忽略并继续Play，和connection errors无关</td>
</tr>
<tr>
<td>ignore_unreachable</td>
<td>忽略无法连接的主机并且继续Play</td>
</tr>
<tr>
<td>loop</td>
<td>迭代一组任务，每个条目默认存入名为item的变量</td>
</tr>
<tr>
<td>loop_control</td>
<td>用于控制循环行为，例如修改item变量的名字</td>
</tr>
<tr>
<td>name</td>
<td>任务标识符</td>
</tr>
<tr>
<td>no_log</td>
<td>是否隐藏信息</td>
</tr>
<tr>
<td>notify</td>
<td>当changed=true时执行的处理器</td>
</tr>
<tr>
<td>port</td>
<td>修改连接的默认端口</td>
</tr>
<tr>
<td>register</td>
<td>用于存放任务状态、模块返回数据的变量</td>
</tr>
<tr>
<td>remote_user</td>
<td>connection plugin使用的用户</td>
</tr>
<tr>
<td>tags</td>
<td>为Task打标签</td>
</tr>
<tr>
<td>throttle</td>
<td>控制任务并发数量，也就是任务在同一时刻，最多在几个主机上并行执行</td>
</tr>
<tr>
<td>vars</td>
<td>变量列表</td>
</tr>
<tr>
<td>when</td>
<td>条件满足时才执行任务</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Handlers</span></div>
<p>对于任何一个主机，一个特定的Handler只会执行一次，不管它被Notify了多少次</p>
<p>Handlers 会在 'pre_tasks', 'roles', 'tasks', 和 'post_tasks' 之间自动执行，你也可以手工立即调用Handler：</p>
<pre class="crayon-plain-tag">tasks:
   - shell: some tasks go here
   # 立即调用
   - meta: flush_handlers
   - shell: some other tasks</pre>
<div class="blog_h2"><span class="graybg">Include</span></div>
<p>编写一个巨大的Playbook文件，不利于维护。 可以将具有重用价值的部分独立编写，然后通过include引用。</p>
<p>include主要用于包含来自其它文件的Task，也可以包含来自其它文件的Play。Handlers本质上也是Task，亦可被引用。</p>
<div class="blog_h3"><span class="graybg">包含Task</span></div>
<p>用于被引用的Task列表，形式如下：</p>
<pre class="crayon-plain-tag">---
- name: placeholder foo
  command: /bin/foo

- name: placeholder bar
  command: /bin/bar</pre>
<p>在Playbook中，include上述列表的语法：</p>
<pre class="crayon-plain-tag">tasks:
  - include: tasks/foo.yml</pre>
<p>include还支持多种写法： </p>
<pre class="crayon-plain-tag"># 1.0引入的语法
tasks:
  - include: wordpress.yml
    vars:
        wp_user: timmy
        some_list_variable:
          - alpha
          - beta
          - gamma

# 1.4之后，可以使用下面的语法

tasks:
  - { include: wordpress.yml, wp_user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }</pre>
<div class="blog_h3"><span class="graybg">传递变量</span></div>
<p>include的时候，可以传递变量，所谓参数化include：</p>
<pre class="crayon-plain-tag"># 
tasks: 
                           # 变量列表，在被包含文件中引用变量  {{ wp_user }}  
  - include: wordpress.yml wp_user=timmy
  - include: wordpress.yml wp_user=alice
  - include: wordpress.yml wp_user=bob</pre>
<p>除了显式传递的变量之外，在Play的vars段中声明的变量，均可在被包含的列表中引用。</p>
<div class="blog_h3"><span class="graybg">包含Handler</span></div>
<p>include也可以出现在handlers段： </p>
<pre class="crayon-plain-tag">handlers:
  - include: handlers/handlers.yml</pre>
<div class="blog_h3"><span class="graybg">包含Play</span></div>
<p>甚至，include还可以将Playbook包含到另外一个Playbook中，这种情况下，<span style="background-color: #c0c0c0;">不支持传递变量</span>：</p>
<pre class="crayon-plain-tag"># Play
- name: this is a play at the top level of a file
  hosts: all
  remote_user: root

  tasks:

  - name: say hi
    tags: foo
    shell: echo "hi..."

# 来自其它文件的Plays
- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml</pre>
<div class="blog_h2"><span class="graybg">Role </span></div>
<p>使用Role，使组织Playbook的最佳方式。<span style="color: #404040;">Roles <span style="background-color: #c0c0c0;">基于特定的文件结构</span>，去<span style="background-color: #c0c0c0;">自动的加载某些 vars_files，tasks 以及 handlers</span>。基于 roles 对内容进行分组，使得我们可以容易地与其他用户分享 roles</span></p>
<p>考虑下面的Ansible项目结构：</p>
<pre class="crayon-plain-tag"># 剧本
site.yml
webservers.yml
fooservers.yml
# 角色，可以被剧本引用
roles/
   common/
     # 部分目录可以不存在
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   webservers/
     files/          # copy、script模块引用的文件
     templates/      # template模块引用的模版
     tasks/          # main.yaml为该角色的主任务列表，main.yaml可以include该目录的其它文件中定义的任务
     handlers/       # main.yaml为该角色的Handler列表
     vars/           # 自动添加到引用该角色的Play中
     defaults/       # 为角色、该角色依赖的其它角色，定义默认变量
     meta/           # 依赖的其它角色</pre>
<p>其中剧本webservers.yml的内容： </p>
<pre class="crayon-plain-tag">---
- hosts: webservers
  # Play包含了两个角色
  roles:
     - common
     - webservers</pre>
<p>对于该剧本包含的任何Role x：</p>
<ol>
<li>如果 roles/x/tasks/main.yml 存在, 其中列出的 tasks 将被添加到 play 中</li>
<li>所有 include tasks 可以引用 roles/x/tasks/ 中的文件，不需要指明文件的路径</li>
<li>如果 roles/x/handlers/main.yml 存在, 其中列出的 handlers 将被添加到 play 中</li>
<li>如果 roles/x/vars/main.yml 存在, 其中列出的 variables 将被添加到 play 中</li>
<li>如果 roles/x/meta/main.yml 存在, 其中列出的 “角色依赖” 将被添加到 roles 列表中，需要1.3+</li>
<li>所有 copy tasks 可以引用 roles/x/files/ 中的文件，不需要指明文件的路径</li>
<li>所有 script tasks 可以引用 roles/x/files/ 中的脚本，不需要指明文件的路径</li>
<li>所有 template tasks 可以引用 roles/x/templates/ 中的文件，不需要指明文件的路径</li>
</ol>
<div class="blog_h3"><span class="graybg">参数化Role</span></div>
<p>包含角色时，可以指定参数：</p>
<pre class="crayon-plain-tag">- hosts: webservers
  roles:
    - common
    - { role: foo_app_instance, dir: '/opt/a',  port: 5000 }
    - { role: foo_app_instance, dir: '/opt/b',  port: 5001 }</pre>
<div class="blog_h3"><span class="graybg">条件性Role</span></div>
<pre class="crayon-plain-tag">---

- hosts: webservers
  roles:
    - { role: some_role, when: "ansible_os_family == 'RedHat'" }</pre>
<p>对于Role 的每个Task，都会执行when中的判断，仅当结果为true，该Task才被包含到剧本中。</p>
<div class="blog_h3"><span class="graybg">为角色添加Tag</span></div>
<pre class="crayon-plain-tag">---

- hosts: webservers
  roles:
    - { role: foo, tags: ["bar", "baz"] }</pre>
<p>Tags 是一种实现部分运行 playbook 的机制</p>
<div class="blog_h3"><span class="graybg">前/后置Task</span></div>
<p>如果你想在Role执行之前、之后，执行一些Task，使用下面的配置：</p>
<pre class="crayon-plain-tag">---

- hosts: webservers

  pre_tasks:
    - shell: echo 'hello'

  roles:
    - { role: some_role }

  tasks:
    - shell: echo 'still busy'

  post_tasks:
    - shell: echo 'goodbye'</pre>
<p>注意，使用了Role的Play，<span style="background-color: #c0c0c0;">如果同时还定义了tasks段，则其中的任务都在Role中的Task结束后才执行</span>。 </p>
<div class="blog_h3"><span class="graybg">角色默认变量</span></div>
<p>你可以为角色、以及这些角色依赖（dependent，包含的其它，定义在角色的meta/main.yaml）的角色，设置默认变量。</p>
<p>这些变量在所有可用变量中拥有<span style="background-color: #c0c0c0;">最低优先级</span>，可能被其他地方定义的变量(包括 inventory 中的变量)所覆盖。</p>
<div class="blog_h3"><span class="graybg">角色依赖</span></div>
<p>可以将其它角色，自动拉取到当前角色中。依赖的角色定义在 meta/main.yaml 中：</p>
<pre class="crayon-plain-tag">---
dependencies:
  #                   可以传递参数
  - { role: postgres, dbname: blarg, other_parameter: 12 }
  # 可以使用绝对路径引用Role
  - { role: '/path/to/common/roles/foo', x: 1 }
  # 甚至引用Git服务器、压缩包中的角色，可以指定友好角色名
  - { role: 'git+http://git.example.com/repos/role-foo,v1.1,foo' }
  - { role: '/path/to/tar/file.tgz,,friendly-name' }</pre>
<p>我们知道，Role定义了完整的可执行实体，Playbook可以仅简单的引用、传递参数给它。</p>
<p>当Role依赖于其它Role时，<span style="background-color: #c0c0c0;">被依赖的Role先执行，这个过程可以是递归的</span>。 </p>
<p><span style="background-color: #c0c0c0;">默认情况下，被依赖的Role仅仅会执行一次</span>。如果有两个Role同时依赖它，第二个依赖被忽略。要改变此行为，可以修改依赖规则：</p>
<pre class="crayon-plain-tag">---
allow_duplicates: yes
dependencies:
- { role: tire }
- { role: brake }


# 在上述配置下
---
dependencies:
- { role: wheel, n: 1 }
- { role: wheel, n: 2 }
- { role: wheel, n: 3 }
- { role: wheel, n: 4 }
# 多次依赖
---
allow_duplicates: yes
dependencies:
- { role: tire }
- { role: brake }
# 执行序列：
# tire(n=1)
# brake(n=1)
# wheel(n=1)
# tire(n=2)
# brake(n=2)
# wheel(n=2)
# ...
#</pre>
<div class="blog_h3"><span class="graybg">模块嵌入到角色 </span></div>
<p>如果你自己开发了一个Ansible 模块，你甚至可以将它作为角色的一部分，进行分发。 </p>
<pre class="crayon-plain-tag">roles/
   my_custom_modules/
       library/
          module1
          module2

- hosts: webservers
  roles:
    - my_custom_modules
    - some_other_role_using_my_custom_modules
    - yet_another_role_using_my_custom_modules</pre>
<p>这些自定义模块，在当前Role，以及后面执行的Role中可用。</p>
<div class="blog_h2"><span class="graybg">Variables</span></div>
<div class="blog_h3"><span class="graybg">Inventory中的变量</span></div>
<p>你可以为主机或分组定义变量，具体参考<a href="#inventory">前面的章节</a>。</p>
<div class="blog_h3"><span class="graybg">Playbook变量</span></div>
<p>在Play中，你可以直接定义变量：</p>
<pre class="crayon-plain-tag">- hosts: webservers
  vars:
    http_port: 80</pre>
<div class="blog_h3"><span class="graybg">Role中的变量 </span></div>
<p>角色有vars、defaults目录，存放变量信息。</p>
<div class="blog_h3"><span class="graybg">Jinja2语法</span></div>
<p>为了使用变量，你需要在Playbook以及它引用的文件中，使用Jinja模板。该模板语言使用<pre class="crayon-plain-tag">{{}}</pre>作为变量占位符： </p>
<pre class="crayon-plain-tag">template: src=foo.cfg.j2 dest={{ remote_install_path }}/foo.cfg</pre>
<p><a href="/gotpl">和Go Template是相似的</a>。</p>
<p>需要注意，YAML语法要求，如果值以{{  }}开头的话我们需要将整行用双引号包起来，防止错误的解释为YAML字典：</p>
<pre class="crayon-plain-tag"># 错误
- hosts: app_servers
  vars:
      app_path: {{ base_path }}/22

# 正确
- hosts: app_servers
  vars:
       app_path: "{{ base_path }}/22 </pre>
<div class="blog_h3"><span class="graybg">Jinja2过滤器</span></div>
<p>类似于Go Template的函数语法：<pre class="crayon-plain-tag">{{ my_variable|default('my_variable is not defined') }}</pre></p>
<p>Jinja2包含许多内置过滤器：<a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters">https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters</a></p>
<div class="blog_h3"><span class="graybg">注册结果为变量</span></div>
<p>Ansible模块通常会返回一个数据结构，你可以把模块调用的结果存放到一个变量中：</p>
<pre class="crayon-plain-tag">- hosts: web_servers

  tasks:

     - shell: /usr/bin/foo
       # 注册结果为变量
       register: foo_result
       ignore_errors: True

     - shell: /usr/bin/bar
       # 使用变量进行判断
       when: foo_result.rc == 5</pre>
<div class="blog_h3"><span class="graybg">结果字段说明</span></div>
<p>这个数据结构包含的字段如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>backup_file</td>
<td>某些操控文件的模块支持 backup=no|yes，这个字段存放备份文件的路径</td>
</tr>
<tr>
<td>changed</td>
<td>bool，提示任务是否对目标进行了变更</td>
</tr>
<tr>
<td>diff</td>
<td>数组，说明执行任务前后的变化</td>
</tr>
<tr>
<td>failed</td>
<td>bool，提示任务是否失败</td>
</tr>
<tr>
<td>invocation</td>
<td>对象，模块调用的细节信息</td>
</tr>
<tr>
<td>msg</td>
<td>一般性的、中继给用户的消息</td>
</tr>
<tr>
<td>rc</td>
<td>某些模块，例如raw shell command，会执行命令，这个字段存放命令退出码</td>
</tr>
<tr>
<td>results</td>
<td>数组。如果该字段存在，则提示任务是一个loop，该字段存放每次loop的结果数据结构</td>
</tr>
<tr>
<td>skipped</td>
<td>bool，提示任务是否被跳过</td>
</tr>
<tr>
<td>stderr</td>
<td>标准错误</td>
</tr>
<tr>
<td>stderr_lines</td>
<td>标准错误，每行一个元素，构成数组</td>
</tr>
<tr>
<td>stdout</td>
<td>标准输出</td>
</tr>
<tr>
<td>stdout_lines</td>
<td>标准输出，每行一个元素，构成数组</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">变量访问语法 </span></div>
<pre class="crayon-plain-tag"># 方括号语法
{{ ansible_eth0["ipv4"]["address"] }}
# 点好导航语法
{{ ansible_eth0.ipv4.address }}</pre>
<div class="blog_h3"><span class="graybg">魔法变量</span></div>
<p>Ansible会自动提供给你一些变量，即使你并没有定义过它们，这些变量名是预留的，用户不应当覆盖它们：</p>
<ol>
<li>hostvars：可以让你访问其它主机的变量、包括哪些主机中获取到的facts</li>
<li>group_names：当前主机所在所有群组的列表</li>
<li>groups：inventory中所有群组/主机的列表。可以用来列举主机：<br />
<pre class="crayon-plain-tag">{% for host in groups['app_servers'] %}
   {{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}
{% endfor %}</pre>
</li>
</ol>
<p><a href="#internal-magic-vars">所有魔法变量的列表，请参考上文</a>。</p>
<div class="blog_h3"><span class="graybg">命令行变量</span></div>
<p>可以在调用Ansible命令时传递变量：</p>
<pre class="crayon-plain-tag"># ---

# - hosts: '{{ hosts }}'
#   remote_user: '{{ user }}'

#   tasks:
#      - ...

ansible-playbook release.yml --extra-vars "hosts=vipers user=starbuck"
                             # 支持JSON
                             --extra-vars '{"pacman":"mrs","ghosts":["inky","pinky","clyde","sue"]}'</pre>
<div class="blog_h3"><span class="graybg">变量优先级</span></div>
<p>从高到低：</p>
<p>命令行-e ⇨</p>
<p style="padding-left: 30px;">Inventory中定义的连接变量（例如ansible_ssh_user）⇨</p>
<p style="padding-left: 60px;">Play中的变量、included的变量、Role中的变量⇨</p>
<p style="padding-left: 90px;">Inventory中的其它变量 ⇨</p>
<p style="padding-left: 120px;">Facts ⇨</p>
<p style="padding-left: 150px;">Role默认变量 </p>
<div class="blog_h2"><span class="graybg">Facts</span></div>
<p>这是一类变量，但是它们的值是Ansible自动检测出来的，而非你去指定的。</p>
<p>使用命令 <pre class="crayon-plain-tag">ansible hostname -m setup</pre>可以获取Facts，数据量非常大：</p>
<pre class="crayon-plain-tag">"ansible_all_ipv4_addresses": [
    "REDACTED IP ADDRESS"
],
"ansible_all_ipv6_addresses": [
    "REDACTED IPV6 ADDRESS"
],
"ansible_architecture": "x86_64",
"ansible_bios_date": "09/20/2012",
"ansible_bios_version": "6.00",
"ansible_cmdline": {
    "BOOT_IMAGE": "/boot/vmlinuz-3.5.0-23-generic",
    "quiet": true,
    "ro": true,
    "root": "UUID=4195bff4-e157-4e41-8701-e93f0aec9e22",
    "splash": true
},
"ansible_date_time": {
    "date": "2013-10-02",
    "day": "02",
    "epoch": "1380756810",
    "hour": "19",
    "iso8601": "2013-10-02T23:33:30Z",
    "iso8601_micro": "2013-10-02T23:33:30.036070Z",
    "minute": "33",
    "month": "10",
    "second": "30",
    "time": "19:33:30",
    "tz": "EDT",
    "year": "2013"
},
"ansible_default_ipv4": {
    "address": "REDACTED",
    "alias": "eth0",
    "gateway": "REDACTED",
    "interface": "eth0",
    "macaddress": "REDACTED",
    "mtu": 1500,
    "netmask": "255.255.255.0",
    "network": "REDACTED",
    "type": "ether"
},
"ansible_default_ipv6": {},
"ansible_devices": {
    "fd0": {
        "holders": [],
        "host": "",
        "model": null,
        "partitions": {},
        "removable": "1",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "0",
        "sectorsize": "512",
        "size": "0.00 Bytes",
        "support_discard": "0",
        "vendor": null
    },
    "sda": {
        "holders": [],
        "host": "SCSI storage controller: LSI Logic / Symbios Logic 53c1030 PCI-X Fusion-MPT Dual Ultra320 SCSI (rev 01)",
        "model": "VMware Virtual S",
        "partitions": {
            "sda1": {
                "sectors": "39843840",
                "sectorsize": 512,
                "size": "19.00 GB",
                "start": "2048"
            },
            "sda2": {
                "sectors": "2",
                "sectorsize": 512,
                "size": "1.00 KB",
                "start": "39847934"
            },
            "sda5": {
                "sectors": "2093056",
                "sectorsize": 512,
                "size": "1022.00 MB",
                "start": "39847936"
            }
        },
        "removable": "0",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "41943040",
        "sectorsize": "512",
        "size": "20.00 GB",
        "support_discard": "0",
        "vendor": "VMware,"
    },
    "sr0": {
        "holders": [],
        "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)",
        "model": "VMware IDE CDR10",
        "partitions": {},
        "removable": "1",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "2097151",
        "sectorsize": "512",
        "size": "1024.00 MB",
        "support_discard": "0",
        "vendor": "NECVMWar"
    }
},
"ansible_distribution": "Ubuntu",
"ansible_distribution_release": "precise",
"ansible_distribution_version": "12.04",
"ansible_domain": "",
"ansible_env": {
    "COLORTERM": "gnome-terminal",
    "DISPLAY": ":0",
    "HOME": "/home/mdehaan",
    "LANG": "C",
    "LESSCLOSE": "/usr/bin/lesspipe %s %s",
    "LESSOPEN": "| /usr/bin/lesspipe %s",
    "LOGNAME": "root",
    "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:",
    "MAIL": "/var/mail/root",
    "OLDPWD": "/root/ansible/docsite",
    "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "PWD": "/root/ansible",
    "SHELL": "/bin/bash",
    "SHLVL": "1",
    "SUDO_COMMAND": "/bin/bash",
    "SUDO_GID": "1000",
    "SUDO_UID": "1000",
    "SUDO_USER": "mdehaan",
    "TERM": "xterm",
    "USER": "root",
    "USERNAME": "root",
    "XAUTHORITY": "/home/mdehaan/.Xauthority",
    "_": "/usr/local/bin/ansible"
},
"ansible_eth0": {
    "active": true,
    "device": "eth0",
    "ipv4": {
        "address": "REDACTED",
        "netmask": "255.255.255.0",
        "network": "REDACTED"
    },
    "ipv6": [
        {
            "address": "REDACTED",
            "prefix": "64",
            "scope": "link"
        }
    ],
    "macaddress": "REDACTED",
    "module": "e1000",
    "mtu": 1500,
    "type": "ether"
},
"ansible_form_factor": "Other",
"ansible_fqdn": "ubuntu2.example.com",
"ansible_hostname": "ubuntu2",
"ansible_interfaces": [
    "lo",
    "eth0"
],
"ansible_kernel": "3.5.0-23-generic",
"ansible_lo": {
    "active": true,
    "device": "lo",
    "ipv4": {
        "address": "127.0.0.1",
        "netmask": "255.0.0.0",
        "network": "127.0.0.0"
    },
    "ipv6": [
        {
            "address": "::1",
            "prefix": "128",
            "scope": "host"
        }
    ],
    "mtu": 16436,
    "type": "loopback"
},
"ansible_lsb": {
    "codename": "precise",
    "description": "Ubuntu 12.04.2 LTS",
    "id": "Ubuntu",
    "major_release": "12",
    "release": "12.04"
},
"ansible_machine": "x86_64",
"ansible_memfree_mb": 74,
"ansible_memtotal_mb": 991,
"ansible_mounts": [
    {
        "device": "/dev/sda1",
        "fstype": "ext4",
        "mount": "/",
        "options": "rw,errors=remount-ro",
        "size_available": 15032406016,
        "size_total": 20079898624
    }
],
"ansible_nodename": "ubuntu2.example.com",
"ansible_os_family": "Debian",
"ansible_pkg_mgr": "apt",
"ansible_processor": [
    "Intel(R) Core(TM) i7 CPU         860  @ 2.80GHz"
],
"ansible_processor_cores": 1,
"ansible_processor_count": 1,
"ansible_processor_threads_per_core": 1,
"ansible_processor_vcpus": 1,
"ansible_product_name": "VMware Virtual Platform",
"ansible_product_serial": "REDACTED",
"ansible_product_uuid": "REDACTED",
"ansible_product_version": "None",
"ansible_python_version": "2.7.3",
"ansible_selinux": false,
"ansible_ssh_host_key_dsa_public": "REDACTED KEY VALUE"
"ansible_ssh_host_key_ecdsa_public": "REDACTED KEY VALUE"
"ansible_ssh_host_key_rsa_public": "REDACTED KEY VALUE"
"ansible_swapfree_mb": 665,
"ansible_swaptotal_mb": 1021,
"ansible_system": "Linux",
"ansible_system_vendor": "VMware, Inc.",
"ansible_user_id": "root",
"ansible_userspace_architecture": "x86_64",
"ansible_userspace_bits": "64",
"ansible_virtualization_role": "guest",
"ansible_virtualization_type": "VMware"</pre>
<p>Facts可以直接在Playbook中引用，例如获取第一块磁盘的型号：<pre class="crayon-plain-tag">{{ ansible_devices.sda.model }}</pre></p>
<div class="blog_h3"><span class="graybg">禁用Facts</span></div>
<p>你可以为Play禁用Fact收集：</p>
<pre class="crayon-plain-tag">- hosts: whatever
  gather_facts: no</pre>
<div class="blog_h3"><span class="graybg">设置Facts</span></div>
<p>一般情况下，Facts都是Ansible自动检测的，目标主机的信息。</p>
<p>你可以在目标主机下放置配置文件，硬编码一些变量值：</p>
<pre class="crayon-plain-tag">[general]
asdf=1
bar=2</pre>
<p>这将产生如下变量： </p>
<pre class="crayon-plain-tag"># ansible &lt;hostname&gt; -m setup -a "filter=ansible_local"

"ansible_local": {
        "preferences": {
            "general": {
                "asdf" : "1",
                "bar"  : "2"
            }
        }
 }</pre>
<p>可以在剧本或模板中引用这种变量：<pre class="crayon-plain-tag">{{ ansible_local.preferences.general.asdf }}</pre> </p>
<div class="blog_h3"><span class="graybg">Facts缓存 </span></div>
<p>在一个主机上运行的剧本中，引用另外一个主机中的变量，是可能的：<pre class="crayon-plain-tag">{{ hostvars['asdf.example.com']['ansible_os_family'] }}</pre></p>
<p>如果没有Facts缓存，则Ansible需要在当前主机和另外一台主机进行通信，获取必要的变量。这在大规模集群中，可能造成负担。</p>
<p>Facts缓存是一种分布式缓存，可以依托Jsonfile或Redis实现，解决上述问题。</p>
<div class="blog_h2"><span class="graybg">条件</span></div>
<div class="blog_h3"><span class="graybg">when语句</span></div>
<p>可以使用逻辑表达式、Jinja2过滤器，来编写when，在运行时决定Task是否执行：</p>
<pre class="crayon-plain-tag">tasks:
  - name: "shutdown Debian flavored systems"
    command: /sbin/shutdown -t now
    when: ansible_os_family == "Debian"   
    # 使用过滤器
    when: result|success                
    when: ansible_os_family == "RedHat" and ansible_lsb.major_release|int &gt;= 6
    # 取反
    when: not var0
    # 是否定义
    when: bar is not defined
    # 字符串包含
    when: "'reticulating splines' in output"
tasks:
    - command: echo {{ item }}
      # 在循环中使用
      with_items: [ 0, 2, 4, 6, 8, 10 ]
      when: item &gt; 5</pre>
<p>除了Task，你还可以在roles、include语句中使用when：</p>
<pre class="crayon-plain-tag">- include: tasks/sometasks.yml
  when: "'reticulating splines' in output"


- hosts: webservers
  roles:
     - { role: debian_stock_config, when: ansible_os_family == 'Debian' }</pre>
<div class="blog_h2"><span class="graybg">循环 </span></div>
<p>如果想在一个任务中干很多事，比如创建一群用户、安装很多包、或者重复一个轮询步骤直到收到某个特定结果，考虑使用循环支持。</p>
<div class="blog_h3"><span class="graybg">标准循环</span></div>
<p>循环变量是简单值：</p>
<pre class="crayon-plain-tag">- name: add several users
  # 通过item引用当前循环变量
  user: name={{ item }} state=present groups=wheel
  # 循环
  with_items:
     - testuser1
     - testuser2
  # 可以引用变量定义的列表
  with_items: "{{somelist}}"</pre>
<p>循环变量是字典：</p>
<pre class="crayon-plain-tag">- name: add several users
  user: name={{ item.name }} state=present groups={{ item.groups }}
  with_items:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }</pre>
<p>循环变量是哈希表的条目：</p>
<pre class="crayon-plain-tag">---
users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-7890
  bob:
    name: Bob Bananarama
    telephone: 987-654-3210



tasks:
  - name: Print phone records
    #                   访问键             访问值
    debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
    with_dict: "{{users}}"</pre>
<div class="blog_h3"><span class="graybg">嵌套循环 </span></div>
<pre class="crayon-plain-tag">- name: give users access to multiple databases
  mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  with_nested:
    # 外层变量
    - [ 'alice', 'bob' ]
    # 内层变量
    - [ 'clientdb', 'employeedb', 'providerdb' ]</pre>
<div class="blog_h3"><span class="graybg">子元素嵌套</span></div>
<p>对于变量定义：</p>
<pre class="crayon-plain-tag">---
users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
    mysql:
        password: mysql-password
        hosts:
          - "%"
          - "127.0.0.1"
          - "::1"
          - "localhost"
        privs:
          - "*.*:SELECT"
          - "DB1.*:ALL"
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    mysql:
        password: other-mysql-password
        hosts:
          - "db1"
        privs:
          - "*.*:SELECT"
          - "DB2.*:ALL"</pre>
<p>如果我们想给每个name创建系统用户，并且为每个用户的每个authorized创建密钥登陆，可以：</p>
<pre class="crayon-plain-tag">- user: name={{ item.name }} state=present generate_ssh_key=yes
  # 遍历每个用户，创建用户
  with_items: "{{users}}"

#                          items.0表示当前外层变量   
#                                     items.1表示当前内层变量                          
- authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
  with_subelements:
     # 外层是用户
     - users
     # 内层是当前用户的authorized列表（users[x].authorized）
     - authorized</pre>
<p>类似的，创建MySQL用户</p>
<pre class="crayon-plain-tag">- name: Setup MySQL users
  mysql_user: name={{ item.0.user }} password={{ item.0.mysql.password }} 
              host={{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}
  with_subelements:
    - users
    - mysql.hosts</pre>
<div class="blog_h3"><span class="graybg">访问文件列表</span></div>
<pre class="crayon-plain-tag">---
- hosts: all

  tasks:

    # 确保目录存在
    - file: dest=/etc/fooapp state=directory

    # 拷贝所有匹配Pattern的文件
    - copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
      with_fileglob:
        - /playbooks/files/fooapp/*</pre>
<div class="blog_h3"><span class="graybg">同时迭代两个列表</span></div>
<pre class="crayon-plain-tag">---
alpha: [ 'a', 'b', 'c', 'd' ]
numbers:  [ 1, 2, 3, 4 ]



tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
        - "{{alpha}}"
        - "{{numbers}}"</pre>
<div class="blog_h3"><span class="graybg">访问列表的索引</span> </div>
<pre class="crayon-plain-tag">- name: indexed loop demo
  debug: msg="at array position {{ item.0 }} there is a value {{ item.1 }}"
  with_indexed_items: "{{some_list}}"</pre>
<div class="blog_h3"><span class="graybg">随机选取</span></div>
<p>可以从列表中随机选取一组值，然后循环：</p>
<pre class="crayon-plain-tag">- debug: msg={{ item }}
  with_random_choice:
     - "go through the door"
     - "drink from the goblet"
     - "press the red button"
     - "do nothing"</pre>
<div class="blog_h3"><span class="graybg">遍历整数序列 </span></div>
<pre class="crayon-plain-tag">---
- hosts: all

  tasks:

    # create groups
    - group: name=evens state=present
    - group: name=odds state=present

    - user: name={{ item }} state=present groups=evens
      #              起      止      格式化item
      with_sequence: start=0 end=32 format=testuser%02x

    - file: dest=/var/stuff/{{ item }} state=directory
      #                             步进
      with_sequence: start=4 end=16 stride=2


    - group: name=group{{ item }} state=present
      with_sequence: count=4</pre>
<div class="blog_h3"><span class="graybg">Do-Until</span></div>
<pre class="crayon-plain-tag">- action: shell /usr/bin/foo
  # 结果注册为变量
  register: result
  # 循环判断
  until: result.stdout.find("all systems go") != -1
  # 最大重试次数和延迟
  retries: 5
  delay: 10</pre>
<div class="blog_h3"><span class="graybg">逐行处理 </span></div>
<pre class="crayon-plain-tag">- name: Example of looping over a command result
  shell: /usr/bin/frobnicate {{ item }}
  with_lines: /usr/bin/frobnications_per_host --param {{ inventory_hostname }}</pre>
<div class="blog_h2"><span class="graybg">异步和轮询</span></div>
<p>默认情况下playbook中的任务执行时会一直保持连接，直到该任务在每个节点都执行完毕。</p>
<p>对于长时间运行的任务，可以一起异步发动，然后进行轮询，来判断是否结束。</p>
<pre class="crayon-plain-tag">- hosts: all
  remote_user: root

  tasks:

  - name: simulate long running op (15 sec), wait for up to 45 sec, poll every 5 sec
    command: /bin/sleep 15
    # 异步等待45s
    async: 45
    # 每5s检查一下
    poll: 5</pre>
<div class="blog_h2"><span class="graybg">错误处理</span></div>
<div class="blog_h3"><span class="graybg">忽略错误</span></div>
<p>默认情况下，一旦出现错误，后续任务就不在该主机上运行，可以改变此行为：</p>
<pre class="crayon-plain-tag">- name: this will not be counted as a failure
  command: /bin/false</pre>
<div class="blog_h3"><span class="graybg">修改失败定义 </span></div>
<p>默认情况下，退出码决定是否失败，你可以修改为基于stderr判断：</p>
<pre class="crayon-plain-tag">- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: "'FAILED' in command_result.stderr"</pre>
<div class="blog_h2"><span class="graybg">标签</span></div>
<p>前面提到过Tags，它的作用是，仅仅运行Playbook的一部分。 </p>
<pre class="crayon-plain-tag"># 可以对include添加tag
include: foo.yml tags=web,foo
tasks:
    
    - yum: name={{ item }} state=installed
      with_items:
         - httpd
         - memcached
      tags:
         - packages

    - template: src=templates/src.j2 dest=/etc/foo.conf
      tags:
         - configuration</pre><br />
<pre class="crayon-plain-tag"># 仅仅执行具有某些标签的任务
ansible-playbook example.yml --tags "configuration,packages"

# 跳过具有特定标签的任务
ansible-playbook example.yml --skip-tags "notification"</pre>
<div class="blog_h3"><span class="graybg">特殊标签</span></div>
<p>Ansible支持两个特殊标签：</p>
<ol>
<li>always，具有这种标签的role或play总是会运行，除非明确给出<pre class="crayon-plain-tag">--skip-tags always</pre></li>
<li>never，具有这种标签的role不会执行，除非明确给出<pre class="crayon-plain-tag">--tags never</pre></li>
</ol>
<div class="blog_h2"><span class="graybg">Vault</span></div>
<p>Ansible 1.5的新版本中，作为Vault Ansible 的一项新功能可将例如passwords、keys等敏感数据文件进行加密，而非存放在明文的 playbooks 或 roles 中。这些 vault 文件可以分散存放也可以集中存放。</p>
<div class="blog_h3"><span class="graybg">命令</span></div>
<p>创建加密文件：</p>
<pre class="crayon-plain-tag">ansible-vault create foo.yml</pre>
<p>你需要指定密码，算法AES。未来使用Vault时需要该密码。包括编辑、查看它时：</p>
<pre class="crayon-plain-tag">ansible-vault edit foo.yml
ansible-vault view foo.yml bar.yml baz.yml</pre>
<p>可以修改、批量修改多个Vault的密码：</p>
<pre class="crayon-plain-tag">ansible-vault rekey foo.yml bar.yml baz.yml</pre>
<p>解密：</p>
<pre class="crayon-plain-tag">ansible-vault decrypt foo.yml bar.yml baz.yml</pre>
<div class="blog_h3"><span class="graybg">在剧本中使用 </span></div>
<pre class="crayon-plain-tag">#                         询问Vault密码
ansible-playbook site.yml --ask-vault-pass

# 密码可以存放在文件或脚本中
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
ansible-playbook site.yml --vault-password-file ~/.vault_pass.py</pre>
<div class="blog_h1"><span class="graybg">Jinja语法</span></div>
<div class="blog_h2"><span class="graybg">内置过滤器</span></div>
<p>过滤器输入显示为第一个参数：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">过滤器</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">abs(number)</td>
<td>取绝对值</td>
</tr>
<tr>
<td class="blog_h3">attr(obj, name)</td>
<td>获取对象属性</td>
</tr>
<tr>
<td class="blog_h3">capitalize(s)</td>
<td>转为大写</td>
</tr>
<tr>
<td class="blog_h3">center(value, width=80)</td>
<td>将字符置于中间</td>
</tr>
<tr>
<td class="blog_h3">
<p>default(value, default=u'', boolean=False)</p>
</td>
<td>
<p>别名：d</p>
<p>如果变量未定义，则返回指定的默认值：</p>
<pre class="crayon-plain-tag">{{ my_variable|default('my_variable is not defined') }}</pre>
<p>如果需要当变量可以转换为false时，返回默认值，则第二个参数需要设置为true：</p>
<pre class="crayon-plain-tag">{{ ''|default('the string was empty', true) }}</pre>
</td>
</tr>
<tr>
<td class="blog_h3">escape(s)</td>
<td>转换为HTML安全的序列 </td>
</tr>
<tr>
<td class="blog_h3">filesizeformat(value, binary=False)</td>
<td>转换为人类可读的尺寸格式，例如13kb 4.1MB</td>
</tr>
<tr>
<td class="blog_h3">first(seq)</td>
<td>返回序列的第一个元素</td>
</tr>
<tr>
<td class="blog_h3">float(value, default=0.0)</td>
<td>转换为浮点数 </td>
</tr>
<tr>
<td class="blog_h3">format(value, *args, **kwargs)</td>
<td>
<p> C风格的格式化：
<p><pre class="crayon-plain-tag">{{ "%s - %s"|format("Hello?", "Foo!") }} </pre>
</td>
</tr>
<tr>
<td class="blog_h3">indent(s, width=4, indentfirst=False)</td>
<td>缩进文本</td>
</tr>
<tr>
<td class="blog_h3">int(value, default=0)</td>
<td>转换为整数</td>
</tr>
<tr>
<td class="blog_h3">join(value, d=u'', attribute=None)</td>
<td>连接列表元素为字符串：<pre class="crayon-plain-tag">{{ [1, 2, 3]|join('|') }}</pre></td>
</tr>
<tr>
<td class="blog_h3">last(seq)</td>
<td>返回列表的最后一个元素</td>
</tr>
<tr>
<td class="blog_h3">length(object)</td>
<td>
<p>别名：count</p>
<p>返回对象长度</p>
</td>
</tr>
<tr>
<td class="blog_h3">list(value)</td>
<td>转换为列表，如果输入是字符串，则返回字符列表</td>
</tr>
<tr>
<td class="blog_h3">lower(s)</td>
<td>转换为小写</td>
</tr>
<tr>
<td class="blog_h3">map()</td>
<td>
<p>针对序列的每个对象进行转换操作，可以：</p>
<ol>
<li>得到对象的某个属性：<br />
<pre class="crayon-plain-tag">users|map(attribute='username')|join(', ')</pre>
</li>
<li>为每个对象应用过滤器：<br />
<pre class="crayon-plain-tag">titles|map('lower')|join(', ') </pre>
</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">pprint(value, verbose=False)</td>
<td>漂亮打印，由于调试</td>
</tr>
<tr>
<td class="blog_h3">random(seq)</td>
<td>随机返回一个条目</td>
</tr>
<tr>
<td class="blog_h3">reject()</td>
<td>工作方式类似于map，根据属性/过滤器来筛掉某些元素</td>
</tr>
<tr>
<td class="blog_h3">replace(s, old, new, count=None)</td>
<td>
<p>字符串替换：</p>
<pre class="crayon-plain-tag">{{ "Hello World"|replace("Hello", "Goodbye") }}
    -&gt; Goodbye World</pre>
</td>
</tr>
<tr>
<td class="blog_h3">reverse(value)</td>
<td>反转列表或迭代器</td>
</tr>
<tr>
<td class="blog_h3">round(value, precision=0, method='common')</td>
<td>舍入</td>
</tr>
<tr>
<td class="blog_h3">select()</td>
<td>
<p>工作方式类似于map，根据属性/过滤器来选择某些元素
<pre class="crayon-plain-tag">{{ numbers|select("odd") }} </pre>
</td>
</tr>
<tr>
<td class="blog_h3">sort(value, reverse=False, case_sensitive=False, attribute=None)</td>
<td>
<p>排序： 
<pre class="crayon-plain-tag">{% for item in iterable|sort %}
    ...
{% endfor %}</pre>
</td>
</tr>
<tr>
<td class="blog_h3">string(object)</td>
<td>转换为字符串</td>
</tr>
<tr>
<td class="blog_h3">striptags(value)</td>
<td>脱去SGML/XML标记，合并多余的空白为单个空格</td>
</tr>
<tr>
<td class="blog_h3">sum(iterable, attribute=None, start=0)</td>
<td>求和 </td>
</tr>
<tr>
<td class="blog_h3">trim(value)</td>
<td>去掉首尾空白</td>
</tr>
<tr>
<td class="blog_h3">truncate(s, length=255, killwords=False, end='...')</td>
<td>
<p>截断字符串：
<p><pre class="crayon-plain-tag">{{ "foo bar"|truncate(5) }}
    -&gt; "foo ..."
{{ "foo bar"|truncate(5, True) }}
    -&gt; "foo b..."</pre>
</td>
</tr>
<tr>
<td class="blog_h3">upper(s)</td>
<td>转为大写</td>
</tr>
<tr>
<td class="blog_h3">urlencode(value)</td>
<td>转换为URL中适用的字符串，UTF-8编码</td>
</tr>
<tr>
<td class="blog_h3">wordcount(s)</td>
<td>统计单词数量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">内置测试</span></div>
<p>对于测试<pre class="crayon-plain-tag">test(arg,arg1,arg2...)</pre>，在if语句中需要这样：<pre class="crayon-plain-tag">{% if arg is test(arg1,arg2...) %}</pre></p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">测试</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">callable(object)</td>
<td>是否可调用</td>
</tr>
<tr>
<td class="blog_h3">defined(value)</td>
<td>变量是否已定义</td>
</tr>
<tr>
<td class="blog_h3">divisibleby(value, num)</td>
<td>是否可以整除</td>
</tr>
<tr>
<td class="blog_h3">escaped(value)</td>
<td>是否已经转义过</td>
</tr>
<tr>
<td class="blog_h3">even(value)</td>
<td>是否偶数</td>
</tr>
<tr>
<td class="blog_h3">iterable(value)</td>
<td>是否可迭代</td>
</tr>
<tr>
<td class="blog_h3">lower(value)</td>
<td>是否小写</td>
</tr>
<tr>
<td class="blog_h3">mapping(value)</td>
<td>是否是映射，例如dict</td>
</tr>
<tr>
<td class="blog_h3">none(value)</td>
<td>是否为None</td>
</tr>
<tr>
<td class="blog_h3">number(value)</td>
<td>是否为数字</td>
</tr>
<tr>
<td class="blog_h3">odd(value)</td>
<td>是否为奇数</td>
</tr>
<tr>
<td class="blog_h3">sameas(value, other)</td>
<td>判断是否和另外一个对象具有相同的内存地址</td>
</tr>
<tr>
<td class="blog_h3">sequence(value)</td>
<td>是否可迭代</td>
</tr>
<tr>
<td class="blog_h3">string(value)</td>
<td>是否字符串</td>
</tr>
<tr>
<td class="blog_h3">undefined(value)</td>
<td>是否未定义</td>
</tr>
<tr>
<td class="blog_h3">upper(value)</td>
<td>是否为大写</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">全局函数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">函数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">range([start], stop[, step])</td>
<td>
<p>返回一个等差数列的列表：</p>
<p><pre class="crayon-plain-tag">{% for number in range(10 - users|count) %}
    &lt;li class="empty"&gt;&lt;span&gt;...&lt;/span&gt;&lt;/li&gt;
{% endfor %} </pre>
</td>
</tr>
<tr>
<td class="blog_h3">dict(**items)</td>
<td>转换为字典，<pre class="crayon-plain-tag">{'foo' : 'bar'}</pre> 与 dict<pre class="crayon-plain-tag">(foo=bar)</pre>等价</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">定界符</span></div>
<p>要在模板中插入一段表达式，需要使用双括号 <pre class="crayon-plain-tag">{{ expr }}</pre></p>
<p>要在模板中插入一段语句，需要使用<pre class="crayon-plain-tag">{% statement%}</pre></p>
<div class="blog_h2"><span class="graybg">注释</span></div>
<p>注释语法为：<pre class="crayon-plain-tag">{#  #}</pre></p>
<div class="blog_h2"><span class="graybg">转义</span></div>
<p>要转义双括号，可以使用：<pre class="crayon-plain-tag">{{ '{{' }}</pre></p>
<p>对于大段文本，需要保持原样，可以：</p>
<pre class="crayon-plain-tag">{% raw %}
    &lt;ul&gt;
    {% for item in seq %}
        &lt;li&gt;{{ item }}&lt;/li&gt;
    {% endfor %}
    &lt;/ul&gt;
{% endraw %} </pre>
<div class="blog_h2"><span class="graybg">操作符</span></div>
<table class="full-width fixed-word-wrap" style="height: 416px;" width="1055">
<thead>
<tr>
<td style="width: 20%; text-align: center;">操作符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">+ - * /</td>
<td>加减乘除</td>
</tr>
<tr>
<td class="blog_h3">//</td>
<td>返回整数商</td>
</tr>
<tr>
<td class="blog_h3">%</td>
<td>取余数</td>
</tr>
<tr>
<td class="blog_h3">**</td>
<td>N次幂</td>
</tr>
<tr>
<td class="blog_h3">==   != &gt; &gt;= &lt; &lt;=</td>
<td>比较操作符</td>
</tr>
<tr>
<td class="blog_h3">and  or not</td>
<td>逻辑运算符</td>
</tr>
<tr>
<td class="blog_h3">in</td>
<td>元素是否在集合中判断：<tt class="docutils literal"><pre class="crayon-plain-tag"><span class="pre">{{</pre> <span class="pre">1</span> <span class="pre">in</span> <span class="pre">[1,2,3]</span> <span class="pre">}}</span></span></tt></td>
</tr>
<tr>
<td class="blog_h3">is</td>
<td>运行测试</td>
</tr>
<tr>
<td class="blog_h3">|</td>
<td>应用过滤器</td>
</tr>
<tr>
<td class="blog_h3">~</td>
<td>将操作数转换为字符串并连接：<tt class="docutils literal"><pre class="crayon-plain-tag"><span class="pre">{{</pre> <span class="pre">"Hello</span> <span class="pre">"</span> <span class="pre">~</span> <span class="pre">name</span> <span class="pre">~</span> <span class="pre">"!"</span> <span class="pre">}}</span></span></tt></td>
</tr>
<tr>
<td class="blog_h3">.   []</td>
<td>获取对象属性</td>
</tr>
<tr>
<td class="blog_h3">()</td>
<td>执行调用</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">字面值</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类型</td>
<td style="text-align: center;">示例</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">字符串</td>
<td><pre class="crayon-plain-tag">"Hello World"</pre></td>
</tr>
<tr>
<td class="blog_h3">数字</td>
<td><pre class="crayon-plain-tag">42</pre> / <pre class="crayon-plain-tag">42.23</pre> 分别是整数、浮点数</td>
</tr>
<tr>
<td class="blog_h3">列表</td>
<td><pre class="crayon-plain-tag">[ 'list',  'of', 'objects' ]</pre></td>
</tr>
<tr>
<td class="blog_h3">元组</td>
<td><pre class="crayon-plain-tag">( 'tuple', 'of', 'values' )</pre></td>
</tr>
<tr>
<td class="blog_h3">字典</td>
<td><pre class="crayon-plain-tag">{ 'dict': 'of', 'key': 'and', 'value': 'pairs' }</pre></td>
</tr>
<tr>
<td class="blog_h3">布尔</td>
<td><pre class="crayon-plain-tag">true</pre> / <pre class="crayon-plain-tag">false</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">变量</span></div>
<div class="blog_h3"><span class="graybg">属性访问</span></div>
<p>支持下标和点号两种导航语法：</p>
<pre class="crayon-plain-tag">{{ foo.bar }}
{{ foo['bar'] }}</pre>
<div class="blog_h3"><span class="graybg">过滤器</span></div>
<p>可以使用过滤器修改变量，过滤器和变量之间用管道符号 | 分隔，过滤器可以链式调用，前一个的输出，是后一个的输入：</p>
<pre class="crayon-plain-tag">{{ name|striptags|title }}</pre>
<p>过滤器支持参数，参数需要放在括号中： </p>
<pre class="crayon-plain-tag">{{ list|join(', ') }}</pre>
<div class="blog_h3"><span class="graybg">设置变量</span></div>
<pre class="crayon-plain-tag">{% set pipe = joiner("|") %} </pre>
<div class="blog_h2"><span class="graybg">空白控制</span></div>
<p>可以用 - 符号提示，需要将定界符左侧或右侧的空白移除：</p>
<pre class="crayon-plain-tag"># 移除右侧
{% for item in seq -%}
    {{ item }}
{%- endfor %}
# 移除左侧</pre>
<div class="blog_h2"><span class="graybg">分支判断</span></div>
<p> 要测试一个变量或表达式，你要在<span style="background-color: #c0c0c0;">变量后加上一个 is 以及测试的名称</span>：</p>
<pre class="crayon-plain-tag"># 测试变量是否定义
{% if name is defined %}

# 测试是否可以整除
{% if loop.index is divisibleby 3 %}
{% if loop.index is divisibleby(3) %}


# if-elif-else

{% if kenny.sick %}
    Kenny is sick.
{% elif kenny.dead %}
    You killed Kenny!  You bastard!!!
{% else %}
    Kenny looks okay --- so far
{% endif %}</pre>
<div class="blog_h2"><span class="graybg">循环控制</span></div>
<pre class="crayon-plain-tag"># 迭代列表
{% for user in users %}
  &lt;li&gt;{{ user.username|e }}&lt;/li&gt;
{% endfor %}

# 迭代字典
{% for key, value in my_dict.iteritems() %}
    &lt;dt&gt;{{ key|e }}&lt;/dt&gt;
    &lt;dd&gt;{{ value|e }}&lt;/dd&gt;
{% endfor %}</pre>
<div class="blog_h3"><span class="graybg">特殊变量</span></div>
<table class="docutils fixed-word-wrap full-width" border="1">
<colgroup>
<col width="31%" />
<col width="69%" /> </colgroup>
<thead valign="bottom">
<tr class="row-odd">
<td class="head" style="text-align: center;">变量</td>
<td class="head" style="text-align: center;">描述</td>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even">
<td>loop.index</td>
<td>当前循环迭代的次数（从 1 开始）</td>
</tr>
<tr class="row-odd">
<td>loop.index0</td>
<td>当前循环迭代的次数（从 0 开始）</td>
</tr>
<tr class="row-even">
<td>loop.revindex</td>
<td>到循环结束需要迭代的次数（从 1 开始）</td>
</tr>
<tr class="row-odd">
<td>loop.revindex0</td>
<td>到循环结束需要迭代的次数（从 0 开始）</td>
</tr>
<tr class="row-even">
<td>loop.first</td>
<td>如果是第一次迭代，为 True 。</td>
</tr>
<tr class="row-odd">
<td>loop.last</td>
<td>如果是最后一次迭代，为 True 。</td>
</tr>
<tr class="row-even">
<td>loop.length</td>
<td>序列中的项目数。</td>
</tr>
<tr class="row-odd">
<td>loop.cycle</td>
<td>在一串序列间期取值的辅助函数。见下面的解释。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">with作用域控制</span></div>
<p>用于新建一个作用域：</p>
<pre class="crayon-plain-tag">{% with %}
    {% set foo = 42 %}
    {{ foo }}           foo is 42 here
{% endwith %}
foo is not visible here any longer</pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">读取文件内容为变量</span></div>
<pre class="crayon-plain-tag">- name: Rendering init service job tempalte to local directory
  vars:
    download-script: "{{ lookup('file', 'files/download.sh') }}"
  template:
    src: "init.yaml.j2"
    dest: "{{ role_path }}/files/init-{{item.name}}.yaml"
  with_items: "{{ services }}"</pre>
<p>在上面的模板init.yaml.j2中，你可以直接引用变量download-script：</p>
<pre class="crayon-plain-tag">apiVersion: batch/v1
kind: Job
metadata:
  name: init-{{ item.name }}
spec:
  template:
    spec:
      containers:
      - command:
        - /bin/bash
        - -c
        - |
{{ download_script | indent( width=10, indentfirst=True) }}</pre>
<div class="blog_h2"><span class="graybg">模板中如何缩进 </span></div>
<p>使用indent函数，参考上面。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ansible-study-note">Ansible学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ansible-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
