<?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; C</title>
	<atom:link href="https://blog.gmem.cc/category/work/c/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +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-69d3dc89a3853385792390-i/]或[crayon-69d3dc89a385a099865248-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-69d3dc89a861f928972614-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-69d3dc89a8623139315590/] <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>Bazel学习笔记</title>
		<link>https://blog.gmem.cc/bazel-study-note</link>
		<comments>https://blog.gmem.cc/bazel-study-note#comments</comments>
		<pubDate>Mon, 07 Jan 2019 08:46:20 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24313</guid>
		<description><![CDATA[<p>简介 Bazel是Google开源的，类似于Make、Maven或Gradle的构建和测试工具。它使用可读性强的、高层次的构建语言，支持多种编程语言，以及为多种平台进行交叉编译。 Bazel的优势： 高层次的构建语言：更加简单，Bazel抽象出库、二进制、脚本、数据集等概念，不需要编写调用编译器或链接器的脚本 快而可靠：能够缓存所有已经完成的工作步骤，并且跟踪文件内容、构建命令的变动情况，避免重复构建。此外Bazel还支持高度并行构建、增量构建 多平台支持：可以在Linux/macOS/Windows上运行，可以构建在桌面/服务器/移动设备上运行的应用程序 可扩容性：处理10万以上源码文件时仍然能保持速度 可扩展性：支持Android、C/C++、Java、Objective-C、Protocol Buffer、Python…还支持扩展以支持其它语言 如何工作 当运行构建或者测试时，Bazel会： 加载和目标相关的BUILD文件 分析输入及其依赖，应用指定的构建规则，产生一个Action图。这个图表示需要构建的目标、目标之间的关系，以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动，并确定哪些目标需要重新构建 针对输入执行构建动作，直到最终的构建输出产生出来 如何使用 当你需要构建或者测试一个项目时，通常执行以下步骤： 下载并安装Bazel 创建一个工作空间。Bazel从此工作空间寻找构建输入和BUILD文件，同时也将构建输出存放在（指向）工作空间（的符号链接中） 编写BUILD文件，以及可选的WORKSPACE文件，告知Bazel需要构建什么，如何构建。此文件基于Starlark这种DSL 从命令行调用Bazel命令，构建、测试或者运行项目 概念和术语 Workspace <a class="read-more" href="https://blog.gmem.cc/bazel-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/bazel-study-note">Bazel学习笔记</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>Bazel是Google开源的，类似于Make、Maven或Gradle的构建和测试工具。它使用可读性强的、高层次的构建语言，支持多种编程语言，以及为多种平台进行交叉编译。</p>
<p>Bazel的优势：</p>
<ol>
<li>高层次的构建语言：更加简单，Bazel抽象出<span style="background-color: #c0c0c0;">库、二进制、脚本、数据集等</span>概念，不需要编写调用编译器或链接器的脚本</li>
<li>快而可靠：能够缓存所有已经完成的工作步骤，并且跟踪文件内容、构建命令的变动情况，避免重复构建。此外Bazel还支持高度<span style="background-color: #c0c0c0;">并行构建、增量构建</span></li>
<li>多平台支持：可以在Linux/macOS/Windows上运行，可以构建在桌面/服务器/移动设备上运行的应用程序</li>
<li>可扩容性：处理10万以上源码文件时仍然能保持速度</li>
<li>可扩展性：支持Android、C/C++、Java、Objective-C、Protocol Buffer、Python…还支持扩展以支持其它语言</li>
</ol>
<div class="blog_h2"><span class="graybg">如何工作</span></div>
<p>当运行构建或者测试时，Bazel会：</p>
<ol>
<li>加载和目标相关的<span style="background-color: #c0c0c0;">BUILD</span>文件</li>
<li>分析输入及其依赖，应用指定的<span style="background-color: #c0c0c0;">构建规则</span>，产生一个Action图。这个图表示需要构建的目标、目标之间的关系，以及为了构建目标需要执行的动作。Bazel依据此图来跟踪文件变动，并确定哪些目标需要重新构建</li>
<li>针对输入执行构建动作，直到最终的构建输出产生出来</li>
</ol>
<div class="blog_h2"><span class="graybg">如何使用</span></div>
<p>当你需要构建或者测试一个项目时，通常执行以下步骤：</p>
<ol>
<li>下载并安装Bazel</li>
<li>创建一个工作空间。Bazel从此工作空间寻找构建输入和BUILD文件，同时也将构建输出存放在（指向）工作空间（的符号链接中）</li>
<li>编写BUILD文件，以及可选的WORKSPACE文件，告知Bazel需要构建什么，如何构建。此文件基于<span style="background-color: #c0c0c0;">Starlark这种DSL</span></li>
<li>从命令行调用Bazel命令，构建、测试或者运行项目</li>
</ol>
<div class="blog_h2"><span class="graybg">概念和术语</span></div>
<div class="blog_h3"><span class="graybg">Workspace</span></div>
<p>工作空间是一个目录，它包含：</p>
<ol>
<li>构建目标所需要的源码文件，以及相应的BUILD文件</li>
<li>指向构建结果的符号链接</li>
<li>WORKSPACE文件，可以为空，可以包含对外部依赖的引用</li>
</ol>
<div class="blog_h3"><span class="graybg">Package</span></div>
<p>包是工作空间中主要的代码组织单元，其中包含一系列相关的文件（主要是代码）以及描述这些文件之间关系的BUILD文件</p>
<p>包是工作空间的子目录，它的<span style="background-color: #c0c0c0;">根目录必须包含文件BUILD.bazel或BUILD</span>。<span style="background-color: #c0c0c0;">除了那些具有BUILD文件的子目录——子包——</span>以外，其它子目录属于包的一部分</p>
<div class="blog_h3"><span class="graybg">Target</span></div>
<p>包是一个容器，它的元素定义在BUILD文件中，包括：</p>
<ol>
<li>规则（Rule），指定输入集和输出集之间的关系，声明从输入产生输出的必要步骤。<span style="background-color: #c0c0c0;">一个规则的输出可以是另外一个规则的输入</span></li>
<li>文件（File），可以分为两类：
<ol>
<li>源文件</li>
<li>自动生成的文件（Derived files），由构建工具依据规则生成</li>
</ol>
</li>
<li>包组：一组包，<span style="background-color: #c0c0c0;">包组用于限制特定规则的可见性</span>。包组由函数package_group定义，参数是包的列表和包组名称。你可以在规则的visibility属性中引用包组，声明那些包组可以引用当前包中的规则</li>
</ol>
<p>任何包生成的文件都属于当前包，不能为其它包生成文件。但是可以<span style="background-color: #c0c0c0;">从其它包中读取输入</span></p>
<div class="blog_h3"><span class="graybg">Label</span></div>
<p>引用一个目标时需要使用“标签”。标签的规范化表示：<pre class="crayon-plain-tag">@project//my/app/main:app_binary</pre>， 冒号前面是所属的包名，后面是目标名。如果不指定目标名，则默认以包路径最后一段作为目标名，例如：</p>
<pre class="crayon-plain-tag">//my/app
//my/app:app</pre>
<p>这两者是等价的。在BUILD文件中，引用当前包中目标时，包名部分可以省略，因此下面四种写法都可以等价：</p>
<pre class="crayon-plain-tag"># 当前包为my/app
//my/app:app
//my/app
:app
app</pre>
<p>在BUILD文件中，引用当前包中定义的<span style="background-color: #c0c0c0;">规则</span>时，<span style="background-color: #c0c0c0;">冒号不能省略</span>。引用当前包中<span style="background-color: #c0c0c0;">文件</span>时，<span style="background-color: #c0c0c0;">冒号可以省略</span>。 例如：<pre class="crayon-plain-tag">generate.cc</pre>。</p>
<p>但是，<span style="background-color: #c0c0c0;">从其它包引用时、从命令行引用时，都必须使用完整的标签</span>：<pre class="crayon-plain-tag">//my/app:generate.cc</pre></p>
<p>@project这一部分通常不需要使用，引用外部存储库中的目标时，project填写外部存储库的名字。</p>
<div class="blog_h3"><span class="graybg">Rule</span></div>
<p>规则指定<span style="background-color: #c0c0c0;">输入和输出之间的关系</span>，并且说明<span style="background-color: #c0c0c0;">产生输出的步骤</span>。</p>
<p>规则有很多类型。每个规则都具有一个名称属性，<span style="background-color: #c0c0c0;">此名称亦即目标名称</span>。对于某些规则，此名称就是产生的输出的文件名。</p>
<p>在BUILD中声明规则的语法时：</p>
<pre class="crayon-plain-tag">规则类型(
    name = "...",
    其它属性 = ...
)</pre>
<div class="blog_h3"><span class="graybg">BUILD文件</span></div>
<p>BUILD文件定义了包的所有元数据。其中的语句被<span style="background-color: #c0c0c0;">从上而下的逐条解释</span>，某些语句的顺序很重要， 例如<span style="background-color: #c0c0c0;">变量必须先定义</span>后使用，但是<span style="background-color: #c0c0c0;">规则声明的顺序无所谓</span>。</p>
<p>BUILD文件仅能包含ASCII字符，且不得声明函数、使用for/if语句，你可以在Bazel扩展——扩展名为.bzl的文件中声明函数、控制结构。并在BUILD文件中用load语句加载Bazel扩展：</p>
<pre class="crayon-plain-tag">load("//foo/bar:file.bzl", "some_library")</pre>
<p>上面的语句加载foo/bar/file.bzl并添加其中定义的符号some_libraray到当前环境中，load语句可以用来加载规则、函数、常量（字符串、列表等）。</p>
<p><span style="background-color: #c0c0c0;">load语句必须出现在顶级作用域</span>，不能出现在函数中。第一个参数说明扩展的位置，你可以为导入的符号设置别名。</p>
<p>规则的类型，一般以编程语言为前缀，例如cc，java，后缀通常有：</p>
<ol>
<li>*_binary 用于构建目标语言的可执行文件</li>
<li>*_test 用于自动化测试，其目标是可执行文件，如果测试通过应该退出0</li>
<li>*_library 用于构建目标语言的库 </li>
</ol>
<div class="blog_h3"><span class="graybg">Dependency</span></div>
<p>目标A依赖B，就意味着A在<span style="background-color: #c0c0c0;">构建或执行期间需要B</span>。所有目标的依赖关系构成非环有向图（DAG）称为依赖图。</p>
<p>距离为1的依赖称为直接依赖，大于1的依赖则称为传递性依赖。</p>
<p>依赖分为以下几种：</p>
<ol>
<li>srcs依赖：直接被当前规则消费的文件</li>
<li>deps依赖：独立编译的模块，为当前规则提供头文件、符号、库、数据</li>
<li>data依赖：不属于源码，不影响目标如何构建，但是目标在运行时可能依赖之</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Bazel</span></div>
<div class="blog_h3"><span class="graybg">Ubuntu</span></div>
<p>参考下面的步骤安装Bazel：</p>
<pre class="crayon-plain-tag">echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -

sudo apt-get update &amp;&amp; sudo apt-get install bazel</pre>
<p>可以用如下命令升级到最新版本的Bazel：</p>
<pre class="crayon-plain-tag">sudo apt-get install --only-upgrade bazel</pre>
<div class="blog_h2"><span class="graybg">Bazelisk</span></div>
<p>这是基于Go语言编写的Bazel启动器，它会为你的工作区下载最适合的Bazel，并且透明的将命令转发给该Bazel。</p>
<p>由于Bazellisk提供了和Bazel一样的接口，因此通常直接将其命名为bazel：</p>
<pre class="crayon-plain-tag">sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/v0.0.8/bazelisk-linux-amd64
sudo chmod +x /usr/local/bin/bazel </pre>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg">构建C++项目</span></div>
<div class="blog_h3"><span class="graybg">示例项目</span></div>
<p>执行下面的命令下载示例项目：</p>
<pre class="crayon-plain-tag">git clone https://github.com/bazelbuild/examples/</pre>
<p>你可以看到stage1、stage2、stage3这几个WORKSPACE：</p>
<pre class="crayon-plain-tag">examples
└── cpp-tutorial
    ├──stage1
    │  ├── main
    │  │   ├── BUILD
    │  │   └── hello-world.cc
    │  └── WORKSPACE
    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── WORKSPACE
    └──stage3
       ├── main
       │   ├── BUILD
       │   ├── hello-world.cc
       │   ├── hello-greet.cc
       │   └── hello-greet.h
       ├── lib
       │   ├── BUILD
       │   ├── hello-time.cc
       │   └── hello-time.h
       └── WORKSPACE</pre>
<p>本节后续内容会依次使用到这三个WORKSPACE。</p>
<div class="blog_h3"><span class="graybg">通过Bazel构建</span></div>
<p>第一步是创建工作空间。工作空间中包含以下特殊文件：</p>
<ol>
<li>WORKSPACE，此文件位于根目录中，将当前目录<span style="background-color: #c0c0c0;">定义为Bazel工作空间</span></li>
<li>BUILD，告诉Bazel项目的不同部分如何构建。工作空间中<span style="background-color: #c0c0c0;">包含BUILD文件的目录称为包</span></li>
</ol>
<p>当Bazel构建项目时，<span style="background-color: #c0c0c0;">所有的输入和依赖都必须位于工作空间中</span>。除非被链接，不同工作空间的文件相互独立没有关系。</p>
<p>每个BUILD文件包含若干Bazel指令，其中最重要的指令类型是构建<span style="background-color: #c0c0c0;">规则（Build Rule）</span>，构建规则说明如何产生期望的输出——例如可执行文件或库。<span style="background-color: #c0c0c0;"> BUILD中的每个构建规则也称为目标（Target）</span>，<span style="background-color: #c0c0c0;">目标指向若干源文件和依赖，也可以指向其它目标</span>。</p>
<p>下面是stage1的BUILD文件：</p>
<pre class="crayon-plain-tag">cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)</pre>
<p>这里定义了一个名为hello-world的目标，它使用了内置的cc_binary规则。该规则告诉Bazel，从源码hello-world.cc构建一个自包含的可执行文件。</p>
<p>执行下面的命令可以触发构建：</p>
<pre class="crayon-plain-tag">#   //main: BUILD文件相对于工作空间的位置
#          hello-world 是BUILD文件中定义的目标
bazel build //main:hello-world</pre>
<p>构建完成后，工作空间根目录会出现bazel-bin等目录，它们都是指向$HOME/.cache/bazel某个后代目录的符号链接。执行：</p>
<pre class="crayon-plain-tag">bazel-bin/main/hello-world</pre>
<p>可以运行构建好的二进制文件。</p>
<div class="blog_h3"><span class="graybg">查看依赖图</span></div>
<p>Bazel会根据BUILD中的声明产生一张依赖图，并根据这个依赖图实现精确的增量构建。</p>
<p>要查看依赖图，先安装：</p>
<pre class="crayon-plain-tag">sudo apt install graphviz xdot</pre>
<p>然后执行：</p>
<pre class="crayon-plain-tag">bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph | xdot</pre>
<div class="blog_h3"><span class="graybg">指定多个目标 </span></div>
<p>大型项目通常会划分为多个包、多个目标，以实现更快的增量构建、并行构建。工作空间stage2包含单个包、两个目标：</p>
<pre class="crayon-plain-tag"># 首先构建hello-greet库，cc_library是内建规则
cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    # 头文件
    hdrs = ["hello-greet.h"],
)

# 然后构建hello-world二进制文件
cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        # 提示Bazel，需要hello-greet才能构建当前目标
        # 依赖当前包中的hello-greet目标
        ":hello-greet",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">使用多个包</span></div>
<p>工作空间stage3更进一步的划分出新的包，提供打印时间的功能：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    # 让当前目标对于工作空间的main包可见。默认情况下目标仅仅被当前包可见
    visibility = ["//main:__pkg__"],
)</pre><br />
<pre class="crayon-plain-tag">cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        # 依赖当前包中的hello-greet目标
        ":hello-greet",
        # 依赖工作空间根目录下的lib包中的hello-time目标
        "//lib:hello-time",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">如何引用目标</span></div>
<p>在BUILD文件或者命令行中，你都使用标签（Label）来引用目标，其语法为：</p>
<pre class="crayon-plain-tag">//path/to/package:target-name

# 当引用当前包中的其它目标时，可以：
//:target-name
# 当引用当前BUILD文件中其它目标时，可以：
:target-name</pre>
<div class="blog_h1"><span class="graybg">目录布局</span></div>
<pre class="crayon-plain-tag">workspace-name&gt;/                          # 工作空间根目录
  bazel-my-project =&gt; &lt;...my-project&gt;     # execRoot的符号链接，所有构建动作在此目录下执行
  bazel-out =&gt; &lt;...bin&gt;                   # outputPath的符号链接
  bazel-bin =&gt; &lt;...bin&gt;                   # 最近一次写入的二进制目录的符号链接，即$(BINDIR)
  bazel-genfiles =&gt; &lt;...genfiles&gt;         # 最近一次写入的genfiles目录的符号链接，即$(GENDIR)



/home/user/.cache/bazel/                  # outputRoot，所有工作空间的Bazel输出的根目录
  _bazel_$USER/                           # outputUserRoot，当前用户的Bazel输出的根目录
    install/
      fba9a2c87ee9589d72889caf082f1029/   # installBase，Bazel安装清单的哈希值
        _embedded_binaries/               # 第一次运行时从Bazel可执行文件的数据段解开的可执行文件或脚本
    7ffd56a6e4cb724ea575aba15733d113/     # outputBase，某个工作空间根目录的哈希值
      action_cache/                       # Action cache目录层次
      action_outs/                        # Action output目录
      command.log                         # 最近一次Bazel命令的stdout/stderr输出
      external/                           # 远程存储库被下载、链接到此目录
      server/                             # Bazel服务器将所有服务器有关的文件存放在此
        jvm.out                           # Bazel服务器的调试输出
      execroot/                           # 所有Bazel Action的工作目录
        &lt;workspace-name&gt;/                 # Bazel构建的工作树
          _bin/                           # 助手工具链接或者拷贝到此
          bazel-out/                      # outputPath，构建的实际输出目录
            local_linux-fastbuild/        # 每个独特的BuildConfiguration实例对应一个子目录
              bin/                        # 单个构建配置二进制输出目录，$(BINDIR)
                foo/bar/_objs/baz/        # 命名为//foo/bar:baz的cc_*规则的Object文件所在目录
                  foo/bar/baz1.o          # //foo/bar:baz1.cc对应的Object文件
                  other_package/other.o   # //other_package:other.cc对应的Object文件
                foo/bar/baz               # //foo/bar:baz这一cc_binary生成的构件
                foo/bar/baz.runfiles/     # //foo/bar:baz生成的二进制构件的runfiles目录
                  MANIFEST
                  &lt;workspace-name&gt;/
                    ...
              genfiles/                   # 单个构建配置生成的源文件目录，$(GENDIR)
              testlogs/                   # Bazel的内部测试运行器将日志文件存放在此
              include/                    # 按需生成的include符号链接树，符号链接bazel-include指向这里
            host/                         # 本机的BuildConfiguration
        &lt;packages&gt;/                       # 构建引用的包，对于此包来说，它就像一个正常的WORKSPACE </pre>
<div class="blog_h1"><span class="graybg">Starlark</span></div>
<p>Bazel配置文件使用Starlark（原先叫Skylark）语言，具有短小、简单、线程安全的特点。</p>
<p>这种语言的语法和Python很类似，Starlark是Python2/Python3的子集。不支持的Python特性包括：</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>隐含字符串连接</td>
<td>需要明确使用 + 操作符</td>
</tr>
<tr>
<td>链式比较操作符</td>
<td>例如：1 &lt; x &lt; 5</td>
</tr>
<tr>
<td>class</td>
<td>使用struct函数</td>
</tr>
<tr>
<td>import</td>
<td>使用load语句</td>
</tr>
<tr>
<td>is</td>
<td>使用==代替</td>
</tr>
<tr>
<td colspan="2">以下关键字：while、yield、try、raise、except、finally 、global、nonlocal</td>
</tr>
<tr>
<td colspan="2">以下数据类型：float、set</td>
</tr>
<tr>
<td colspan="2">生成器、生成器表达式</td>
</tr>
<tr>
<td colspan="2">lambda以及嵌套函数</td>
</tr>
<tr>
<td colspan="2">绝大多数内置函数、方法</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<p>Starlark支持的数据类型包括：None、bool、dict、function、int、list、string，以及两种Bazel特有的类型：depset、struct。</p>
<div class="blog_h2"><span class="graybg">代码示例</span></div>
<pre class="crayon-plain-tag"># 定义一个数字
number = 18

# 定义一个字典
people = {
    "Alice": 22,
    "Bob": 40,
    "Charlie": 55,
    "Dave": 14,
}

names = ", ".join(people.keys())

# 定义一个函数
def greet(name):
  """Return a greeting."""
  return "Hello {}!".format(name)
# 调用函数
greeting = greet(names)


def fizz_buzz(n):
  """Print Fizz Buzz numbers from 1 to n."""
  # 循环结构
  for i in range(1, n + 1):
    s = ""
    # 分支结构
    if i % 3 == 0:
      s += "Fizz"
    if i % 5 == 0:
      s += "Buzz"
    print(s if s else i)</pre>
<div class="blog_h1"><span class="graybg">变量</span></div>
<p>你可以在BUILD文件中声明和使用变量。使用变量可以减少重复的代码：</p>
<pre class="crayon-plain-tag">COPTS = ["-DVERSION=5"]

cc_library(
  name = "foo",
  copts = COPTS,
  srcs = ["foo.cc"],
)

cc_library(
  name = "bar",
  copts = COPTS,
  srcs = ["bar.cc"],
  deps = [":foo"],
)</pre>
<div class="blog_h2"><span class="graybg">跨BUILD变量</span></div>
<p>如果要声明跨越多个BUILD文件共享的变量，<span style="background-color: #c0c0c0;">必须把变量放入.bzl文件中</span>，然后通过load加载bzl文件。</p>
<div class="blog_h2"><span class="graybg">Make变量</span></div>
<p>所谓Make变量，是一类特殊的、可展开的字符串变量，这种变量类似Shell中变量替换那样的展开。</p>
<p>Bazel提供了：</p>
<ol>
<li>预定义变量，可以在任何规则中使用</li>
<li>自定义变量，在规则中定义。仅仅在依赖该规则的那些规则中，可以使用这些变量</li>
</ol>
<div class="blog_h3"><span class="graybg">使用Make变量</span></div>
<p>仅仅那些标记为Subject to 'Make variable' substitution的规则属性，才可以使用Make变量。例如：</p>
<pre class="crayon-plain-tag"># 使用Make变量FOO
my_attr = "prefix $(FOO) suffix"
# 如果变量FOO的值为bar，则实际my_attr的值为prefix bar suffix</pre>
<p>如果要使用$字符，需要用<pre class="crayon-plain-tag">$$</pre>代替。 </p>
<div class="blog_h3"><span class="graybg">一般预定义变量</span></div>
<p>执行命令：<pre class="crayon-plain-tag">bazel info --show_make_env [build options]</pre>可以查看所有预定义变量的列表。</p>
<p>任何规则可以使用以下变量：</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>COMPILATION_MODE</td>
<td>编译模式：fastbuild、dbg、opt</td>
</tr>
<tr>
<td>BINDIR</td>
<td>目标体系结构的二进制树的根目录</td>
</tr>
<tr>
<td>GENDIR</td>
<td>目标体系结构的生成代码树的根目录</td>
</tr>
<tr>
<td>TARGET_CPU</td>
<td>目标体系结构的CPU</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">genrule预定义变量</span></div>
<p>下表中的变量可以在genrule规则的cmd属性中使用：</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>OUTS</td>
<td>genrule的outs列表，如果只有一个输出文件，可以用<pre class="crayon-plain-tag">$@</pre></td>
</tr>
<tr>
<td>SRCS</td>
<td>genrule的srcs列表，如果只有一个输入文件，可以用<pre class="crayon-plain-tag">$&lt;</pre></td>
</tr>
<tr>
<td>@D</td>
<td>
<p>输出目录，如果：</p>
<ol>
<li>outs仅仅包含一个文件名，则展开为包含该文件的目录</li>
<li>outs包含多个文件，则此变量展开为在genfiles树中，当前包的根目录</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">输入输出路径变量</span></div>
<p>下表中的变量以Bazel的Label为参数，获取包的某类输入/输出路径：</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>execpath</td>
<td rowspan="2">
<p>获取指定标签对应的规则（此规则必须仅仅输出单个文件）或文件（必须是单个文件），位于execroot下的对应路径</p>
<p>对于项目myproject，<span style="background-color: #c0c0c0;">所有构建动作</span>在工作空间根目录下的符号链接bazel-myproject对应的目录下执行，此目录即execroot。源码empty.source被链接到bazel-myproject/testapp/empty.source，因此其execpath为testapp/empty.source</p>
<p>对于目标：</p>
<pre class="crayon-plain-tag">cc_binary(
  name = "app",
  srcs = ["app.cc"]
)</pre>
<p>执行构建：<pre class="crayon-plain-tag">bazel build //testapp:app</pre>时： </p>
<pre class="crayon-plain-tag">$(execpath :app)  # bazel-out/host/bin/testapp/app
$(execpath empty.source) # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>execpaths</td>
</tr>
<tr>
<td>rootpath</td>
<td rowspan="2">
<p>获取runfiles路径，二进制文件通过此路径在运行时寻找其依赖
<p>对于上面的//testapp:app目标：</p>
<pre class="crayon-plain-tag">$(rootpath :app)  # testapp/app
$(rootpath empty.source)  # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>rootpaths</td>
</tr>
<tr>
<td>location</td>
<td rowspan="2">
<p>根据当前所声明的属性，等价于execpath或rootpath
<p>对于上面的//testapp:app目标：</p>
<pre class="crayon-plain-tag">$(location :app) # bazel-out/host/bin/testapp/app
$(location empty.source) # testapp/empty.source </pre>
</td>
</tr>
<tr>
<td>locations</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">filegroup</span></div>
<p>为一组目标指定一个名字，你可以从其它规则中方便的引用这组目标。
<p>Bazel鼓励使用filegroup，而不是直接引用目录。Bazel构建系统不能完全了解目录中文件的变化情况，因而文件发生变化时，可能不会进行重新构建。而使用filegroup，即使联用glob，目录中所有文件仍然能够被构建系统正确的监控。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">filegroup(
    name = "exported_testdata",
    srcs = glob([
        "testdata/*.dat",
        "testdata/logs/**/*.log",
    ]),
)</pre>
<p>要引用filegroup，只需要使用标签：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "my_library",
    srcs = ["foo.cc"],
    data = [
        "//my_package:exported_testdata",
        "//my_package:mygroup",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">test_suite</span></div>
<p>定义一组测试用例，给出一个有意义的名称，便于在特定时机  —— 例如迁入代码、执行压力测试 —— 时执行这些测试用例。</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 匹配当前包中所有small测试
test_suite(
    name = "small_tests",
    tags = ["small"],
)
# 匹配不包含flaky标记的测试
test_suite(
    name = "non_flaky_test",
    tags = ["-flaky"],
)
# 指定测试列表
test_suite(
    name = "smoke_tests",
    tests = [
        "system_unittest",
        "public_api_unittest",
    ],
)</pre>
<div class="blog_h3"><span class="graybg">alias</span></div>
<p>为规则设置一个别名：</p>
<pre class="crayon-plain-tag">filegroup(
    name = "data",
    srcs = ["data.txt"],
)
# 定义别名
alias(
    name = "other",
    actual = ":data",
)</pre>
<div class="blog_h3"><span class="graybg">config_setting</span></div>
<p>通过匹配<span style="background-color: #c0c0c0;">以Bazel标记或平台约束来表达的“配置状态”</span>，config_setting能够触发可配置的属性。</p>
<p>下面这个例子，匹配针对ARM平台的构建：</p>
<pre class="crayon-plain-tag">config_setting(
    name = "arm_build",
    values = {"cpu": "arm"},
)</pre>
<p>下面的例子，匹配任何定义了宏FOO=bar的针对X86平台的调试（-c dbg）构建：</p>
<pre class="crayon-plain-tag">config_setting(
    name = "x86_debug_build",
    values = {
        "cpu": "x86",
        "compilation_mode": "dbg",
        "define": "FOO=bar"
    },
)</pre>
<p>下面的库，通过select来声明可配置属性：</p>
<pre class="crayon-plain-tag">cc_binary(
    name = "mybinary",
    srcs = ["main.cc"],
    deps = select({
        # 如果config_settings arm_build匹配正在进行的构建，则依赖arm_lib这个目标
        ":arm_build": [":arm_lib"],
        # 如果config_settings x86_debug_build匹配正在进行的构建，则依赖x86_devdbg_lib
        ":x86_debug_build": [":x86_devdbg_lib"],
        # 默认情况下，依赖generic_lib
        "//conditions:default": [":generic_lib"],
    }),
) </pre>
<div class="blog_h3"><span class="graybg">genrule</span></div>
<p>一般性的规则 —— 使用用户指定的Bash命令，生成一个或多个文件。使用genrule理论上可以实现任何构建行为，例如压缩JavaScript代码。但是在执行C++、Java等构建任务时，最好使用相应的专用规则，更加简单。</p>
<p>不要使用genrule来运行测试，如果需要一般性的测试规则，可以考虑使用sh_test。</p>
<p>genrule在一个Bash shell环境下执行，当任意一个命令或管道失败（set -e -o pipefail），整个规则就失败。你不应该在genrule中访问网络。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">genrule(
    name = "foo",
    # 不需要输入
    srcs = [],
    # 生成一个foo.h
    outs = ["foo.h"],
    # 运行当前规则所在包下的一个Perl脚本
    cmd = "./$(location create_foo.pl) &gt; \"$@\"",
    tools = ["create_foo.pl"],
) </pre>
<div class="blog_h1"><span class="graybg">C++规则</span></div>
<div class="blog_h2"><span class="graybg">规则列表</span></div>
<div class="blog_h3"><span class="graybg">cc_binary</span></div>
<p>隐含输出：</p>
<ol>
<li>name.stripped，仅仅当显式要求才会构建此输出，针对生成的二进制文件运行strip -g以驱除debug符号。额外的strip选项可以通过命令行--stripopt=-foo传入</li>
<li>name.dwp，仅仅当显式要求才会构建此输出，如果启用了 Fission ，则此文件包含用于远程调试的调试信息，否则是空文件</li>
</ol>
<p>属性列表：</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>name</td>
<td>目标的名称</td>
</tr>
<tr>
<td>deps</td>
<td>
<p>需要链接到此二进制目标的其它库的列表，以Label引用</p>
<p>这些库可以是cc_library或objc_library定义的目标</p>
</td>
</tr>
<tr>
<td>srcs</td>
<td>
<p>C/C++源文件列表，以Label引用</p>
<p>这些文件是C/C++源码文件或头文件，可以是自动生成的或人工编写的。</p>
<p>所有cc/c/cpp文件都会被编译。<span style="background-color: #c0c0c0;">如果某个声明的文件在其它规则的outs列表中，则当前规则自动依赖于那个规则</span></p>
<p>所有.h文件都不会被编译，仅仅供源码文件包含之。所有.h/.cc等文件都可以包含<span style="background-color: #c0c0c0;">srcs中声明的</span>、<span style="background-color: #c0c0c0;">deps中声明的目标的hdrs中声明</span>的头文件。也就是说，任何#include的文件要么在此属性中声明，要么在依赖的cc_library的hdrs属性中声明</p>
<p>如果某个规则的名称出现在srcs列表中，则当前规则自动依赖于那个规则：</p>
<ol>
<li>如果那个规则的输出是C/C++源文件，则它们被编译进当前目标</li>
<li>如果那个规则的输出是库文件，则被链接到当前目标</li>
</ol>
<p>允许的文件类型：</p>
<ol>
<li>C/C++源码，扩展名.c, .cc, .cpp, .cxx, .c++, .C</li>
<li>C/C++头文件，扩展名.h, .hh, .hpp, .hxx, .inc</li>
<li>汇编代码，扩展名.S</li>
<li>归档文件，扩展名.a, .pic.a</li>
<li>共享库，扩展名.so, .so.version，version为soname版本号</li>
<li>对象文件，扩展名.o, .pic.o</li>
<li>任何能够产生上述文件的规则</li>
</ol>
</td>
</tr>
<tr>
<td>copts</td>
<td>
<p>字符串列表</p>
<p>为C++编译器提供的选项，在编译目标之前，这些选项按顺序添加到COPTS。这些选项仅仅影响当前目标的编译，而<span style="background-color: #c0c0c0;">不影响其依赖</span>。选项中的任何路径都<span style="background-color: #c0c0c0;">相对于当前工作空间而非当前包</span></p>
<p>也可以在bazel build时通过--copts选项传入，例如：</p>
<pre class="crayon-plain-tag">--copt "-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" </pre>
</td>
</tr>
<tr>
<td>defines</td>
<td>
<p>字符串列表
<p>为C++编译器传递宏定义，实际上会前缀以-D并添加到COPTS。与copts属性不同，这些宏定义会添加到当前目标，以及<span style="background-color: #c0c0c0;">所有依赖它的目标</span></p>
</td>
</tr>
<tr>
<td>includes</td>
<td>
<p>字符串列表</p>
<p>为C++编译器传递的头文件包含目录，实际上会前缀以-isystem并添加到COPTS。与copts属性不同，这些头文件包含会影响当前目标，以及<span style="background-color: #c0c0c0;">所有依赖它的目标</span></p>
<p>如果不清楚有何副作用，可以<span style="background-color: #c0c0c0;">传递-I到copts</span>，而不是使用当前属性</p>
</td>
</tr>
<tr>
<td>linkopts </td>
<td>
<p>字符串列表</p>
<p>为C++链接器传递选项，在链接二进制文件之前，此属性中的每个字符串被添加到LINKOPTS</p>
<p>此属性列表中，任何不以$和-开头的项，都被认为是deps中声明的<span style="background-color: #c0c0c0;">某个目标的Label，目标产生的文件会添加到链接选项</span>中</p>
</td>
</tr>
<tr>
<td>linkshared</td>
<td>
<p>布尔，默认False。用于创建共享库</p>
<p>要创建共享库，指定属性linkshared = True，对于GCC来说，会添加选项-shared。生成的结果适合被Java这类应用程序加载</p>
<p>需要注意，这里创建的共享库<span style="background-color: #c0c0c0;">绝不会被链接到依赖它的二进制文件</span>，而只适用于被其它程序手工的加载。因此，不能代替cc_library</p>
<p>如果同时指定<pre class="crayon-plain-tag">linkopts=['-static']</pre>和linkshared=True，你会得到一个完全自包含的单元。如果同时指定linkstatic=True和linkshared=True会得到一个基本是完全自包含的单元</p>
</td>
</tr>
<tr>
<td>linkstatic</td>
<td>
<p>布尔，默认True</p>
<p>对于cc_binary和cc_test，以静态形式链接二进制文件。对于cc_binary此选项默认True，其它目标默认False</p>
<p>如果当前目标是binary或test，此选项提示构建工具，<span style="background-color: #c0c0c0;">尽可能链接到用户库的.a版本而非.so</span>版本。某些系统库可能仍然需要动态链接，原因是没有静态库，这导致最终的输出仍然使用动态链接，不是完全静态的</p>
<p>链接一个可执行文件时，实际上有三种方式：</p>
<ol>
<li>STATIC，使用完全静态链接特性。所有依赖都被静态链接，GCC命令示例：<br />
<pre class="crayon-plain-tag">gcc -static foo.o libbar.a libbaz.a -lm</pre>
</li>
<li>STATIC，所有用户库静态链接（如果存在静态库版本），但是系统库（除去C/C++运行时库）动态链接，GCC命令示例：<br />
<pre class="crayon-plain-tag"># 此方式可以由linkstatic=True 启用
gcc foo.o libfoo.a libbaz.a -lm </pre>
</li>
<li>DYNAMIC，所有依赖被动态链接（如果存在动态库版本），GCC命令示例：<br />
<pre class="crayon-plain-tag"># 此方式可以由linkstatic=False 启用
gcc foo.o libfoo.so libbaz.so -lm </pre>
</li>
</ol>
<p>对于cc_library来说，linkstatic属性的含义不同。对于C++库来说：</p>
<ol>
<li>linkstatic=True表示仅仅允许静态链接，也就是不产生.so文件</li>
<li>linkstatic=False表示允许动态链接，同时产生.a和.so文件</li>
</ol>
</td>
</tr>
<tr>
<td>malloc </td>
<td>
<p>指向标签，默认@bazel_tools//tools/cpp:malloc</p>
<p>覆盖默认的malloc依赖，默认情况下C++二进制文件链接到//tools/cpp:malloc，这是一个空库，这导致实际上链接到libc的malloc</p>
</td>
</tr>
<tr>
<td>nocopts</td>
<td>
<p>字符串</p>
<p>从C++编译命令中移除匹配的选项，此属性的值是正则式，任何匹配正则式的、已经存在的COPTS被移除 </p>
</td>
</tr>
<tr>
<td>stamp </td>
<td>
<p>整数，默认-1</p>
<p>用于将构建信息嵌入到二进制文件中，可选值：</p>
<ol>
<li>stamp = 1，将构建信息嵌入，目标二进制仅仅在其依赖变化时重新构建</li>
<li>stamp = 0，总是将构建信息替换为常量值，有利于构建结果缓存</li>
<li>stamp = -1 ，由--[no]stamp标记控制是否嵌入</li>
</ol>
</td>
</tr>
<tr>
<td>toolchains </td>
<td>
<p>标签列表</p>
<p>提供构建变量（Make variables，这些变量可以被当前目标使用）的工具链的标签列表 </p>
</td>
</tr>
<tr>
<td>win_def_file</td>
<td>
<p>标签</p>
<p>传递给链接器的Windows DEF文件。在Windows上，此属性可以在链接共享库时导出符号 </p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">cc_import</span></div>
<p>导入预编译好的C/C++库。</p>
<p>属性列表：</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>hdrs</td>
<td>此预编译库对外发布的头文件列表，依赖此库的规则（dependent rule）会直接将这些头文件包含在源码列表中</td>
</tr>
<tr>
<td>alwayslink</td>
<td>
<p>布尔，默认False</p>
<p>如果为True，则依赖此库的二进制文件会将此静态库归档中的对象文件链接进去，就算某些对象文件中的符号并没有被二进制文件使用</p>
</td>
</tr>
<tr>
<td>interface_library</td>
<td>用于链接共享库时使用的接口（导入）库</td>
</tr>
<tr>
<td>shared_library</td>
<td>共享库，Bazel保证在运行时可以访问到共享库</td>
</tr>
<tr>
<td>static_library</td>
<td>静态库</td>
</tr>
<tr>
<td>system_provided</td>
<td>提示运行时所需的共享库由操作系统提供，如果为True则应该指定interface_library，shared_library应该为空</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">cc_library</span></div>
<p>对于所有cc_*规则来说，构建所需的任何头文件都要在hdrs或srcs中声明。</p>
<p>对于cc_library规则，在hdrs声明的头文件构成库的公共接口。这些头文件可以被当前库的hdrs/srcs中的文件直接包含，也可以被依赖（deps）当前库的其它cc_*的hdrs/srcs直接包含。<span style="background-color: #c0c0c0;">位于srcs中的头文件，则仅仅能被当前库的</span>hdrs/srcs包含。</p>
<p>cc_binary和cc_test不会暴露接口，因此它们没有hdrs属性。</p>
<p>属性列表：</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>name</td>
<td>库的名称</td>
</tr>
<tr>
<td>deps</td>
<td>需要链接到（into）当前库的其它库</td>
</tr>
<tr>
<td>srcs</td>
<td>头文件和源码列表</td>
</tr>
<tr>
<td>hdrs</td>
<td>导出的头文件列表</td>
</tr>
<tr>
<td>copts/nocopts</td>
<td>传递给C++编译命令的参数</td>
</tr>
<tr>
<td>defines</td>
<td>宏定义列表</td>
</tr>
<tr>
<td>include_prefix</td>
<td>hdrs中头文件的路径前缀</td>
</tr>
<tr>
<td>includes</td>
<td>
<p>字符串列表</p>
<p>需要添加到编译命令的包含文件列表</p>
</td>
</tr>
<tr>
<td>linkopts</td>
<td>链接选项</td>
</tr>
<tr>
<td>linkstatic</td>
<td>是否生成动态库</td>
</tr>
<tr>
<td>strip_include_prefix</td>
<td>
<p>字符串</p>
<p>需要脱去的头文件路径前缀，也就是说使用hdrs中头文件时，要把这个前缀去除，路径才匹配</p>
</td>
</tr>
<tr>
<td>textual_hdrs</td>
<td>
<p>标签列表</p>
<p>头文件列表，这些头文件是不能独立编译的。依赖此库的目标，直接以文本形式包含这些头文件到它的源码列表中，这样才能正确编译这些头文件</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">常见用例</span></div>
<div class="blog_h3"><span class="graybg">通配符</span></div>
<p>可以使用Glob语法为目标添加多个文件：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "build-all-the-files",
    srcs = glob(["*.cc"]),
    hdrs = glob(["*.h"]),
)</pre>
<div class="blog_h3"><span class="graybg">传递性依赖</span></div>
<p>如果源码依赖于某个头文件，则该源码的规则需要dep头文件的库，仅仅直接依赖才需要声明：</p>
<pre class="crayon-plain-tag"># 三明治依赖面包
cc_library(
    name = "sandwich",
    srcs = ["sandwich.cc"],
    hdrs = ["sandwich.h"],
    # 声明当前包下的目标为依赖
    deps = [":bread"],
)
# 面包依赖于面粉，三明治间接依赖面粉，因此不需要声明
cc_library(
    name = "bread",
    srcs = ["bread.cc"],
    hdrs = ["bread.h"],
    deps = [":flour"],
)

cc_library(
    name = "flour",
    srcs = ["flour.cc"],
    hdrs = ["flour.h"],
)</pre>
<div class="blog_h3"><span class="graybg">添加头文件路径 </span></div>
<p>有些时候你不愿或不能将头文件放到工作空间的include目录下，现有的库的include目录可能不符合</p>
<div class="blog_h3"><span class="graybg">导入已编译库</span></div>
<p>导入一个库，用于静态链接：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  static_library = "libmylib.a",
  # 如果为1则libmylib.a总会链接到依赖它的二进制文件
  alwayslink = 1,
)</pre>
<p>导入一个库，用于共享链接（UNIX）： </p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  shared_library = "libmylib.so",
)</pre>
<p>通过接口库（Interface library）链接到共享库（Windows）：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  # mylib.lib是mylib.dll的导入库，此导入库会传递给链接器
  interface_library = "mylib.lib",
  # mylib.dll在运行时需要，链接时不需要
  shared_library = "mylib.dll",
)</pre>
<p>在二进制目标中选择链接到共享库还是静态库（UNIX）：</p>
<pre class="crayon-plain-tag">cc_import(
  name = "mylib",
  hdrs = ["mylib.h"],
  # 同时声明共享库和静态库
  static_library = "libmylib.a",
  shared_library = "libmylib.so",
)

# 此二进制目标链接到静态库
cc_binary(
  name = "first",
  srcs = ["first.cc"],
  deps = [":mylib"],
  linkstatic = 1, # default value
)

# 此二进制目标链接到共享库
cc_binary(
  name = "second",
  srcs = ["second.cc"],
  deps = [":mylib"],
  linkstatic = 0,
)</pre>
<div class="blog_h3"><span class="graybg">包含外部库</span></div>
<p>你可以在WORKSPACE中调用new_*存储库函数，来从网络中下载依赖。下面的例子下载Google Test库：</p>
<pre class="crayon-plain-tag"># 下载归档文件，并让其在工作空间的存储库中可用
new_http_archive(
    name = "gtest",
    url = "https://github.com/google/googletest/archive/release-1.7.0.zip",
    sha256 = "b58cb7547a28b2c718d1e38aee18a3659c9e3ff52440297e965f5edffe34b6d0",
    # 外部库的构建规则编写在gtest.BUILD
    # 如果此归档文件已经自带了BUILD文件，则可以调用不带new_前缀的函数
    build_file = "gtest.BUILD",
    # 去除路径前缀
    strip_prefix = "googletest-release-1.7.0",
)</pre>
<p>构建此外部库的规则如下：</p>
<pre class="crayon-plain-tag">cc_library(
    name = "main",
    srcs = glob(
        # 前缀去除，原来是googletest-release-1.7.0/src/*.cc
        ["src/*.cc"],
        # 排除此文件
        exclude = ["src/gtest-all.cc"]
    ),
    hdrs = glob([
        # 前缀去除
        "include/**/*.h",
        "src/*.h"
    ]),
    copts = [
        # 前缀去除，原来是external/gtest/googletest-release-1.7.0/include
        "-Iexternal/gtest/include"
    ],
    # 链接到pthread
    linkopts = ["-pthread"],
    visibility = ["//visibility:public"],
)</pre>
<div class="blog_h3"><span class="graybg">使用外部库</span></div>
<p>沿用上面的例子，下面的目标使用gtest编写测试代码：</p>
<pre class="crayon-plain-tag">cc_test(
    name = "hello-test",
    srcs = ["hello-test.cc"],
    # 前缀去除
    copts = ["-Iexternal/gtest/include"],
    deps = [
        # 依赖gtest存储库的main目标
        "@gtest//:main",
        "//lib:hello-greet",
    ],
)</pre>
<div class="blog_h1"><span class="graybg">外部依赖</span></div>
<p>Bazel允许依赖其它项目中定义的目标，这些<span style="background-color: #c0c0c0;">来自其它项目的依赖叫做“外部依赖“</span>。当前工作空间的<span style="background-color: #c0c0c0;">WORKSPACE文件声明从何处下载外部依赖的源码</span>。</p>
<p>外部依赖可以有自己的1-N个BUILD文件，其中定义自己的目标。当前项目可以使用这些目标。例如下面的两个项目结构：</p>
<pre class="crayon-plain-tag">/
  home/
    user/
      project1/
        WORKSPACE
        BUILD
        srcs/
          ...
      project2/
        WORKSPACE
        BUILD
        my-libs/</pre>
<p>如果project1需要依赖定义在project2/BUILD中的目标:foo，则可以在其WORKSPACE中<span style="background-color: #c0c0c0;">声明一个存储库（repository）</span>，名字为project2，位于/home/user/project2。然后，可以在BUILD中通过标签<span style="background-color: #c0c0c0;">@project2//:foo</span>引用目标foo。</p>
<p>除了依赖来自文件系统其它部分的目标、下载自互联网的目标以外，用户还可以<span style="background-color: #c0c0c0;">编写自己的存储库规则（repository rules ）以实现更复杂的行为</span>。</p>
<p>WORKSPACE的语法格式和BUILD相同，但是允许使用<a href="https://docs.bazel.build/versions/master/be/workspace.html">不同的规则集</a>。</p>
<p>Bazel会把外部依赖下载到<pre class="crayon-plain-tag">$(bazel info output_base)/external</pre>目录中，要删除掉外部依赖，执行：</p>
<pre class="crayon-plain-tag">bazel clean --expunge</pre>
<div class="blog_h2"><span class="graybg">外部依赖类型</span></div>
<div class="blog_h3"><span class="graybg">Bazel项目</span></div>
<p>可以使用local_repository、git_repository或者http_archive这几个规则来引用。</p>
<p>引用本地Bazel项目的例子：</p>
<pre class="crayon-plain-tag">local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
)</pre>
<p>在BUILD中，引用coworkers_project中的目标//foo:bar时，使用标签@coworkers_project//foo:bar </p>
<div class="blog_h3"><span class="graybg">非Bazel项目</span></div>
<p>可以使用new_local_repository、new_git_repository或者new_http_archive这几个规则来引用。你需要自己编写BUILD文件来构建这些项目。</p>
<p>引用本地非Bazel项目的例子：</p>
<pre class="crayon-plain-tag">new_local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
    build_file = "coworker.BUILD",
)</pre><br />
<pre class="crayon-plain-tag">cc_library(
    name = "some-lib",
    srcs = glob(["**"]),
    visibility = ["//visibility:public"],
)&amp;nbsp;</pre>
<p>在BUILD文件中，使用标签@coworkers_project//:some-lib引用上面的库。 </p>
<div class="blog_h3"><span class="graybg">外部包</span></div>
<p>对于Maven仓库，可以使用规则maven_jar/maven_server来下载JAR包，并将其作为Java依赖。</p>
<div class="blog_h2"><span class="graybg">依赖拉取</span></div>
<p>默认情况下，执行bazel Build时会按需自动拉取依赖，你也可以禁用此特性，并使用bazel fetch预先手工拉取依赖。</p>
<div class="blog_h2"><span class="graybg">使用代理</span></div>
<p>Bazel可以使用HTTPS_PROXY或HTTP_PROXY定义的代理地址。</p>
<div class="blog_h2"><span class="graybg">依赖缓存</span></div>
<p>Bazel会缓存外部依赖，当WORKSPACE改变时，会重新下载或更新这些依赖。</p>
<div class="blog_h1"><span class="graybg">.bazelrc</span></div>
<p>Bazel命令接收大量的参数，其中一部分很少变化，这些不变的配置项可以存放在.bazelrc中。</p>
<div class="blog_h2"><span class="graybg">位置</span></div>
<p>Bazel按以下顺序寻找.bazelrc文件：</p>
<ol>
<li>除非指定--nosystem_rc，否则寻找/etc/bazel.bazelrc</li>
<li>除非指定--noworkspace_rc，否则寻找工作空间根目录的.bazelrc</li>
<li>除非指定--nohome_rc，否则寻找当前用户的$HOME/.bazelrc</li>
</ol>
<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>import</td>
<td>导入其它bazelrc文件，例如：<pre class="crayon-plain-tag">import %workspace%/tools/bazel.rc</pre></td>
</tr>
<tr>
<td>默认参数</td>
<td>
<p>可以提供以下行：</p>
<p>startup ... 启动参数<br />common... 适用于所有命令的参数<br /><em>command</em>...为某个子命令指定参数，例如buildquery、</p>
<p>以上三类行，都可以出现多次</p>
</td>
</tr>
<tr>
<td>
<p>--config</p>
</td>
<td>
<p>用于定义一组参数的组合，在调用bazel命令时指定--config=memcheck，可以引用名为memcheck的参数组。此参数组的定义示例：</p>
<pre class="crayon-plain-tag">build:memcheck --strip=never --test_timeout=3600</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">扩展</span></div>
<p>所谓Bazel扩展，是扩展名为.bzl的文件。你可以使用load语句加载扩展中定义的符号到BUILD中。
<div class="blog_h2"><span class="graybg">构建阶段</span></div>
<p>一次Bazel构建包含三个阶段：</p>
<ol>
<li>加载阶段：加载、eval本次构建需要的所有扩展、所有BUILD文件。宏在此阶段执行，规则被实例化。BUILD文件中<span style="background-color: #c0c0c0;">调用的宏/函数，在此阶段执行函数体</span>，其结果是<span style="background-color: #c0c0c0;">宏里面实例化的规则被填充到BUILD文件</span>中</li>
<li>分析阶段：<span style="background-color: #c0c0c0;">规则的代码——也就是它的implementation函数被执行</span>，导致规则的Action被实例化，Action描述如何从输入产生输出</li>
<li>执行阶段：执行Action，产生输出，<span style="background-color: #c0c0c0;">测试也在此阶段执行</span></li>
</ol>
<p>Bazel会并行的读取/解析/eval BUILD文件和.bzl文件。每个文件在每次构建最多被读取一次，eval的结果被缓存并重用。每个文件在它<span style="background-color: #c0c0c0;">的全部依赖被解析之后</span>才eval。加载一个.bzl文件没有副作用，仅仅是定义值和函数</p>
<div class="blog_h2"><span class="graybg">宏</span></div>
<p>宏（Macro）是一种函数，用来实例化（instantiates）规则。如果BUILD文件太过重复或复杂，可以考虑使用宏，以便减少代码。<span style="background-color: #c0c0c0;">宏的函数在BUILD文件被读取时就立即执行</span>。BUILD被读取（eval）之后，宏被替换为它生成的规则。bazel query只会列出生成的规则而非宏。</p>
<p>编写宏时需要注意：</p>
<ol>
<li>所有实例化规则的公共函数，都必须具有一个无默认值的name参数</li>
<li>公共函数应当具有docstring</li>
<li>在BUILD文件中，调用宏时name参数必须是关键字参数</li>
<li>宏所生成的规则的name属性，必须以调用宏的name参数作为后缀</li>
<li>大部分情况下，可选参数应该具有默认值None</li>
<li>应当具有可选的visibility参数</li>
</ol>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>要在宏中实例化原生规则（Native rules，不需要load即可使用的那些规则），可以<span style="background-color: #c0c0c0;">使用native模块</span>：</p>
<pre class="crayon-plain-tag"># 该宏实例化一个genrule规则
def file_generator(name, arg, visibility=None):
  // 生成一个genrule规则
  native.genrule(
    name = name,
    outs = [name + ".txt"],
    cmd = "$(location generator) %s &gt; $@" % arg,
    tools = ["//test:generator"],
    visibility = visibility,
  )</pre>
<p>使用上述宏的BUILD文件：</p>
<pre class="crayon-plain-tag">load("//path:generator.bzl", "file_generator")

file_generator(
    name = "file",
    arg = "some_arg",
)</pre>
<p>执行下面的命令查看宏展开后的情况：</p>
<pre class="crayon-plain-tag"># bazel query --output=build //label

genrule(
  name = "file",
  tools = ["//test:generator"],
  outs = ["//test:file.txt"],
  cmd = "$(location generator) some_arg &gt; $@",
)</pre>
<div class="blog_h2"><span class="graybg">规则</span></div>
<p><a href="https://docs.bazel.build/versions/master/skylark/rules.html">规则（Rule）</a>比宏更强大，能够对Bazel内部特性进行访问，并可以完全控制Bazel。</p>
<p>规则定义了为了产生输出，需要在输入上执行的一系列动作。例如，C++二进制文件规则以一系列.cpp文件为输入，针对输入调用g++，输出一个可执行文件。注意，从Bazel的角度来说，不但cpp文件是输入，g++、C++库也是输入。当编写自定义规则时，你需要注意，将执行Action所需的库、工具作为输入看待。</p>
<p>Bazel内置了一些规则，这些规则叫原生规则，例如cc_library、cc_library，对一些语言提供了基础的支持。通过编写自定义规则，你可以实现对任何语言的支持。</p>
<p>定义在.bzl中的规则，用起来就像原生规则一样 —— 规则的目标具有标签、可以出现在bazel query。</p>
<p>规则在分析阶段的行为，由它的implementation函数决定。此函数不得调用任何外部工具，它只是注册在执行阶段需要的Action。</p>
<div class="blog_h3"><span class="graybg">自定义规则</span></div>
<p>在.bzl文件中，你可以调用rule创建自定义规则，并将其保存到全局变量：</p>
<pre class="crayon-plain-tag">def _empty_impl(ctx):
    # 分析阶段此函数被执行
    print("This rule does nothing")

empty = rule(implementation = _empty_impl)</pre>
<p>然后，规则可以通过load加载到BUILD文件：</p>
<pre class="crayon-plain-tag">load("//empty:empty.bzl", "empty")

# 实例化规则
empty(name = "nothing")</pre>
<div class="blog_h3"><span class="graybg">规则属性 </span></div>
<p>属性即实例化规则时需要提供的参数，例如srcs、deps。在自定义规则的时候，你可以列出所有属性的名字和Schema：</p>
<pre class="crayon-plain-tag">sum = rule(
    implementation = _impl,
    attrs = {
        # 定义一个整数属性，一个列表属性
        "number": attr.int(default = 1),
        "deps": attr.label_list(),
    },
)</pre>
<p>实例化规则的时候，你需要以参数的形式指定属性：</p>
<pre class="crayon-plain-tag">sum(
    name = "my-target",
    deps = [":other-target"],
)

sum(
    name = "other-target",
)</pre>
<p>如果实例化规则的时候，没有指定某个属性的值（且没指定默认值），规则的实现函数会在ctx.attr中看到一个占位符，此占位符的值取决于属性的类型。</p>
<p>使用default为属性指定默认值，使用 mandatory=True 声明属性必须提供。</p>
<div class="blog_h3"><span class="graybg">默认属性</span></div>
<p>任何规则自动具有以下属性：deprecation, features, name, tags, testonly, visibility。</p>
<p>任何测试规则具有以下额外属性：args, flaky, local, shard_count, size, timeout。</p>
<div class="blog_h3"><span class="graybg">特殊属性</span></div>
<p>有两类特殊属性需要注意：</p>
<ol>
<li>依赖属性：例如attr.label、attr.label_list，用于声明拥有此属性的目标所依赖的其它目标</li>
<li>输出属性：例如attr.output、attr.output_list，声明目标的输出文件，较少使用</li>
</ol>
<p>上面两类属性的值都是Label类型。 </p>
<div class="blog_h3"><span class="graybg">隐含依赖</span></div>
<p>具有默认值的依赖属性，称为隐含依赖（implicit dependency）。如果要硬编码规则和工具（例如编译器）之间的关系，可通过隐含依赖。从规则的角度来看，这些工具仍然属于输入，就像源代码一样。</p>
<div class="blog_h3"><span class="graybg">私有属性</span></div>
<p>某些情况下，我们会为规则添加具有默认值的属性，同时还想禁止用户修改属性值，这种情况下可以使用私有属性。</p>
<p>私有属性以下划线 _ 开头，必须具有默认值。</p>
<div class="blog_h3"><span class="graybg">目标</span></div>
<p>实例化规则不会返回值，但是会定义一个新的目标。</p>
<div class="blog_h3"><span class="graybg">规则实现</span></div>
<p>任何规则都需要提供一个实现函数。提供在分析阶段需要严格执行的逻辑。此函数不能有任何读写行为，仅仅用于注册Action。</p>
<p>实现函数具有唯一性入参  —— <a href="https://docs.bazel.build/versions/master/skylark/lib/ctx.html">规则上下文</a>，通常将其命名为ctx。通过规则上下文你可以：</p>
<ol>
<li>访问规则属性</li>
<li>获得输入输出文件的handle</li>
<li>创建Actions</li>
<li>通过providers向依赖于当前规则的其它规则传递信息</li>
</ol>
<div class="blog_h3"><span class="graybg">ctx</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>action</td>
<td>废弃，使用ctx.actions.run()或ctx.actions.run_shell()代替</td>
</tr>
<tr>
<td>actions.run</td>
<td>
<p>创建一个调用可执行文件的Action，参数：Bazel加载阶段</p>
<p>outputs 此动作的输出文件列表<br />inputs 此动作输入文件的列表/depset<br />executable 执行此动作需要调用的可执行文件<br />tools 执行此动作需要的工具的列表/depset<br />arguments 传递给可执行文件的参数列表<br />mnemonic 动作的描述<br />progress_message 动作执行时，显示给用户的信息<br />use_default_shell_env 是否在内建Shell环境下运行可执行文件<br />env 环境变量字典<br />execution_requirements 调度此动作需要的信息<br />input_manifests 输入runfiles元数据，通常由resolve_command生成</p>
<p>示例：</p>
<pre class="crayon-plain-tag">def _impl(ctx):
    # The list of arguments we pass to the script.
    args = [ctx.outputs.out.path] + [f.path for f in ctx.files.chunks]

    # 调用可执行文件
    ctx.actions.run(
        inputs = ctx.files.chunks,
        outputs = [ctx.outputs.out],
        arguments = args,
        progress_message = "Merging into %s" % ctx.outputs.out.short_path,
        executable = ctx.executable._merge_tool,
    )
// 规则定义
concat = rule(
    implementation = _impl,
    attrs = {
        "chunks": attr.label_list(allow_files = True),
        "out": attr.output(mandatory = True),
        "_merge_tool": attr.label(
            executable = True,
            cfg = "host",
            allow_files = True,
            default = Label("//actions_run:merge"),
        ),
    },
) </pre>
</td>
</tr>
<tr>
<td>actions.run_shell</td>
<td>
<p>创建一个执行Shell脚本的Action
<p>示例：</p>
<p><pre class="crayon-plain-tag">def _impl(ctx):
    output = ctx.outputs.out
    input = ctx.file.file

    # 访问inputs中声明的文件
    ctx.actions.run_shell(
        inputs = [input],
        outputs = [output],
        progress_message = "Getting size of %s" % input.short_path,
        command = "stat -L -c%%s '%s' &gt; '%s'" % (input.path, output.path),
    )

# 规则定义
size = rule(
    implementation = _impl,
    attrs = {"file": attr.label(mandatory = True, allow_single_file = True)},
    outputs = {"out": "%{name}.size"},
)</pre>
</td>
</tr>
<tr>
<td>actions.write</td>
<td>此Action写入内容到文件</td>
</tr>
<tr>
<td>actions.declare_file</td>
<td>此Action创建新的文件</td>
</tr>
<tr>
<td>actions.do_nothing</td>
<td>不做任何事情的Action</td>
</tr>
<tr>
<td>ctx.attr</td>
<td>用于访问属性值的结构 </td>
</tr>
<tr>
<td>bin_dir</td>
<td>二进制目录的根</td>
</tr>
<tr>
<td>genfiles_dir</td>
<td>genfiles目录的根</td>
</tr>
<tr>
<td>build_file_path</td>
<td>相对于源码目录根的，当前BUILD文件的路径</td>
</tr>
<tr>
<td>executable </td>
<td>一个结构，可以引用任何通过<pre class="crayon-plain-tag">attr.label(executable=True)</pre>定义的规则属性</td>
</tr>
<tr>
<td>expand_location </td>
<td>
<p>展开input中定义的所有$(location //x)为目标x的真实路径。仅仅对当前规则的直接依赖、明确列在targets属性中的目标使用</p>
<p><pre class="crayon-plain-tag">string ctx.expand_location(input, targets=[])</pre>
</td>
</tr>
<tr>
<td>features</td>
<td>列出此规则明确启用的特性列表 </td>
</tr>
<tr>
<td>file</td>
<td>
<p>此结构包含任何通过<pre class="crayon-plain-tag">attr.labe(allow_single_file=True)</pre>定义的属性所指向的文件。此结构的字段名即文件属性名，结构字段值是file或Node类型</p>
<p>此结构是表达式<pre class="crayon-plain-tag">list(ctx.attr.&lt;ATTR&gt;.files)[0]</pre>的快捷方式</p>
</td>
</tr>
<tr>
<td>fragments</td>
<td>用于访问目标配置中的配置片断（configuration fragments ） </td>
</tr>
<tr>
<td>host_configuration</td>
<td>返回主机配置的configuration对象。configuration包含构建所在的运行环境信息 </td>
</tr>
<tr>
<td>host_fragments </td>
<td>用于访问host配置中的配置片断（configuration fragments ）  </td>
</tr>
<tr>
<td>label</td>
<td>当前正在分析的目标的标签 </td>
</tr>
<tr>
<td>outputs</td>
<td>一个包含所有预声明的输出文件的伪结构 </td>
</tr>
<tr>
<td>resolve_command</td>
<td>
<p>解析一个命令，返回(inputs, command, input_manifests)元组：</p>
<p>inputs，表示解析后的输入列表<br />command，解析后的命令的argv列表<br />input_manifests，执行命令需要的runfiles元数据</p>
</td>
</tr>
<tr>
<td>resolve_tools </td>
<td>解析工具，返回(inputs, input_manifests)元组 </td>
</tr>
<tr>
<td>runfiles </td>
<td>创建一个Runfiles</td>
</tr>
<tr>
<td>toolchains</td>
<td>此规则需要的工具链 </td>
</tr>
<tr>
<td>var </td>
<td>配置变量的字典</td>
</tr>
<tr>
<td>workspace_name </td>
<td>当前工作空间的名称 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">存储库规则</span></div>
<p>存储库规则用于定义外部存储库。<span style="background-color: #c0c0c0;">外部存储库是一种规则</span>，这种规则只能用在WORKSPACE文件中，可以在<span style="background-color: #c0c0c0;">Bazel加载阶段</span>启用<span style="background-color: #c0c0c0;">非封闭性</span>（ non-hermetic，所谓封闭是指自包含，不依赖于外部环境）操作。每个外部存储库都创建自己的WORKSPACE，具有自己的BUILD文件和构件。</p>
<p>外部存储库可以用来：</p>
<ol>
<li>加载第三方依赖，例如Maven打包的库</li>
<li>为运行构件的主机生成特化的BUILD文件</li>
</ol>
<p>在bzl文件中，调用repository_rule函数可以创建一个存储库规则，你需要将其存放在全局变量中：</p>
<pre class="crayon-plain-tag">local_repository = repository_rule(
    # 实现函数
    implementation=_impl,
    local=True,
    # 属性列表
    attrs={"path": attr.string(mandatory=True)}) </pre>
<p>每个存储库规则都必须提供实现函数，其中包含<span style="background-color: #c0c0c0;">在Bazel加载阶段需要执行</span>的严格的逻辑。该函数具有唯一的入参repository_ctx：</p>
<pre class="crayon-plain-tag">def _impl(repository_ctx):
  # 你可以通过repository_ctx访问属性值、调用非密封性函数（例如查找、执行二进制文件，创建或下载文件到存储库）
  repository_ctx.symlink(repository_ctx.attr.path, "") </pre>
<p>引用存储库中规则时，可以使用<pre class="crayon-plain-tag">@REPO_NAMAE//package:target</pre>这样的标签。</p>
<div class="blog_h3"><span class="graybg">repository_ctx</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>attr</td>
<td>用于访问所有属性的结构</td>
</tr>
<tr>
<td>download</td>
<td>
<pre class="crayon-plain-tag">struct repository_ctx.download(url, output='', sha256='', executable=False)</pre></p>
<p> 下载文件到输出路径，返回包含字段sha256的结构</p>
</td>
</tr>
<tr>
<td>download_and_extract</td>
<td>下载并解压</td>
</tr>
<tr>
<td>execute</td>
<td>执行指定的命令</td>
</tr>
<tr>
<td>file</td>
<td>以指定的内容在存储库目录下生成文件</td>
</tr>
<tr>
<td>name</td>
<td>此规则生成的外部存储库的名称</td>
</tr>
<tr>
<td>path</td>
<td>返回字符串/路径/标签对应的实际路径</td>
</tr>
<tr>
<td>symlink</td>
<td>在文件系统中创建符号链接<br />
<pre class="crayon-plain-tag"># from 符号链接的源，string/Label/path类型
# to 相对于存储库目录的符号链接文件的路径
None repository_ctx.symlink(from, to)</pre>
</td>
</tr>
<tr>
<td>template</td>
<td>使用模板创建文件</td>
</tr>
<tr>
<td>which</td>
<td>返回指定程序的路径</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">bazel</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 class="blog_h3">analyze-profile</td>
<td>分析构建配置数据（build profile data）</td>
</tr>
<tr>
<td class="blog_h3">aquery</td>
<td>针对post-analysis操作图执行查询</td>
</tr>
<tr>
<td class="blog_h3">build</td>
<td>构建指定的目标：<br />
<pre class="crayon-plain-tag"># 构建foo/bar包中的wiz目标
bazel build //foo/bar:wiz
# 构建foo/bar包中的bar目标
bazel build //foo/bar
# 构建foo/bar包中的所有规则
bazel build //foo/bar:all
# 构建foo目录下所有子代包的所有规则
bazel build //foo/...
bazel build //foo/...:all
# 构建foo目录下所有子代包的所有目标（规则和文件）
bazel build //foo/...:*
bazel build //foo/...:all-targets</pre></p>
<p>如果目标标签不以<pre class="crayon-plain-tag">//</pre>开头，则相对于当前目录。如果当前目录是foo则bar:wiz等价于//foo/bar:wiz</p>
<p>Bazel支持通过符号链接来寻找子包，除了：</p>
<ol>
<li>那些指向输出目录的子目录的符号链接，例如bazel-bin</li>
<li>包含了名为DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN文件的目录</li>
</ol>
<p>指定了<pre class="crayon-plain-tag">tags = ["manual"]</pre>的目标必须手工构建，无法通过...、:*、:all等自动构建</p>
<p>常用选项：</p>
<p style="padding-left: 30px;">--loading_phase_threads   加载阶段使用的线程数量，可以防止并发太多导致下载缓慢，进而超时</p>
</td>
</tr>
<tr>
<td class="blog_h3">canonicalize-flags</td>
<td>规范化Bazel标记</td>
</tr>
<tr>
<td class="blog_h3">clean</td>
<td>清除输出文件，可选的停止服务器</td>
</tr>
<tr>
<td class="blog_h3">cquery</td>
<td>针对post-analysis依赖图查询</td>
</tr>
<tr>
<td class="blog_h3">dump</td>
<td>输出Bazel服务器的内部状态</td>
</tr>
<tr>
<td class="blog_h3">info</td>
<td>输出Bazel服务器的运行时信息</td>
</tr>
<tr>
<td class="blog_h3">fetch</td>
<td>
<p>拉取某个目标的外部依赖</p>
<p>使用<pre class="crayon-plain-tag">--fetch=false</pre>标记可以禁止在构建时进行自动的外部依赖（本地系统依赖除外）抓取，通过local_repository、new_local_repository声明的“本地”外部存储库，总是会抓取</p>
<p>如果禁用了自动抓取，你需要在以下时机手工抓取：</p>
<ol>
<li>第一次构建之前</li>
<li>每当新增了外部依赖之后</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag"># 抓取两个外部依赖
bazel fetch //foo:bar //bar:baz
# 抓取工作空间的全部外部依赖
bazel fetch //...</pre>
<p><strong><span style="background-color: #c0c0c0;">存储库缓存</span></strong></p>
<p>Bazel会避免反复抓取同一个文件，即使：</p>
<ol>
<li>多个工作空间使用同一外部依赖</li>
<li>外部存储库的定义改变了，但是需要下载的还是那个文件</li>
</ol>
<p>Bazel在本地文件系统维护外部存储库的缓存，默认位置在~/.cache/bazel/_bazel_$USER/cache/repos/v1/。可以使用选项--repository_cache指定不同的缓存位置。缓存可以被所有命名空间、所有Bazel版本共享</p>
<p><strong><span style="background-color: #c0c0c0;">避免下载</span></strong></p>
<p>你可以指定--distdir选项，其值是一个只读的目录，bazel会在目录中寻找文件，而非去网络上下载。匹配方式是URL中的Basename + 文件哈希。如果不指定哈希值，则Bazel不会去--distdir寻找文件</p>
</td>
</tr>
<tr>
<td class="blog_h3">mobile-install</td>
<td>在移动设备上安装目标</td>
</tr>
<tr>
<td class="blog_h3">query</td>
<td>执行依赖图查询</td>
</tr>
<tr>
<td class="blog_h3">run</td>
<td>运行指定的目标</td>
</tr>
<tr>
<td class="blog_h3">shutdown</td>
<td>关闭Bazel服务器</td>
</tr>
<tr>
<td class="blog_h3">test</td>
<td>构建并运行指定的测试目标</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/bazel-study-note">Bazel学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/bazel-study-note/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>libevent学习笔记</title>
		<link>https://blog.gmem.cc/libevent-study-note</link>
		<comments>https://blog.gmem.cc/libevent-study-note#comments</comments>
		<pubDate>Wed, 21 Feb 2018 03:49:55 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25325</guid>
		<description><![CDATA[<p>简介 libevent是一个事件驱动编程库，可以在文件描述符上发生特定事件、超时后，执行相应的回调函数。回调函数还可以由信号、定时器触发。 使用libevent后，你不需要为事件驱动的网络服务编写事件循环，而只需要调用[crayon-69d3dc89aa4a6148152233-i/]并动态注册/删除事件。 libevent目前支持/dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), epoll(4)等I/O多路复用机制，这些底层机制和libevent提供的API完全解耦，你不需要关心。libevent可以根据系统选择最适合的机制。 你可以在多线程环境下使用libevent，并选取以下编程风格中的一种： 每个线程独立使用一个[crayon-69d3dc89aa4aa132012748-i/] 使用共享的event_base并使用锁保护 除了基础的事件轮询之外，libevent还实现了精巧的网络IO框架，支持套接字、过滤器、限速、SSL、零拷贝文件传输、IOCP，libevent内置对DNS、HTTP的支持，还提供了一个小型的RPC框架。 安装 [crayon-69d3dc89aa4ac350687361/] epoll 本章以Linux下的epoll为例，了解I/O多路复用技术的原理。  支持I/O操作的内核对象，都属于流（Stream），包括文件、管道、套接字。操控流时需要使用其文件描述符。当流中没有数据时，读线程会阻塞；当流已经写满数据时，写线程会阻塞。 <a class="read-more" href="https://blog.gmem.cc/libevent-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/libevent-study-note">libevent学习笔记</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>libevent是一个事件驱动编程库，可以在文件描述符上发生<span style="background-color: #c0c0c0;">特定事件、超时</span>后，执行相应的回调函数。回调函数还可以由<span style="background-color: #c0c0c0;">信号、定时器触发</span>。</p>
<p>使用libevent后，你不需要为事件驱动的网络服务编写事件循环，而只需要调用<pre class="crayon-plain-tag">event_dispatch()</pre>并动态注册/删除事件。</p>
<p>libevent目前支持/dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), epoll(4)等I/O多路复用机制，这些底层机制和libevent提供的API完全解耦，你不需要关心。libevent可以根据系统选择最适合的机制。</p>
<p>你可以在多线程环境下使用libevent，并选取以下编程风格中的一种：</p>
<ol>
<li>每个线程独立使用一个<pre class="crayon-plain-tag">event_base</pre></li>
<li>使用共享的event_base并使用锁保护</li>
</ol>
<p>除了基础的事件轮询之外，libevent还实现了精巧的<span style="background-color: #c0c0c0;">网络IO框架</span>，支持套接字、过滤器、限速、SSL、零拷贝文件传输、IOCP，libevent内置对<span style="background-color: #c0c0c0;">DNS、HTTP的支持</span>，还提供了一个小型的<span style="background-color: #c0c0c0;">RPC框架</span>。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">wget https://github.com/libevent/libevent/releases/download/release-2.1.8-stable/libevent-2.1.8-stable.tar.gz
tar -zxvf libevent-2.1.8-stable.tar.gz
pushd libevent-2.1.8-stable
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
sudo make install</pre>
<div class="blog_h1"><span class="graybg">epoll</span></div>
<p>本章以Linux下的epoll为例，了解I/O多路复用技术的原理。 </p>
<p>支持I/O操作的内核对象，都属于流（Stream），包括<span style="background-color: #c0c0c0;">文件、管道、套接字</span>。操控流时需要使用其<span style="background-color: #c0c0c0;">文件描述符</span>。当流中没有数据时，读线程会阻塞；当流已经写满数据时，写线程会阻塞。</p>
<p>为了解决阻塞的问题，Linux提供了多种系统调用，包括select、epoll。后者目前被广泛使用，它的优势是：</p>
<ol>
<li>无需遍历被监控文件描述符的完整集合，只需要关注活动的连接</li>
<li>支持处理大量连接请求而不出现性能下降</li>
</ol>
<div class="blog_h2"><span class="graybg">触发模式</span></div>
<p>epoll支持两种事件触发模式：</p>
<ol>
<li>水平触发：内核事件集会拷贝给用户态事件，假设用户处理了一部分，那些<span style="background-color: #c0c0c0;">没有处理的会在下次epoll_wait再次返回</span></li>
<li>边缘触发：任何内核事件仅仅会被通知一次。拷贝减少性能增加，但是用户代码如果没有处理事件，会导致事件丢失</li>
</ol>
<div class="blog_h2"><span class="graybg">主要接口</span></div>
<div class="blog_h3"><span class="graybg">创建epoll实例</span></div>
<pre class="crayon-plain-tag">// size 提示内核此epoll实例需要监控的fd的数量
// 返回epoll实例的文件描述符
int epoll_create(int size);

// 示例：
int epfd = epoll_create(1024);</pre>
<div class="blog_h3"><span class="graybg">控制epoll实例</span></div>
<pre class="crayon-plain-tag">// epfd 控制的epoll实例
// op 执行的控制行为：
//   EPOLL_CTL_ADD，注册新的fd到epfd
//   EPOLL_CTL_MOD，修改已经注册的fd的监听事件
//   EPOLL_CTL_DEL，删除一个fd
// 
// fd 目标文件描述符
// event 告诉内核需要监听的事件
// 
// 成功返回0，失败返回-1, errno查看错误信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// 事件的结构如下：
struct epoll_event
{
  // EPOLLIN, EPOLLOUT, EPOLLPRI, EPOLLHUP, EPOLLET, EPOLLONESHOT等类型
  uint32_t events;    // 事件类型
  epoll_data_t data;  // 用户传递的数据
}
// 用户数据联合体
typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;</pre>
<div class="blog_h3"><span class="graybg">等待epoll事件</span></div>
<pre class="crayon-plain-tag">// epfd 等待的epoll实例
// event 事件缓冲，内核将等到的事件存放到此数组中
// maxevents 最多获取多少个事件，不得大于epoll_create()调用时指定的size
// timeout 超时，超过指定的时间从等待返回：
//         -1: 永久阻塞   0: 立即返回，非阻塞     &gt;0: 指定微秒数
// 返回 就绪的文件描述符数量
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);

// 示例：
struct epoll_event event_buffer[512];
int event_count = epoll_wait(epfd, epoll_event, 512, -1);</pre>
<div class="blog_h2"><span class="graybg">编程模板 </span></div>
<pre class="crayon-plain-tag">// 创建实例
int epfd = epoll_crete(1024);

// 将监听套接字的文件描述符加入监控
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &amp;listen_event);

while (1) {
    // 等待就绪
    int event_count = epoll_wait(epfd, events, 512, -1);
    // 遍历就绪事件列表
    for (i = 0 ; i &lt; event_count; i++) {
        if (evnets[i].data.fd == listen_fd) {
            // 这是监听套接字上的事件，accept连接，并将连接套接字的fd加入到epoll
        }
        else if (events[i].events &amp; EPOLLIN) {
            // 可读
        }
        else if (events[i].events &amp; EPOLLOUT) {
            // 可写
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">示例代码</span></div>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;
#include &lt;string.h&gt;

#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;

#include &lt;sys/epoll.h&gt;

#define SERVER_PORT         (8080)
#define SERVER_IP           ("127.0.0.1")
#define EPOLL_MAX_NUM       (1024)
#define BUFFER_MAX_LEN      (4096)

char buffer[BUFFER_MAX_LEN];

int main(int argc, char **argv) {
  int listen_fd = 0;
  int client_fd = 0;
  struct sockaddr_in server_addr;
  struct sockaddr_in client_addr;
  socklen_t client_len;

  int epfd = 0;
  struct epoll_event event, *events_buffer;

  // 创建套接字
  listen_fd = socket(AF_INET, SOCK_STREAM, 0);

  // 绑定到指定地址和端口
  server_addr.sin_family = AF_INET;
  // 展现形式（p，字符串）转换为数字形式（n）
  inet_pton(AF_INET, SERVER_IP, &amp;server_addr.sin_addr);
  server_addr.sin_port = htons(SERVER_PORT);
  bind(listen_fd, (struct sockaddr *) &amp;server_addr, sizeof(server_addr));

  // 开始监听连接
  listen(listen_fd, 10);

  // 创建epoll实例
  epfd = epoll_create(EPOLL_MAX_NUM);
  if (epfd &lt; 0)
    goto END;

  // 将监听套接字的可读事件加入到epoll
  event.events = EPOLLIN;
  event.data.fd = listen_fd;
  if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &amp;event) &lt; 0)
    goto END;

  // 分配事件缓冲
  events_buffer = malloc(sizeof(struct epoll_event)*EPOLL_MAX_NUM);

  // 事件循环
  while (1) {
    // 等待一批事件，有事件就可能返回，不一定要等待到EPOLL_MAX_NUM
    int active_fds_cnt = epoll_wait(epfd, events_buffer, EPOLL_MAX_NUM, 1000);
    for (int i = 0; i &lt; active_fds_cnt; i++) {
      // 监听套接字就绪
      if (events_buffer[i].data.fd==listen_fd) {
        // 接受新连接
        client_fd = accept(listen_fd, (struct sockaddr *) &amp;client_addr, &amp;client_len);
        if (client_fd &lt; 0)
          continue;

        char ip[20];
        // 数字转换为展现形式
        const char *client_ip = inet_ntop(AF_INET, &amp;client_addr.sin_addr, ip, sizeof(ip));
        // 网络序转换为主机序
        uint16_t client_port = ntohs(client_addr.sin_port);
        printf("new connection %s:%d accepted\n", client_ip, client_port);

        // 将连接套接字加入epoll，关注其可读可写事件
        event.events = EPOLLIN | EPOLLET;
        event.data.fd = client_fd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &amp;event);
      } else if (events_buffer[i].events &amp; EPOLLIN) {
        // 连接套接字就绪，可读
        client_fd = events_buffer[i].data.fd;

        buffer[0] = '\0';
        // 读入最多5字节
        int n = read(client_fd, buffer, 5);
        if (n &lt; 0) {
          // 读取失败
          continue;
        } else if (n==0) {
          // 读取到0字节，说明EOF，关闭连接并注册epoll监听
          epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &amp;event);
          close(client_fd);
        } else {
          // 读取到数据
          printf("data from client: %s\n", buffer);
          // 原样写回去
          buffer[n] = '\0';
          // 这是阻塞写
          write(client_fd, buffer, strlen(buffer));
          // 重置缓冲区
          memset(buffer, 0, BUFFER_MAX_LEN);
        }
      } else if (events_buffer[i].events &amp; EPOLLOUT) {
        // 在这里进行非阻塞写
      }
    }
  }

  END:
  close(epfd);
  close(listen_fd);
  return 0;
} </pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;strings.h&gt;

#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;unistd.h&gt;
#include &lt;fcntl.h&gt;

#define BUFFER_MAX_LEN      (1024)
#define SERVER_PORT         (8080)
#define SERVER_IP           ("127.0.0.1")

int main(int argc, char **argv) {
  int sockfd;
  char recvbuf[BUFFER_MAX_LEN + 1] = {0};

  struct sockaddr_in server_addr;

  // 创建套接字
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) &lt; 0)
    exit(0);

  // 服务器地址
  bzero(&amp;server_addr, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, SERVER_IP, &amp;server_addr.sin_addr);

  // 连接到服务器
  if (connect(sockfd, (struct sockaddr *) &amp;server_addr, sizeof(server_addr)) &lt; 0) {
    fprintf(stderr, "failed to connect to server \n");
    exit(0);
  }

  char input[100];

  // 从标准输入读取输入
  while (fgets(input, 100, stdin)!=NULL) {
    send(sockfd, input, strlen(input), 0);
    // 读取服务器输出
    read(sockfd, recvbuf, BUFFER_MAX_LEN);
    printf("echo %s\n", recvbuf);
  }

}</pre>
<div class="blog_h1"><span class="graybg">libevent基础</span></div>
<div class="blog_h2"><span class="graybg">event_base</span></div>
<p>使用 libevent 函数之前需要分配一个或者多个event_base结构。此结构持有libevent分发循环（dispatch loop）的信息和状态，它是libevent的中心，跟踪所有未决、活动事件并将活动事件通知给应用程序。</p>
<p>event_base是一个不透明结构（其字段没有定义在头文件中）。调用<pre class="crayon-plain-tag">event_base_new()</pre>或<pre class="crayon-plain-tag">event_base_new_with_config()</pre>可以实例化event_base。</p>
<p>配合锁，你可以在多个线程中访问event_base，但是，其<span style="background-color: #c0c0c0;">事件循环只能在单个线程中执行</span>。</p>
<p>event_base可以选择一种IO多路复用技术，作为检测事件是否就绪的手段。这些IO多路复用技术包括： select, poll, epoll, kqueue, devpoll, evport, win32</p>
<div class="blog_h3"><span class="graybg">实例化</span></div>
<p>大部分情况下，调用下面的函数即可：</p>
<pre class="crayon-plain-tag">// 创建并返回一个具有默认设置的event_base
// 如果出错返回NULL
struct event_base *event_base_new(void);</pre>
<p>如果要定制even_base的特性，可以调用：</p>
<pre class="crayon-plain-tag">// 创建一个配置
struct event_config *event_config_new(void);

// 定制化
// 禁止使用某种IO多路复用机制
int event_config_avoid_method(struct event_config *cfg, const char *method);
// 要求IO多路复用机制支持某些特性，不支持特性的后端不被使用
enum event_method_feature {
    EV_FEATURE_ET = 0x01,    // 支持边缘触发
    EV_FEATURE_O1 = 0x02,    // 添加、删除单个事件，以及确定哪个事件是激活的 —— 这些操作必须是O(1)复杂度
    EV_FEATURE_FDS = 0x04,   // 支持任何文件描述符，而不仅仅是套接字
};
int event_config_require_features(struct event_config *cfg, enum event_method_feature feature);
// 设置标记
enum event_base_config_flag {
    EVENT_BASE_FLAG_NOLOCK = 0x01, // 不要为 event_base 分配锁，节省加解锁开销，但是多线程访问不安全
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02, // 选择使用的后端时,不要检测 EVENT* 环境变量
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10, // 如果选择了epoll后端，则使用更快的epoll-changelist。不得传递基于dup()或其变体克隆的文件描述符给lbevent
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};
int event_config_set_flag(struct event_config *cfg, enum event_base_config_flag flag);


// 使用配置实例化
struct event_base * event_base_new_with_config(const struct event_config *cfg);</pre>
<div class="blog_h3"><span class="graybg">销毁</span></div>
<pre class="crayon-plain-tag">// 该函数不会释放与 event_base 关联的任何事件，或者关闭他们的套接字，或者释放任何指针
void event_base_free(struct event_base *base);</pre>
<div class="blog_h3"><span class="graybg">优先级 </span></div>
<p>libevent支持为事件设置多个优先级。默认情况下event_base仅仅支持单个优先级，调用下面的方法设置优先级数量：</p>
<pre class="crayon-plain-tag">int event_base_priority_init(struct event_base *base, int n_priorities);</pre>
<p>新的事件可以使用[0,n_priorities-1]这个范围的优先级，值越低优先级越高。</p>
<div class="blog_h3"><span class="graybg">子进程</span></div>
<p>某些事件后端，在fork后不能正常工作，你可能需要重现初始化event_base：</p>
<pre class="crayon-plain-tag">struct event_base *base = event_base_new();

if (fork()) {
    continue_running_parent(base);
} else {
    // 子进程
    event_reinit(base);
    continue_running_child(base);
}</pre>
<div class="blog_h2"><span class="graybg">事件</span></div>
<p>事件是libevent操控的基本单元，事件代表一组条件的<span style="background-color: #c0c0c0;">集合</span>，<span style="background-color: #c0c0c0;">这些条件包括</span>：</p>
<ol>
<li>文件描述符已经就绪，可以读取或者写入</li>
<li>对于边缘触发：文件描述符变为就绪状态，可以读取或者写入</li>
<li>超时</li>
<li>信号</li>
<li>用户触发事件</li>
</ol>
<p>所有事件具有相似的生命周期：</p>
<ol>
<li>创建事件（并关联到event_base）后，变为initialized状态</li>
<li>添加到event_base中后，变为pending状态</li>
<li>触发事件的条件发生后，变为active状态。事件的回调会执行</li>
<li>如果将事件配置为<span style="background-color: #c0c0c0;">persistent</span>，则它总是保持在pending状态。<span style="background-color: #c0c0c0;">否则，执行回调后，事件不再是pending</span></li>
<li>从event_base删除事件后，它从pending变为initialized。再次添加则由变为pending</li>
</ol>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>调用event_new函数可以创建事件：</p>
<pre class="crayon-plain-tag">// 指示超时已经发生
#define EV_TIMEOUT      0x01
// 等待套接字或FD可读
#define EV_READ         0x02
// 等待套接字或FD可写
#define EV_WRITE        0x04
// 等待POSIX信号发生
#define EV_SIGNAL       0x08
// 持久化事件 —— 不会因为激活而被自动移除
#define EV_PERSIST      0x10
// 使用边缘触发 
#define EV_ET           0x20

// 事件回调函数指针的定义
typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

// 分配一个事件并关联到event_base
// base 事件关联到的event_base
// fd 需要监控的文件描述符或者信号
// what 希望监控的条件、事件的性质、触发方式 EV_READ, EV_WRITE, EV_SIGNAL, EV_PERSIST, EV_ET
// cb 事件发生后执行的回调函数
// arg 回调函数的参数
struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn cb, void *arg);

void event_free(struct event *event);</pre>
<p>event_assign也可以用来创建事件，和event_new不同的是，前者<span style="background-color: #c0c0c0;">不负责分配内存，你必须自己分配好event结构</span>：</p>
<pre class="crayon-plain-tag">int event_assign(struct event *, struct event_base *, evutil_socket_t, short, event_callback_fn, void *);</pre>
<p>事件创建后，你可以调用<pre class="crayon-plain-tag">event_add()</pre>或<pre class="crayon-plain-tag">event_del()</pre> 来添加（使之pending）、删除（使之非pending）事件。</p>
<div class="blog_h3"><span class="graybg">创建信号事件</span></div>
<p>可以使用下面的宏：</p>
<pre class="crayon-plain-tag">#define evsignal_new(base, signum, cb, arg) \
    event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)</pre>
<p>对于大部分的IO多路复用机制，每个进程任何时刻<span style="background-color: #c0c0c0;">只能有一个 event_base 可以监听信号</span>。</p>
<div class="blog_h3"><span class="graybg">添加/删除事件</span></div>
<p>构造事件之后，在将其添加到 event_base 之前不能对其做任何操作。你需要调用：</p>
<pre class="crayon-plain-tag">// 如果tv==NULL则添加的事件不会超时
// 如果对已决事件调用该函数，则在tv后事件重新调度，此前保持未决状态
int event_add(struct event *ev, const struct timeval *tv);</pre>
<p>将事件加入event_base并使其进入未决（pending）状态。 </p>
<p>调用下面的函数，可以使事件变为非未决也非激活的：</p>
<pre class="crayon-plain-tag">int event_del(struct event *ev);</pre>
<div class="blog_h3"><span class="graybg">优先级</span></div>
<p>默认情况下，多个事件激活，其回调执行顺序是未知的。使用优先级，可以让某些事件优先执行：</p>
<pre class="crayon-plain-tag">int event_priority_set(struct event *event, int priority);</pre>
<p>注意：<span style="background-color: #c0c0c0;">多个不同优先级的事件同时激活时 ，低优先级的事件不会运行</span> 。libevent会执行高优先级的事件，然后<span style="background-color: #c0c0c0;">重新检查各个事件</span>。只有在没有高优先级的事件是激活的时候，低优先级的事件才有机会执行。</p>
<div class="blog_h3"><span class="graybg">检查事件状态</span></div>
<p>调用下面的函数，可以知晓目标事件是未决还是激活的：</p>
<pre class="crayon-plain-tag">int event_pending(const struct event *ev, short what, struct timeval *tv_out);</pre>
<div class="blog_h3"><span class="graybg">一次触发事件</span></div>
<p>执行下面的方法，可以调度一个”一次性“事件。</p>
<pre class="crayon-plain-tag">int event_base_once(struct event_base *, evutil_socket_t, short, event_callback_fn, void *, const struct timeval *);</pre>
<p>回调仅仅会执行一次。</p>
<div class="blog_h3"><span class="graybg">手工激活事件</span></div>
<p>即少数情况下，需要在条件不满足的情况下，强行触发一个事件</p>
<pre class="crayon-plain-tag">void event_active(struct event *ev, int what, short ncalls); </pre>
<div class="blog_h2"><span class="graybg">事件循环 </span></div>
<p>你一旦为<span style="background-color: #c0c0c0;">event_base注册了某些事件</span>， 你需要让libevent等待事件的发生，并在事件发生后进行通知。</p>
<div class="blog_h3"><span class="graybg">启动循环</span></div>
<p>调用下面的函数启动事件循环：</p>
<pre class="crayon-plain-tag">// 这些宏用作flags参数
// 阻塞，直到有事件激活，当所有事件的回调被执行后，退出事件循环
#define EVLOOP_ONCE             0x01
// 不阻塞，检查那些事件已经就绪，然后执行最高优先级的那些事件的回调，然后退出
#define EVLOOP_NONBLOCK         0x02
// 即使没有pending事件也不退出，必须调用event_base_loopexit()或event_base_loopbreak()来退出
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04

// 等待活动（Active，激活）事件，并且运行它们的回调，此函数是event_base_dispatch()更灵活的版本
// 默认情况下，此事件循环会运行event base直到：
// 1、没有任何pending或active事件
// 2、或者，event_base_loopbreak()或event_base_loopexit()被调用
// 返回值：如果正常退出返回0，否则返回-1
int event_base_loop(struct event_base *base, int flags);</pre>
<p>你也可以调用简化版本：</p>
<pre class="crayon-plain-tag">int event_base_dispatch(struct event_base *base);</pre>
<div class="blog_h3"><span class="graybg">停止循环 </span></div>
<p>根据启动循环时指定的flags的不同，循环可能在<span style="background-color: #c0c0c0;">没有任何pending事件（也就是已经移除所有已注册事件）</span>后自动结束。或者，你可以调用下面的函数提前结束： </p>
<pre class="crayon-plain-tag">// 在给定的时间tv后停止循环，如果tv为NULL立即停止
// 注意：如果 event_base 当前正在执行任何激活事件的回调，则回调会继续运行，直到运行完所有激活事件的回调之才停止
int event_base_loopexit(struct event_base *base,  struct timeval *tv);
// 立即退出循环，如果正在执行回调，当前回调完成后立即停止，不考虑尚未处理的事件
int event_base_loopbreak(struct event_base *base);</pre>
<div class="blog_h1"><span class="graybg">bufferevent</span></div>
<p>libevent提供了一种通用的缓冲机制 —— bufferevent。前面章节介绍的事件，在底层传输接口就绪后立即执行回调，而<span style="background-color: #c0c0c0;">bufferevent在读写足够多的数据之后才执行回调</span>。</p>
<p>bufferevent由<span style="background-color: #c0c0c0;">一个底层传输接口（例如套接字）、一个读缓冲区、一个写缓冲区</span>组成。缓冲区的类型是<pre class="crayon-plain-tag">struct evbuffer</pre>。</p>
<div class="blog_h2"><span class="graybg">回调和水位</span></div>
<p>默认情况下：</p>
<ol>
<li>从底层传输端口<span style="background-color: #c0c0c0;">读取了任意量的数据之后会调用读取回调</span></li>
<li> 输出缓冲区中<span style="background-color: #c0c0c0;">足够量的数据被刷到底层传输端口后写入回调会被调用</span></li>
</ol>
<p>通过调整“水位”，可以覆盖上述默认行为。每个bufferevent具有4个水位：</p>
<ol>
<li>读取低水位：读缓冲区数据量高于此水位后，读取回调被调用。默认值0，导致一旦缓冲区有数据，立即调用回调</li>
<li>读取高水位：读缓冲区数据量高于此水位后，bufferevent不再将数据读取到缓冲区。默认值无限</li>
<li>写入低水位：写缓冲区数据量低于此水位后，写入回调被调用。默认值0，导致仅当缓冲区全部写入到底层传输接口后，才调用写入回调继续写入</li>
<li>写入高水位：没有直接使用。当一个bufferevent用作另外一个bufferevent的底层传输接口时有用</li>
</ol>
<p>下面的函数可以调整bufferevent的读或/和写水位：</p>
<pre class="crayon-plain-tag">// 对于高水位，0表示无限
void bufferevent_setwatermark(struct bufferevent *bufev, short events, size_t lowmark, size_t highmark);</pre>
<div class="blog_h2"><span class="graybg">基于套接字创建</span></div>
<p>调用下面的方式，创建基于套接字的bufferevent：</p>
<pre class="crayon-plain-tag">enum bufferevent_options {
	// 释放 bufferevent 时关闭底层传输端口，例如：关闭底层套接字、释放底层bufferevent
	BEV_OPT_CLOSE_ON_FREE = (1&lt;&lt;0),

	// 自动为 bufferevent 分配锁，使之可线程安全的访问
	BEV_OPT_THREADSAFE = (1&lt;&lt;1),

	// 所有回调在事件循环中延迟的调用
	BEV_OPT_DEFER_CALLBACKS = (1&lt;&lt;2),

	// 执行回调时不对bufferevent进行锁定
	BEV_OPT_UNLOCK_CALLBACKS = (1&lt;&lt;3)
};

struct bufferevent *bufferevent_socket_new(
    // 关联到的event_base
    struct event_base *base,
    // 套接字的描述符，如果希望以后设置此描述符，传入-1
    evutil_socket_t fd,
    enum bufferevent_options options);</pre>
<div class="blog_h2"><span class="graybg">设置回调</span></div>
<p>调用bufferevent_setcb可以修改bufferevent的一个或多个回调：</p>
<ol>
<li>readcb：缓冲区的数据可读（考虑水位）时调用</li>
<li>writecb：文件描述符可以写入时（考虑水位）时调用</li>
<li>eventcb：文件描述符上发生事件时调用</li>
</ol>
<p>参数cbarg为传递给所有回调的参数。注意此参数被上述三种回调共享，修改它会影响所有回调。</p>
<p>要禁用回调，传输NULL作为回调函数。</p>
<pre class="crayon-plain-tag">typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
    short events, void *ctx);

void bufferevent_setcb(struct bufferevent *bufev,
    bufferevent_data_cb readcb, bufferevent_data_cb writecb,
    bufferevent_event_cb eventcb, void *cbarg);

void bufferevent_getcb(struct bufferevent *bufev,
    bufferevent_data_cb *readcb_ptr,
    bufferevent_data_cb *writecb_ptr,
    bufferevent_event_cb *eventcb_ptr,
    void **cbarg_ptr);</pre>
<div class="blog_h2"><span class="graybg">启禁事件</span></div>
<p>下面的函数用于启用、禁用某种事件：</p>
<pre class="crayon-plain-tag">void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);

short bufferevent_get_enabled(struct bufferevent *bufev);</pre>
<p>events可选EV_READ|EV_WRITE。如果没有启用读事件则bufferevent不会尝试读取数据到缓冲区，写事件类似。 </p>
<div class="blog_h2"><span class="graybg">启动连接</span></div>
<p>调用下面的方法可以向服务器发起一个连接：</p>
<pre class="crayon-plain-tag">int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);</pre>
<p>如果执行此调用时：</p>
<ol>
<li>尚未为bufferevent设置套接字，则自动创建非阻塞套接字。</li>
<li>已经为bufferevent设置套接字，则提示套接字尚未连接，直到连接成功前不应对其进行读写操作</li>
</ol>
<p>示例代码：</p>
<pre class="crayon-plain-tag">#include &lt;event2/event.h&gt;
#include &lt;event2/bufferevent.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;string.h&gt;

// 事件回调
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
    // 如果通过bufferevent_socket_connect()发起连接，将会收到BEV_EVENT_CONNECTED事件
    // 如果通过connect()连接，则报告为写入事件
    if (events &amp; BEV_EVENT_CONNECTED) {
         // 连接成功
    } else if (events &amp; BEV_EVENT_ERROR) {
         // 出现错误
    }
}

int main()
{
    struct event_base *base;
    struct bufferevent *bev;
    struct sockaddr_in sin;

    base = event_base_new();

    // 连接到服务器127.0.0.1:8080
    memset(&amp;sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(0x7f000001); 
    sin.sin_port = htons(8080);

    // 创建bufferevent
    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    // 设置回调
    bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);
    // 发起连接
    if (bufferevent_socket_connect(bev, (struct sockaddr *)&amp;sin, sizeof(sin)) &lt; 0) {
        // 出错，释放bufferevent
        bufferevent_free(bev);
        return -1;
    }

    // 启动事件循环
    event_base_dispatch(base);
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">读写数据</span> </div>
<div class="blog_h3"><span class="graybg">得到缓冲</span></div>
<p>调用下面的函数，可以获得bufferevent的读写缓冲区：</p>
<pre class="crayon-plain-tag">// 读缓冲
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
// 写缓冲
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);

// 注意，不得在这些缓冲区上设置回调</pre>
<div class="blog_h3"><span class="graybg">写入</span></div>
<pre class="crayon-plain-tag">// 将data的前size个字节写入输出缓冲区
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
// 移除buf中所有内容，将其放入输出缓冲区的尾部
int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">读取 </span></div>
<pre class="crayon-plain-tag">// 读取并移除输入缓冲区的最多size字节，并转储到data中，返回实际读取（移除）的字节数
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
// 抽取输入缓冲区中所有数据，存放到buf中
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">刷出</span></div>
<p>刷出（flush）读写缓冲，意味着需要尽可能的：</p>
<ol>
<li>从底层传输接口读取尽可能多的数据</li>
<li>将写缓冲区的数据尽可能写如底层传输接口 </li>
</ol>
<pre class="crayon-plain-tag">// iotype，可以是 EV_READ | EV_WRITE 表示需要读还是写
// state，BEV_FINISHED告知对端，没有更多数据需要发送了。BEV_NORMAL、BEV_FLUSH的含义依赖于bufferevent类型
int bufferevent_flush(struct bufferevent *bufev, short iotype, enum bufferevent_flush_mode state);</pre>
<div class="blog_h2"><span class="graybg">操控evbuffer</span></div>
<p>读写bufferevnet的数据，操作的主要类型是结构evbuffer。evbuffer实现了一种优化的字节队列，支持从尾部追加数据，或从头部移除数据。</p>
<p>evbuffer用于缓冲网络I/O的缓冲区的处理。IO调度、触发由bufferevent负责。</p>
<div class="blog_h3"><span class="graybg">创建缓冲</span></div>
<pre class="crayon-plain-tag">struct evbuffer *evbuffer_new(void);</pre>
<div class="blog_h3"><span class="graybg">删除缓冲</span></div>
<pre class="crayon-plain-tag">void evbuffer_free(struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">线程安全</span></div>
<p>默认情况下，在多线程中访问evbuffer是不安全的，除非调用下面的方法启用锁：</p>
<pre class="crayon-plain-tag">// 如果lock为NULL，则libevent利用evthread_set_lock_creation_callback创建锁
int evbuffer_enable_locking(struct evbuffer *buf, void *lock);</pre>
<p>要启用、禁用锁，调用下面的函数：</p>
<pre class="crayon-plain-tag">void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">检查长度</span></div>
<p>下面的函数返回evbuffer存储的字节数：</p>
<pre class="crayon-plain-tag">size_t evbuffer_get_length(const struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">添加数据</span></div>
<pre class="crayon-plain-tag">// 从data处开始，写入datlen字节数进去
int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);

// 格式化数据并全部写入
int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);</pre>
<div class="blog_h3"><span class="graybg">扩充缓冲</span></div>
<pre class="crayon-plain-tag">int evbuffer_expand(struct evbuffer *buf, size_t datlen);</pre>
<p>上述函数确保evbuffer能足以容纳datlen字节，而不需要更多的内存分配 </p>
<div class="blog_h3"><span class="graybg">在缓冲之间移动数据</span></div>
<p>libevent提供了将数据从一个缓冲移动到另外一个的快捷方式：</p>
<pre class="crayon-plain-tag">// 将src中的数据移动到dst的尾部，成功返回0
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
// 将src的datlen字节移动到dst的尾部
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst, size_t datlen); </pre>
<div class="blog_h2"><span class="graybg">释放</span></div>
<pre class="crayon-plain-tag">void bufferevent_free(struct bufferevent *bev);</pre>
<p>上面的函数用于释放bufferevent，bufferevent的内部具有引用计数机制，当上述函数调用时，尚有未决的延迟回调，则回调执行完毕之前不会删除bufferevent。</p>
<p>如果指定了BEV_OPT_CLOSE_ON_FREE，并且bufferevent使用套接字、其它bufferevent作为其底层传输接口，则它们会被一并关闭。 </p>
<div class="blog_h1"><span class="graybg">evconnlistener</span></div>
<p>在event2/listener.h中定义的连接监听器 —— evconnlistener，为libevent添加了监听/接受TCP连接的方法。</p>
<div class="blog_h2"><span class="graybg">创建</span></div>
<p>要创建新的连接监听器，调用：</p>
<pre class="crayon-plain-tag">struct evconnlistener * evconnlistener_new(
    // 关联的event_base
    struct event_base *base,
    // 新连接的回调，如果NULL则连接监听器是禁用的
    evconnlistener_cb cb, 
    // 传递给回调的参数
    void *ptr, 
    // 用于控制回调函数的行为
    unsigned flags, 
    // 尚未被接受的、排队的连接最大数量。如果为负数，libevent自动选择适当的值；如果为0，则认为提供的套接字的listen已经调用过
    int backlog,  
    // 监听套接字的FD
    evutil_socket_t fd
);

struct evconnlistener * evconnlistener_new_bind(struct event_base *base,
    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, 
    // 由libevent负责绑定监听套接字
    const struct sockaddr *sa, int socklen);


// 回调函数：
typedef void (*evconnlistener_cb)(struct evconnlistener *listener,
    // 新接受的套接字
    evutil_socket_t sock, 
    // 客户端地址和地址长度
    struct sockaddr *addr, int len, void *ptr); // ptr是先前提供的指针

// flags支持：

// 默认情况下，监听器接收到新连接后自动将其设置为非阻塞的。下面的标记禁用此行为
#define LEV_OPT_LEAVE_SOCKETS_BLOCKING	(1u&lt;&lt;0)
// 释放此连接监听器时，自动关闭底层套接字
#define LEV_OPT_CLOSE_ON_FREE		(1u&lt;&lt;1)
// 为底层套接字设置 close-on-exec 标志
#define LEV_OPT_CLOSE_ON_EXEC		(1u&lt;&lt;2)
// 某些平台在默认情况下，关闭某监听套接字后，需要经过一个超时，相同端口才可再次绑定，此选项禁用此行为，可以立即再次绑定
#define LEV_OPT_REUSEABLE		(1u&lt;&lt;3)
// 为连接监听器加锁，以便线程安全的访问
#define LEV_OPT_THREADSAFE		(1u&lt;&lt;4)
// 以禁用状态创建连接监听器，后续必须通过evconnlistener_enable()启用
#define LEV_OPT_DISABLED		(1u&lt;&lt;5)
// 提示libevent，如果可能的化，监听器应该延迟accept()连接，直到数据可用
#define LEV_OPT_DEFERRED_ACCEPT		(1u&lt;&lt;6)
// 提示允许多个服务器（进程/线程——绑定到相同端口
#define LEV_OPT_REUSEABLE_PORT		(1u&lt;&lt;7)</pre>
<p>连接监听器依赖于event_base，当监听套接字上有新的TCP连接后，event_base会通知evconnlistener。</p>
<div class="blog_h2"><span class="graybg">销毁</span></div>
<pre class="crayon-plain-tag">void evconnlistener_free(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">启禁</span></div>
<p>下面的函数可以禁止、重新允许连接监听器监听新连接：</p>
<pre class="crayon-plain-tag">int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">设置回调</span></div>
<pre class="crayon-plain-tag">void evconnlistener_set_cb(struct evconnlistener *lev, evconnlistener_cb cb, void *arg);</pre>
<div class="blog_h2"><span class="graybg">获取信息 </span></div>
<pre class="crayon-plain-tag">// 获取监听套接字
evutil_socket_t evconnlistener_get_fd(struct evconnlistener *lev);
// 获取关联的event_base
struct event_base *evconnlistener_get_base(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">错误检测</span></div>
<p>当监听器accept失败后，可以调用预先设置的错误回调：</p>
<pre class="crayon-plain-tag">typedef void (*evconnlistener_errorcb)(struct evconnlistener *lis, void *ptr);
void evconnlistener_set_error_cb(struct evconnlistener *lev, evconnlistener_errorcb errorcb);</pre>
<div class="blog_h1"><span class="graybg">示例</span></div>
<div class="blog_h2"><span class="graybg">Echo服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;stdio.h&gt;
#include &lt;signal.h&gt;
#ifndef _WIN32
#include &lt;netinet/in.h&gt;
# ifdef _XOPEN_SOURCE_EXTENDED
#  include &lt;arpa/inet.h&gt;
# endif
#include &lt;sys/socket.h&gt;
#endif

#include &lt;event2/bufferevent.h&gt;
#include &lt;event2/buffer.h&gt;
#include &lt;event2/listener.h&gt;
#include &lt;event2/util.h&gt;
#include &lt;event2/event.h&gt;

static const char MESSAGE[] = "Hello, World!\n";

static const int PORT = 8080;

static void listener_cb(struct evconnlistener *, evutil_socket_t, struct sockaddr *, int socklen, void *);
static void conn_writecb(struct bufferevent *, void *);
static void conn_eventcb(struct bufferevent *, short, void *);
static void signal_cb(evutil_socket_t, short, void *);

int main(int argc, char **argv) {
  struct event_base *base;
  struct evconnlistener *listener;
  struct event *signal_event;
  struct sockaddr_in sin;
  // 创建event_base
  base = event_base_new();
  if (!base) {
    // 初始化libevent失败
    return 1;
  }

  memset(&amp;sin, 0, sizeof(sin));
  sin.sin_family = AF_INET;
  sin.sin_port = htons(PORT);
  // 创建连接监听器
  listener = evconnlistener_new_bind(base, listener_cb, (void *) base,
                                     LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1,
                                     (struct sockaddr *) &amp;sin,
                                     sizeof(sin));

  if (!listener) {
    // 创建连接监听器失败
    return 1;
  }
  // 添加中断（Ctrl-C）信号事件
  signal_event = evsignal_new(base, SIGINT, signal_cb, (void *) base);

  if (!signal_event || event_add(signal_event, NULL) &lt; 0) {
    // 创建或添加信号事件失败
    return 1;
  }
  // 开始执行事件循环，直到Ctrl-C不会退出
  event_base_dispatch(base);

  evconnlistener_free(listener);  // 销毁监听器
  event_free(signal_event);       // 销毁事件
  event_base_free(base);          // 销毁event_base

  return 0;
}
// 接收到新连接请求
static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
                        struct sockaddr *sa, int socklen, void *user_data) {
  struct event_base *base = user_data;
  struct bufferevent *bev;
  // 为此连接套接字创建bufferevent
  bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
  if (!bev) {
    // 创建失败，退出事件循环
    event_base_loopbreak(base);
    return;
  }
  bufferevent_setcb(bev, NULL, conn_writecb, conn_eventcb, NULL); // 设置回调
  bufferevent_enable(bev, EV_WRITE); // 启用写事件，支持向套接字写数据
  bufferevent_disable(bev, EV_READ); // 禁用读事件
  // 将需要发送给客户端的数据填充到bufferevent的缓冲中
  // 当文件描述符（客户端连接）可写时自动刷出
  bufferevent_write(bev, MESSAGE, strlen(MESSAGE));
}
// 可以向底层连接写入数据了
static void conn_writecb(struct bufferevent *bev, void *user_data) {
  struct evbuffer *output = bufferevent_get_output(bev);
  if (evbuffer_get_length(output)==0) {
    // 输出缓冲已经自动刷空
    // 关闭bufferevent，这里会导致底层连接被关闭
    bufferevent_free(bev);
  }
}

static void conn_eventcb(struct bufferevent *bev, short events, void *user_data) {
  if (events &amp; BEV_EVENT_EOF) {
    // 连接被关闭
  } else if (events &amp; BEV_EVENT_ERROR) {
    // 连接上出现错误
  }
  bufferevent_free(bev);
}

static void signal_cb(evutil_socket_t sig, short events, void *user_data) {
  struct event_base *base = user_data;
  // 接收到信号，两秒后退出事件循环
  struct timeval delay = {2, 0};
  event_base_loopexit(base, &amp;delay);
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/libevent-study-note">libevent学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/libevent-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于C/C++的WebSocket库</title>
		<link>https://blog.gmem.cc/websocket-library-for-c-or-cpp</link>
		<comments>https://blog.gmem.cc/websocket-library-for-c-or-cpp#comments</comments>
		<pubDate>Tue, 19 Sep 2017 07:46:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16206</guid>
		<description><![CDATA[<p>libwebsockets 简介 libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括： 支持ws://和wss://协议 可以选择和OpenSSL、CyaSSL或者WolfSSL链接 轻量和高速，即使在每个线程处理多达250个连接的情况下 支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。 构建 [crayon-69d3dc89ab0a1274852113/] Echo示例 CMake项目配置 [crayon-69d3dc89ab0a5943489991/] 客户端 [crayon-69d3dc89ab0a7518285343/] 服务器 [crayon-69d3dc89ab0ab241194601/] 封装 为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。 客户端封装 [crayon-69d3dc89ab0ae119627007/] 使用客户端封装 <a class="read-more" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</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">libwebsockets</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括：</p>
<ol>
<li>支持ws://和wss://协议</li>
<li>可以选择和OpenSSL、CyaSSL或者WolfSSL链接</li>
<li>轻量和高速，即使在每个线程处理多达250个连接的情况下</li>
<li>支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv</li>
</ol>
<p>libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。</p>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/libwebsockets ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 2.8.9)
project(libws-study C)

include_directories(/home/alex/CPP/lib/libwebsockets/include)

set(CMAKE_CXX_FLAGS "-w -pthread")

set(SF_CLIENT client.c)
set(SF_SERVER server.c)

add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)


add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">#include "libwebsockets.h"
#include &lt;signal.h&gt;

static volatile int exit_sig = 0;
#define MAX_PAYLOAD_SIZE  10 * 1024

void sighdl( int sig ) {
    lwsl_notice( "%d traped", sig );
    exit_sig = 1;
}

/**
 * 会话上下文对象，结构根据需要自定义
 */
struct session_data {
    int msg_count;
    unsigned char buf[LWS_PRE + MAX_PAYLOAD_SIZE];
    int len;
};

/**
 * 某个协议下的连接发生事件时，执行的回调函数
 *
 * wsi：指向WebSocket实例的指针
 * reason：导致回调的事件
 * user 库为每个WebSocket会话分配的内存空间
 * in 某些事件使用此参数，作为传入数据的指针
 * len 某些事件使用此参数，说明传入数据的长度
 */
int callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:   // 连接到服务器后的回调
            lwsl_notice( "Connected to server\n" );
            break;

        case LWS_CALLBACK_CLIENT_RECEIVE:       // 接收到服务器数据后的回调，数据为in，其长度为len
            lwsl_notice( "Rx: %s\n", (char *) in );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:     // 当此客户端可以发送数据时的回调
            if ( data-&gt;msg_count &lt; 3 ) {
                // 前面LWS_PRE个字节必须留给LWS
                memset( data-&gt;buf, 0, sizeof( data-&gt;buf ));
                char *msg = (char *) &amp;data-&gt;buf[ LWS_PRE ];
                data-&gt;len = sprintf( msg, "你好 %d", ++data-&gt;msg_count );
                lwsl_notice( "Tx: %s\n", msg );
                // 通过WebSocket发送文本消息
                lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            }
            break;
    }
    return 0;
}

/**
 * 支持的WebSocket子协议数组
 * 子协议即JavaScript客户端WebSocket(url, protocols)第2参数数组的元素
 * 你需要为每种协议提供回调函数
 */
struct lws_protocols protocols[] = {
    {
        //协议名称，协议回调，接收缓冲区大小
        "", callback, sizeof( struct session_data ), MAX_PAYLOAD_SIZE,
    },
    {
        NULL, NULL,   0 // 最后一个元素固定为此格式
    }
};

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    // 用于创建vhost或者context的参数
    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    // 创建一个WebSocket处理器
    struct lws_context *context = lws_create_context( &amp;ctx_info );

    char *address = "192.168.0.89";
    int port = 9090;
    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", address, port &amp; 65535 );

    // 客户端连接参数
    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = address;
    conn_info.port = port;
    conn_info.ssl_connection = 0;
    conn_info.path = "/h264src";
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;

    // 下面的调用触发LWS_CALLBACK_PROTOCOL_INIT事件
    // 创建一个客户端连接
    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    while ( !exit_sig ) {
        // 执行一次事件循环（Poll），最长等待1000毫秒
        lws_service( context, 1000 );
        /**
         * 下面的调用的意义是：当连接可以接受新数据时，触发一次WRITEABLE事件回调
         * 当连接正在后台发送数据时，它不能接受新的数据写入请求，所有WRITEABLE事件回调不会执行
         */
        lws_callback_on_writable( wsi );
    }
    // 销毁上下文对象
    lws_context_destroy( context );

    return 0;
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">static int protocol0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_ESTABLISHED:       // 当服务器和客户端完成握手后
            break;
        case LWS_CALLBACK_RECEIVE:           // 当接收到客户端发来的帧以后
            // 判断是否最后一帧
            data-&gt;fin = lws_is_final_fragment( wsi );
            // 判断是否二进制消息
            data-&gt;bin = lws_frame_is_binary( wsi );
            // 对服务器的接收端进行流量控制，如果来不及处理，可以控制之
            // 下面的调用禁止在此连接上接收数据
            lws_rx_flow_control( wsi, 0 );

            // 业务处理部分，为了实现Echo服务器，把客户端数据保存起来
            memcpy( &amp;data-&gt;buf[ LWS_PRE ], in, len );
            data-&gt;len = len;

            // 需要给客户端应答时，触发一次写回调
            lws_callback_on_writable( wsi );
            break;
        case LWS_CALLBACK_SERVER_WRITEABLE:   // 当此连接可写时
            lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            // 下面的调用允许在此连接上接收数据
            lws_rx_flow_control( wsi, 1 );
            break;
    }
    // 回调函数最终要返回0，否则无法创建服务器
    return 0;
}

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = 9090;
    ctx_info.iface = NULL; // 在所有网络接口上监听
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    struct lws_context *context = lws_create_context( &amp;ctx_info );
    while ( !exit_sig ) {
        lws_service( context, 1000 );
    }
    lws_context_destroy( context );
}</pre>
<div class="blog_h2"><span class="graybg">封装</span></div>
<p>为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。</p>
<div class="blog_h3"><span class="graybg">客户端封装</span></div>
<pre class="crayon-plain-tag">#ifndef LIVE555_WSCLIENT_H
#define LIVE555_WSCLIENT_H

#include "libwebsockets.h"

#ifndef LWS_MAX_PAYLOAD_SIZE
#define LWS_MAX_PAYLOAD_SIZE  1024 * 1024
#endif

#ifndef SPDLOG_CONST
#define SPDLOG_CONST
const auto LOGGER = spdlog::stdout_color_st( "console" );
#endif

/**
 * 通用回调函数签名
 */
typedef void (*lws_callback)( struct lws *wsi, void *user, void *in, size_t len );

// 用户数据对象
typedef struct lws_user_data {
    // 缓冲区
    unsigned char *buf;
    // 缓冲区有效字节数
    int len;
    // 用户自定义数据
    void *user;
    // 读写缓冲区之前需要加锁
    volatile bool locked;
    // 指示当前缓冲区的数据的重要性，如果为真，发送之前不得被覆盖
    volatile bool critical;
    // 本次数据发送类型
    lws_write_protocol type;
    // 回调函数
    lws_callback esta_callback;
    lws_callback recv_callback;
    lws_callback writ_callback;
};

void writ_callback_send_buf( struct lws *wsi, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
        unsigned char *buf;
        char hex[128]= { 0 };
        int writ_count;

        int len = data-&gt;len;
        if ( len == 0 ) goto cleanup;

        buf = data-&gt;buf + LWS_PRE;
        writ_count = lws_write( wsi, buf, len, data-&gt;type );
        if ( data-&gt;type == LWS_WRITE_BINARY ) {
            char *phex = hex;
            for ( int i = 0; i &lt; 16; i++ ) {
                unsigned char c = *buf++;
                sprintf( phex, "%02x ", c );
                phex += 3;
            }
        }
        LOGGER-&gt;debug( "lws_write {} bytes: {}...", writ_count, hex );
        cleanup:
        data-&gt;locked = 0;
        data-&gt;critical = 0;
        data-&gt;len = 0;
    }
}

static int lws_protocol_0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:
            if ( data-&gt;esta_callback )data-&gt;esta_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_RECEIVE:
            if ( data-&gt;recv_callback )data-&gt;recv_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:
            if ( data-&gt;writ_callback )data-&gt;writ_callback( wsi, user, in, len );
            break;
    }
    return 0;
}

typedef struct lws_client {
    struct lws *wsi;
    struct lws_context *context;
    lws_user_data *data;
    int *cycle;

    // 连接参数
    char *address;
    char *path;
    int port;

    void (*fill_buf)( lws_client *client, void *buf, int len, lws_write_protocol type );

    void (*fire_writable)( lws_client *client );
};

void fill_buf( lws_client *client, void *buf, int len, lws_write_protocol type ) {
    lws_user_data *data = client-&gt;data;
    data-&gt;type = type;
    data-&gt;len = len;
    memcpy( data-&gt;buf + LWS_PRE, buf, len );
}

void fire_writable( lws_client *client ) {
    lws_callback_on_writable( client-&gt;wsi );
    // 停止当前事件循环等待
    lws_cancel_service( client-&gt;context );
}

void *lws_service_thread_func( void *arg ) {
    lws_client *client = (lws_client *) arg;

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    const struct lws_protocols protocols[] = {
        {
            "", lws_protocol_0_callback, sizeof( struct lws_user_data ), LWS_MAX_PAYLOAD_SIZE, 0, 0, LWS_MAX_PAYLOAD_SIZE
        },
        {
            NULL, NULL,                  0
        }
    };
    static const struct lws_extension exts[] = {
        {
            "permessage-deflate",
            lws_extension_callback_pm_deflate,
            "permessage-deflate; client_no_context_takeover; client_max_window_bits"
        },
        { NULL, NULL, NULL /* terminator */ }
    };
    ctx_info.protocols = protocols;
    ctx_info.extensions = exts;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    struct lws_context *context = lws_create_context( &amp;ctx_info );
    client-&gt;context = context;

    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", client-&gt;address, client-&gt;port &amp; 65535 );

    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = client-&gt;address;
    conn_info.port = client-&gt;port;
    conn_info.ssl_connection = 0;
    conn_info.path = client-&gt;path;
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;
    // 用户数据对象由调用者提供，因为需要提供回调
    conn_info.userdata = client-&gt;data;

    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    client-&gt;wsi = wsi;

    int *loop_cycle = client-&gt;cycle;
    int cycle = *loop_cycle;
    while ( *loop_cycle &gt;= 0 ) {
        lws_service( context, cycle );
    }
    lws_context_destroy( context );
}

/**
 * 连接到WebSocket服务器
 * @param address  IP地址
 * @param path  上下文路径URL
 * @param port 端口
 * @param data 用户数据
 * @param loop_cycle 事件循环周期，如果大于等于0则启动事件循环，后续将其置为-1则导致循环终止
 * @return
 */
lws_client *lws_connect( char *address, char *path, int port, lws_user_data *data, int loop_cycle ) {
    lws_client *client = (lws_client *) malloc( sizeof( lws_client ));
    client-&gt;data = data;
    client-&gt;cycle = (int *) malloc( sizeof( int ));
    *client-&gt;cycle = loop_cycle;
    client-&gt;address = address;
    client-&gt;path = path;
    client-&gt;port = port;
    client-&gt;fill_buf = fill_buf;
    client-&gt;fire_writable = fire_writable;
    pthread_t *lws_service_thread = (pthread_t *) malloc( sizeof( pthread_t ));
    pthread_create( lws_service_thread, NULL, lws_service_thread_func, client );
    return client;

}

#endif</pre>
<div class="blog_h3"><span class="graybg">使用客户端封装</span></div>
<pre class="crayon-plain-tag">// 创建用户数据对象
lws_user_data *data = new lws_user_data();
data-&gt;buf = new unsigned char[LWS_PRE + LWS_MAX_PAYLOAD_SIZE];
data-&gt;writ_callback = writ_callback_send_buf_bin;  // 注册回调

// 创建客户端
lws_client *ws_client = lws_connect( "192.168.0.89", "/h264src", 9090, data, 10 );

// 发送数据，需要同步
lws_user_data *data = client-&gt;data;
// GCC内置CAS语义
if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
    client-&gt;fill_buf( client, sink-&gt;recvBuf, frameSize );
    client-&gt;fire_writable( client );
    data-&gt;locked = 0;
}</pre>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">error on reading from skt : 104</span></div>
<p><a href="/network-faq#skt-enos">错误代码104</a>的含义是连接被重置，我遇到这个问题的原因是，Spring的WebSocket消息缓冲区大小不足。</p>
<div class="blog_h1"><span class="graybg">WebSocket++</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>WebSocket++是一个仅仅由头文件构成的C++库，它实现了WebSocket协议（RFC6455），通过它，你可以在C++项目中使用WebSocket客户端或者服务器。</p>
<p>WebSocket++使用两个可以相互替换的网络传输模块，其中一个基于C++ I/O流，另一个基于Asio。</p>
<p>WebSocket++的主要特性包括：</p>
<ol>
<li>事件驱动的接口</li>
<li>支持WSS、IPv6</li>
<li>灵活的依赖管理 —— Boost或者C++ 11标准库</li>
<li>可移植性：Posix/Windows、32/64bit、Intel/ARM/PPC</li>
<li>线程安全</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone https://github.com/zaphoyd/websocketpp.git
cd websocketpp
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/websocketpp ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(websocket__)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-pthread")
add_definitions(-D_WEBSOCKETPP_CPP11_FUNCTIONAL_)
add_definitions(-D_WEBSOCKETPP_CPP11_THREAD_)
add_definitions(-D_WEBSOCKETPP_CPP11_SYSTEM_ERROR_)
add_definitions(-D_WEBSOCKETPP_CPP11_MEMORY_)


include_directories(/home/alex/CPP/lib/websocketpp/include /home/alex/CPP/lib/boost/1.65.1/include/)

set(SF_CLIENT client.cpp)
add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)

set(SF_SERVER server.cpp)
add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)</pre>
<div class="blog_h3"><span class="graybg">客户端 </span></div>
<pre class="crayon-plain-tag">#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;

#include &lt;websocketpp/client.hpp&gt;

#include &lt;iostream&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; client;

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

// 消息指针
typedef websocketpp::config::asio_client::message_type::ptr message_ptr;

// 打开连接时的回调
void on_open( client *c, websocketpp::connection_hdl hdl ) {
    std::string msg = "Hello 1";
    // 发送文本消息
    c-&gt;send( hdl, msg, websocketpp::frame::opcode::text );
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Tx: " + msg );

}

// 连接失败时的回调
void on_fail( client *c, websocketpp::connection_hdl hdl ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Connection Failed" );
}

// 接收到服务器发来的WebSocket消息后的回调
void on_message( client *c, websocketpp::connection_hdl hdl, message_ptr msg ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Rx: " + msg-&gt;get_payload());
    // 关闭连接，导致事件循环退出
    c-&gt;close( hdl, websocketpp::close::status::normal, "" );
}

// 关闭连接时的回调
void on_close( client *c, websocketpp::connection_hdl hdl ) {
}

int main( int argc, char *argv[] ) {
    client echo_client;

    // 调整日志策略
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_header );
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_payload );

    std::string uri = "ws://192.168.0.89:9090/h264src";

    try {
        // 初始化ASIO ASIO
        echo_client.init_asio();

        // 注册回调函数
        echo_client.set_open_handler( std::bind( &amp;on_open, &amp;echo_client, ::_1 ));
        echo_client.set_fail_handler( std::bind( &amp;on_fail, &amp;echo_client, ::_1 ));
        echo_client.set_message_handler( std::bind( &amp;on_message, &amp;echo_client, ::_1, ::_2 ));
        echo_client.set_close_handler( std::bind( &amp;on_close, &amp;echo_client, ::_1 ));

        // 在事件循环启动前创建一个连接对象
        websocketpp::lib::error_code ec;
        client::connection_ptr con = echo_client.get_connection( uri, ec );
        echo_client.connect( con );
        con-&gt;get_handle(); // 连接句柄，发送消息时必须要传入

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_client.run();
    } catch ( const std::exception &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( websocketpp::lib::error_code e ) {
        std::cout &lt;&lt; e.message() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;

#include &lt;websocketpp/config/asio_no_tls.hpp&gt;
#include &lt;websocketpp/server.hpp&gt;

typedef websocketpp::server&lt;websocketpp::config::asio&gt; server;
typedef websocketpp::config::asio::message_type::ptr message_ptr;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

void on_open( server *s, websocketpp::connection_hdl hdl ) {
    // 根据连接句柄获得连接对象
    server::connection_ptr con = s-&gt;get_con_from_hdl( hdl );
    // 获得URL路径
    std::string path = con-&gt;get_resource();
    s-&gt;get_alog().write( websocketpp::log::alevel::app, "Connected to path " + path );
}

void on_message( server *s, websocketpp::connection_hdl hdl, message_ptr msg ) {
    s-&gt;send( hdl, msg-&gt;get_payload(), websocketpp::frame::opcode::text );
}

int main() {
    server echo_server;
    // 调整日志策略
    echo_server.set_access_channels( websocketpp::log::alevel::all );
    echo_server.clear_access_channels( websocketpp::log::alevel::frame_payload );

    try {
        echo_server.init_asio();

        echo_server.set_open_handler( bind( &amp;on_open, &amp;echo_server, ::_1 ));
        echo_server.set_message_handler( bind( &amp;on_message, &amp;echo_server, ::_1, ::_2 ));
        // 在所有网络接口的9090上监听
        echo_server.listen( 9090 );

        // 启动服务器端Accept事件循环
        echo_server.start_accept();

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_server.run();
    } catch ( websocketpp::exception const &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/websocket-library-for-c-or-cpp/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>使用Jansson处理JSON</title>
		<link>https://blog.gmem.cc/json-processing-with-jansson</link>
		<comments>https://blog.gmem.cc/json-processing-with-jansson#comments</comments>
		<pubDate>Thu, 31 Aug 2017 04:36:06 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[JSON]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15678</guid>
		<description><![CDATA[<p>简介 Jansson是一个用于解码、编码、操控JSON的C库。其特性为： 简单直观的API和数据模型 没有依赖项 完整的Unicode支持（UTF-8） 构建 Jansson使用cmake作为构建工具。可以执行下面的命令构建： [crayon-69d3dc89ab5f1682812890/] 上述命令执行后，Jansson的头文件和静态库被安装到/home/alex/CPP/lib/jansson/2.9目录 使用 这里我们创建一个CMake工程： [crayon-69d3dc89ab5f4068664080/] 解码JSON 下面的代码展示了如何把字符串解码为json_t结构： [crayon-69d3dc89ab5f7443037756/] 运行上述代码后输出： [crayon-69d3dc89ab5f9246144325/] 编码JSON 下面的代码展示了如何把json_t结构编码为字符串： [crayon-69d3dc89ab5fb548444592/] 运行上述代码后输出： [crayon-69d3dc89ab5fe192790332/] <a class="read-more" href="https://blog.gmem.cc/json-processing-with-jansson">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/json-processing-with-jansson">使用Jansson处理JSON</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="http://www.digip.org/jansson/">Jansson</a>是一个用于解码、编码、操控JSON的C库。其特性为：</p>
<ol>
<li>简单直观的API和数据模型</li>
<li>没有依赖项</li>
<li>完整的Unicode支持（UTF-8）</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>Jansson使用cmake作为构建工具。可以执行下面的命令构建：</p>
<pre class="crayon-plain-tag">git clone https://github.com/akheron/jansson
cd jansson
mkdir build &amp;&amp; cd build
cmake -DCMAKE_INSTALL_PREFIX:STRING=/home/alex/CPP/lib/jansson/2.9 -DJANSSON_BUILD_DOCS=OFF ..
cmake --build . -- install</pre>
<p>上述命令执行后，Jansson的头文件和静态库被安装到/home/alex/CPP/lib/jansson/2.9目录</p>
<div class="blog_h2"><span class="graybg">使用</span></div>
<p>这里我们创建一个CMake工程：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(jansson_demo C)
set(SOURCE_FILES main.c)
include_directories(/home/alex/CPP/lib/jansson/2.9/include)
add_executable(jansson_demo ${SOURCE_FILES})
set_property(TARGET jansson_demo PROPERTY C_STANDARD 99)
target_link_libraries(jansson_demo /home/alex/CPP/lib/jansson/2.9/lib/libjansson.a)</pre>
<div class="blog_h3"><span class="graybg">解码JSON</span></div>
<p>下面的代码展示了如何把<span style="background-color: #c0c0c0;">字符串解码为json_t</span>结构：</p>
<pre class="crayon-plain-tag">#include &lt;jansson.h&gt;
#include &lt;assert.h&gt;

int main() {
    char *text = "{ \"name\":\"汪震\", \"age\":30, \"children\":[ \"彩彩\", \"当当\" ] }";
    json_error_t error;
    // json_t用于引用任何JSON节点
    json_t *root = json_loads( text, 0, &amp;error );
    // 如果读取失败，自动置为空指针
    if ( !root ) {
        fprintf( stderr, "error: on line %d: %s\n", error.line, error.text );
        return 1;
    } else {
        // json_is_*宏用于判断数据类型
        // 处理JSON对象
        assert( json_is_object( root ));
        json_t *name, *age, *children;
        name = json_object_get( root, "name" );
        age = json_object_get( root, "age" );
        fprintf( stdout, "NAME: %s, AGE: %d\n", json_string_value( name ), json_integer_value( age ));
        children = json_object_get( root, "children" );
        // 处理JSON数组
        assert( json_is_array( children ));
        int sof = json_array_size( children );
        for ( int i = 0; i != sof; i++ ) {
            json_t *child = json_array_get( children, i );
            fprintf( stdout, "CHILDREN: %s\n", json_string_value( child ));
        }
        // 减小引用计数，导致资源回收
        json_decref( root );
    }
    return 0;
}</pre>
<p>运行上述代码后输出：</p>
<pre class="crayon-plain-tag">NAME: 汪震, AGE: 30
CHILDREN: 彩彩
CHILDREN: 当当</pre>
<div class="blog_h3">
<div class="blog_h3"><span class="graybg">编码JSON</span></div>
</div>
<p>下面的代码展示了如何把<span style="background-color: #c0c0c0;">json_t结构编码为字符串</span>：</p>
<pre class="crayon-plain-tag">json_t *root = json_pack( "{sssis[ss]}", "name", "汪震", "age", 30, "children", "彩彩", "当当" );
char *text = json_dumps( root, JSON_INDENT( 4 ) | JSON_SORT_KEYS );
fprintf( stdout, "%s\n", text );</pre>
<p>运行上述代码后输出：</p>
<pre class="crayon-plain-tag">{
    "age": 30,
    "children": [
        "彩彩",
        "当当"
    ],
    "name": "汪震"
}</pre>
<div class="blog_h3"><span class="graybg">打包JSON</span></div>
<p>下面的代码展示了如何把<span style="background-color: #c0c0c0;">零散数据打包（pack）为json_t</span>结构：</p>
<pre class="crayon-plain-tag">assert( json_is_integer( json_pack( "i", 1 )));
assert( json_is_array(
        // jansson使用格式限定符来指定要创建的JSON的JSON节点的类型
        // 下面的例子创建三元素的数组：字符串、字符串、布尔值
        json_pack( "[ssb]", "foo", "bar", 1 )
));
/* 打包一个空对象 */
json_pack( "{}" );
/* 打包出 {age:30,childNum:2} */
json_pack( "{sisi}", "age", 30, "childNum", 2 );
/* 打包出 [[1, 2], {"num": true}] */
json_pack( "[[i,i],{s:b}]", 1, 2, "num", 1 );
return 0;</pre>
<p>可用的格式限定符如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 12%; text-align: center;">限定符</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>s</td>
<td>将null结尾的UTF-8字符串转换为JSON字符串</td>
</tr>
<tr>
<td>s?</td>
<td>和上面类似，但是如果位置参数是NULL则插入JSON null值</td>
</tr>
<tr>
<td>s#</td>
<td>对应两个位置参数const char *, int，将指定长度的UTF-8缓冲转换为JSON字符串</td>
</tr>
<tr>
<td>s%</td>
<td>和上面类似，只是未知参数类型为const char *, size_t</td>
</tr>
<tr>
<td>+</td>
<td>把此位置参数连接到前一个位置参数后面</td>
</tr>
<tr>
<td>+#</td>
<td>和上面类似，接收位置参数const char *, int</td>
</tr>
<tr>
<td>+%</td>
<td>和上面类似，接收位置参数const char *, size_t</td>
</tr>
<tr>
<td>n</td>
<td>输出null，不消耗位置参数 </td>
</tr>
<tr>
<td>b </td>
<td>转换int为JSON布尔值</td>
</tr>
<tr>
<td>i </td>
<td>转换int为JSON整数</td>
</tr>
<tr>
<td>I </td>
<td>转换json_int_t为JSON整数</td>
</tr>
<tr>
<td>f</td>
<td>转换double为JSON实数 </td>
</tr>
<tr>
<td>o</td>
<td>原样插入json_t*结构</td>
</tr>
<tr>
<td>O </td>
<td>和上面类似，但是目标json_t的引用计数会增加</td>
</tr>
<tr>
<td> o?     o?</td>
<td>类似上面两个，但是当位置参数为NULL时插入null</td>
</tr>
<tr>
<td>[fmt]</td>
<td>使用内部的格式限定符构建一个JSON数组，fmt可以是任意符合要求的格式限定符序列</td>
</tr>
<tr>
<td>{fmt}</td>
<td> 使用内部的格式限定符构建一个JSON对象，fmt可以是任意符合要求的格式限定符序列</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">解包JSON</span></div>
<p>下面的代码展示了如何把<span style="background-color: #c0c0c0;">json_t结构解包为零散数据</span>： </p>
<pre class="crayon-plain-tag">json_t *root = json_pack( "{sssis[ss]}", "name", "汪震", "age", 30, "children", "彩彩", "当当" );
char *name;
int age;
// 可以仅仅解包一部分
json_unpack( root, "{sssi}", "name", &amp;name, "age", &amp;age );
fprintf( stdout, "NAME: %s, AGE:%d\n", name, age );</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/json-processing-with-jansson">使用Jansson处理JSON</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/json-processing-with-jansson/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>CLion知识集锦</title>
		<link>https://blog.gmem.cc/clion-faq</link>
		<comments>https://blog.gmem.cc/clion-faq#comments</comments>
		<pubDate>Wed, 13 Jan 2016 03:55:09 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[IntelliJ]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=11699</guid>
		<description><![CDATA[<p>简介 CLion是基于Intellij平台的IDE，主要用于C/C++开发。它使用CMake工程模型，你对CMakeLists.txt的任何更改都会反映到IDE中，CLion调用CMake的命令行完成工程构建。 基础 全局设置 设置路径 说明 B,E,D ⇨ Toolchains 工具链设置，可以指定CMake、GDB的路径，在Windows下你还可以指定MinGW或者Cygwin的安装位置 B,E,D ⇨ CMake 勾选Automatically Reload CMake project on editing Generation CMake Options，指定传递给cmake命令行的选项，例如通过[crayon-69d3dc89ab93f188309729-i/] 传递变量值 <a class="read-more" href="https://blog.gmem.cc/clion-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/clion-faq">CLion知识集锦</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>CLion是基于Intellij平台的IDE，主要用于C/C++开发。它使用CMake工程模型，你对CMakeLists.txt的任何更改都会反映到IDE中，CLion调用CMake的命令行完成工程构建。</p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">全局设置</span></div>
<table class=" full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">设置路径</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>B,E,D ⇨ Toolchains</td>
<td>工具链设置，可以指定CMake、GDB的路径，在Windows下你还可以指定MinGW或者Cygwin的安装位置</td>
</tr>
<tr>
<td>B,E,D ⇨ CMake</td>
<td>
<p>勾选Automatically Reload CMake project on editing</p>
<p><span style="background-color: #c0c0c0;">Generation</span></p>
<p>CMake Options，指定传递给cmake命令行的选项，例如通过<pre class="crayon-plain-tag">-Dvar=name</pre> 传递变量值</p>
<p>展开Pass System Environment，可以覆盖系统环境变量。勾选Pass system environment variables可以传递系统环境变量到CMake的Generation阶段</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">和CMake协同</span></div>
<p>当通过CLion创建新工程时，CMakeLists.txt会自动创建。当工程结构复杂时，你可以手工为子目录创建CMakeLists.txt文件。</p>
<p>当你打开不是基于CMake的工程时，CLion允许做的事情很少——不能编辑代码、不能构建或运行。这种情况下你可以使用CMake的导入功能或者手工创建CMakeLists.txt文件。</p>
<p>手工修改CMakeLists.txt后，需要Reload工程才能体现出修改。当需要Reload时CMake工具窗口会有提示。你还可以启用Auto-Reload。</p>
<div class="blog_h3"><span class="graybg">重置CMake缓存</span></div>
<p>如果需要在构建前清空CMake的缓存条目，可以定位到CMake工具窗口 ⇨ Cache选项卡，点击<img class="aligncenter size-full wp-image-11786 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2016/01/Selection_006.png" alt="Selection_006" width="19" height="19" />按钮。</p>
<div class="blog_h3"><span class="graybg">修改构建配置</span></div>
<p>点击<img class="aligncenter size-full wp-image-11787 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2016/01/Selection_007.png" alt="Selection_007" width="98" height="21" />右侧的向下箭头，选择Edit Configurations，可以修改某个条目使用的构建配置，默认支持CMake的四种构建配置。</p>
<p>你可以在CMakeLists.txt中添加自定义的构建配置：</p>
<pre class="crayon-plain-tag"># 必须是第一个变量定义
set(CMAKE_CONFIGURATION_TYPES "CustomType1;CustomType2" CACHE STRING "" FORCE)</pre>
<div class="blog_h3"><span class="graybg">修改工程根目录</span></div>
<p>打开既有CMake工程或者创建新工程后，默认的CLion把CMakeLists.txt所在目录作为工程根目录（Project root directory）看待，要改变此行为，可以Tools ⇨ CMake ⇨ Change Project Root。</p>
<div class="blog_h3"><span class="graybg">CMakeLists.txt基本命令</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>include_directories</td>
<td>
<p>指定头文件的搜索位置。头文件不但对编译是必须的，也可以被CLion索引，以提供代码自动完成、代码导航</p>
<p>依据操作系统的不同，编译器会自动搜索一些预定义的位置，你可以可以手工添加：</p>
<pre class="crayon-plain-tag"># 可选的BEFORE/AFTER关键字用于控制搜索顺序
include_directories(BEFORE ${MY_SOURCE_DIR}/src )</pre>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>set</td>
<td>设置一系列变量的值：<br />
<pre class="crayon-plain-tag"># 设置C编译器的位置
set (CMAKE_CXX_COMPILER, "C:\\MinGW\\bin\\g++")
# 启用C99标准
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
# 启用C++ 11标准
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# 启用警告
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall")</pre>
</td>
</tr>
<tr>
<td>add_executable</td>
<td>添加可执行的构建目标</td>
</tr>
<tr>
<td>add_library</td>
<td>添加库：<br />
<pre class="crayon-plain-tag">add_library (my_library STATIC|SHARED|MODULE ${SOURCE_FILES})</pre>
</td>
</tr>
<tr>
<td>target_link_libraries</td>
<td>包含链接所需的库：<br />
<pre class="crayon-plain-tag"># 使用BOOST库
find_package(Boost)
IF (Boost_FOUND)
    include_directories(${Boost_INCLUDE_DIR})
endif()
set (Boost_USE_STATIC_LIBS OFF) # enable dynamic linking
set (Boost_USE_MULTITHREAD ON)  # enable multithreading
find_package (Boost COMPONENTS REQUIRED chrono filesystem)
# 声明链接到BOOST库
target_link_libraries (my_target ${Boost_LIBRARIES})</pre>
</td>
</tr>
<tr>
<td>add_subdirectory</td>
<td>
<p>用于包含子工程。</p>
<p>一个工程可以依赖于其它工程，CMake没有类似于VS的解决方案（Solution）的概念，但是它允许你手工定义工程之间的依赖关系</p>
<p>典型的，你希望在工作区中这样管理多工程（Multi-project）结构：</p>
<ol>
<li>打开主工程时，其依赖的工程一并打开</li>
<li>主工程的设置自动应用到被依赖的工程</li>
<li>重构、代码完成等可以影响到所有工程</li>
</ol>
<p>上面的三点需求可以通过合适的CMakeList.txt完成，你需要把上述<span style="background-color: #c0c0c0;">所有工程组织到CMakeList.txt所在目录之下</span>，形成树形结构，每个子目录对应一个子工程，并且子目录有自己的CMakeLists.txt。最后，在根目录的CMakeLists.txt中添加：</p>
<pre class="crayon-plain-tag">add_subdirectory (project1) # 把project1包含到主工程
add_subdirectory (project2) # 把project2包含到主工程</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg"><a id="debug"></a>调试</span></div>
<div class="blog_h2"><span class="graybg">本地调试</span></div>
<div class="blog_h3"><span class="graybg">Attach到进程</span></div>
<p>点击 Run ⇨ Attach to Process... 可以调试运行中的进程。当Debug工具窗口提示Debugger attached to process ...后，<span style="background-color: #c0c0c0;">点击左侧的Pause Program按钮，这时GDB窗格支持输入</span>。
<p>你可能需要设置源码搜索路径，否则设置断点时GDB窗格提示No source file named /home/alex/CPP/project ...要设置源码搜索路径，在GDB窗格中输入set directories命令，示例：</p>
<pre class="crayon-plain-tag">set directories /home/alex/CPP/projects/clion/envoy</pre>
<div class="blog_h3"><span class="graybg">启动并调试程序</span></div>
<p>你可以启动任意应用程序并调试，此程序不一定是CLion通过Cmake构建出来的。</p>
<p>点击 Run ⇨ Edit Configurations，添加一个Application类型的配置，Executable选择你需要调试的应用程序的绝对路径：</p>
<p><img class="aligncenter  wp-image-25049" src="https://blog.gmem.cc/wp-content/uploads/2016/01/Run-Debug-Configurations.png" alt="run-debug-configurations" width="1012" height="645" /></p>
<p>注意：把Before launch下面的Build项去掉。</p>
<p>然后点击调试按钮即可。</p>
<div class="blog_h2"><span class="graybg">远程调试</span></div>
<div class="blog_h3"><span class="graybg">启动gdbserver</span></div>
<p>脚本示例：</p>
<pre class="crayon-plain-tag">#!/usr/bin/env bash
gdbserver --once localhost:2345 envoy ...</pre>
<div class="blog_h3"><span class="graybg">Clion配置</span></div>
<p>配置示例截图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/01/Run-Debug-Configuration-Remote.png"><img class="aligncenter size-full wp-image-25305" src="https://blog.gmem.cc/wp-content/uploads/2016/01/Run-Debug-Configuration-Remote.png" alt="run-debug-configuration-remote" width="933" height="635" /></a></p>
<div class="blog_h2"><span class="graybg">路径映射</span></div>
<p>基于gdbserver进行远程调试时，特别需要注意路径映射的问题。</p>
<p>Remote端使用（符号表中的）相对路径貌似无效，确定决定路径，可以按照如下步骤：</p>
<ol>
<li>使用gdb连接到gdbserver：<br />
<pre class="crayon-plain-tag">(gdb) target remote localhost:2345</pre>
</li>
<li>
<p>打印源码列表：</p>
<pre class="crayon-plain-tag">(gdb) info sources
/proc/self/cwd/source/exe/main_common.cc ...
/proc/self/cwd/bazel-out/k8-dbg/bin/include/envoy/common ...
/home/alex/.cache/bazel/_bazel_alex/3a80e9c345550f05d77beb52ded4f5f3/external/envoy_deps_cache_2c744dffd279d7e9e0910ce594eb4f4f/libevent.dep.build/libevent-release-2.1.8-stable/build/../ratelim-internal.h</pre>
</li>
<li>
<p>通过打印当前栈追踪，查看符号表中的源码路径：
<pre class="crayon-plain-tag">(gdb) where
#0  epoll_wait (epfd=&lt;optimized out&gt;, events=events@entry=0x2e00a80, maxevents=&lt;optimized out&gt;, timeout=timeout@entry=100) at ../epoll_sub.c:66
#1  0x00000000010b7709 in epoll_dispatch (base=0x2e70000, tv=&lt;optimized out&gt;) at ../epoll.c:462
#2  0x00000000010ad8dd in event_base_loop (base=0x2e70000, flags=0) at ../event.c:1947
#3  0x0000000000a5067c in Envoy::Event::DispatcherImpl::run (this=0x2d9d3f0, type=Envoy::Event::Dispatcher::RunType::Block) at source/common/event/dispatcher_impl.cc:165
#4  0x00000000009d74c5 in Envoy::Server::InstanceImpl::run (this=0x2e2d200) at source/server/server.cc:466
#5  0x0000000000425baa in Envoy::MainCommonBase::run (this=0x2e2c8b0) at source/exe/main_common.cc:103
#6  0x000000000040cd68 in Envoy::MainCommon::run (this=0x2e2c480) at bazel-out/k8-dbg/bin/source/exe/_virtual_includes/envoy_main_common_lib/exe/main_common.h:86
#7  0x000000000040a4bf in main (argc=19, argv=0x7fffffffd238) at source/exe/main.cc:37 </pre>
</li>
<li>
<p> 可以看到source目录对应到的是/proc/self/cwd/source目录。假设Debugger端的源码路径是/home/alex/CPP/projects/clion/envoy/source，则需要建立两者的路径映射
</li>
</ol>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">如何执行安装</span></div>
<p>CLion默认指定的构建树的位置，可以在Messages窗口看到，通过CLion执行一次构建，可以看到类似：</p>
<pre class="crayon-plain-tag">bin/cmake --build /home/alex/.CLion2016.1/system/cmake/generated/sds-1b689bf9/1b689bf9/Debug --target sds -- -j 8</pre>
<p>的消息，你只需要CD到<pre class="crayon-plain-tag">--build</pre> 后面跟着的目录，就可以执行<pre class="crayon-plain-tag">make install</pre> 进行安装了，当然前提是在CMakeLists.txt中声明合适的install命令。</p>
<div class="blog_h3"><span class="graybg">调试时对象无法展开</span></div>
<p>也无法调用对象的方法、查看其字段值。</p>
<p>出现此问题的原因是，目标对象在编译时没有开启调试信息。如果使用GCC，尝试添加编译参数-O0 -g3。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/clion-faq">CLion知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/clion-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>CMake学习笔记</title>
		<link>https://blog.gmem.cc/cmake</link>
		<comments>https://blog.gmem.cc/cmake#comments</comments>
		<pubDate>Mon, 11 May 2015 08:09:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[IntelliJ]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=11488</guid>
		<description><![CDATA[<p>基础知识 CMake简介 CMake是一个开源的可扩展工具，用于独立于编译器的管理构建过程。CMake必须和本地构建系统联合使用，在每个源码目录中，需要编写CMakeLists.txt文件，以声明如何生成标准的构建文件（例如GNU Make的Makefiles，或者MSVS的解决方案）。 CMake支持所有平台的内部构建（in-source build）和外部构建（out-of-source build）。内部构建的源码目录和二进制目录为同一目录，即CMake会改变源码目录的内容。通过外部构建，可以针对单个源码树进行多重构建（Multiple builds ）。 CMake会生成一个方便用户编辑的缓存文件，当其运行时，会定位头文件、库、可执行文件，这些信息被收集到缓存文件中。用户可以在生成本地构建文件之前编辑它。 CMake命令行支持自动或者交互式的运行。CMake还提供了一个基于QT的GUI，其名称为cmake-gui。注意此GUI同样依赖于环境变量的正确设置。 基本语法 CMakeLists.txt包含一系列的命令，每个命令都是[crayon-69d3dc89ac55e758247252-i/] 的形式，多个参数使用空白符分隔。CMake提供了很多预定义命令，你可以方便的扩展自己的命令。 CMake支持简单的变量，它们或者是字符串，或者是字符串的列表。引用一个变量的语法是[crayon-69d3dc89ac562661458636-i/] 。 如果向一个命令传递列表变量，效果等同于向它逐个传递列表成员： [crayon-69d3dc89ac564881328484/] 要把一个列表变量作为整体传递，只需要加上双引号即可：  [crayon-69d3dc89ac566591320696/]  CMake可以直接访问环境变量和Windows注册表，前者使用语法[crayon-69d3dc89ac568853966235-i/] ，后者使用语法[crayon-69d3dc89ac56a554041013-i/] 。示例： [crayon-69d3dc89ac56c261136946/] CMake的优势 支持多个底层构建工具，例如GNU <a class="read-more" href="https://blog.gmem.cc/cmake">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/cmake">CMake学习笔记</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">CMake简介</span></div>
<p>CMake是一个开源的可扩展工具，用于<span style="background-color: #c0c0c0;">独立于编译器</span>的管理构建过程。CMake<span style="background-color: #c0c0c0;">必须和本地构建系统联合</span>使用，在每个源码目录中，需要编写<span style="background-color: #c0c0c0;">CMakeLists.txt</span>文件，以声明如何生成标准的构建文件（例如GNU Make的Makefiles，或者MSVS的解决方案）。</p>
<p>CMake支持所有平台的内部构建（in-source build）和外部构建（out-of-source build）。内部构建的源码目录和二进制目录为同一目录，即CMake会改变源码目录的内容。通过外部构建，可以针对单个源码树进行<span style="background-color: #c0c0c0;">多重构建</span>（Multiple builds ）。</p>
<p>CMake会生成一个方便用户编辑的缓存文件，当其运行时，会定位头文件、库、可执行文件，这些信息被收集到缓存文件中。用户可以在生成本地构建文件之前编辑它。</p>
<p>CMake命令行支持自动或者交互式的运行。CMake还提供了一个基于QT的GUI，其名称为cmake-gui。注意此GUI同样依赖于环境变量的正确设置。</p>
<div class="blog_h3"><span class="graybg">基本语法</span></div>
<p>CMakeLists.txt包含一系列的命令，每个命令都是<pre class="crayon-plain-tag">COMMAND (args…)</pre> 的形式，多个参数使用空白符分隔。CMake提供了很多预定义命令，你可以方便的扩展自己的命令。</p>
<p>CMake支持简单的变量，它们或者是<span style="background-color: #c0c0c0;">字符串</span>，或者是<span style="background-color: #c0c0c0;">字符串的列表</span>。引用一个变量的语法是<pre class="crayon-plain-tag">${VAR_NAME}</pre> 。</p>
<p>如果向一个命令传递列表变量，效果等同于向它<span style="background-color: #c0c0c0;">逐个传递</span>列表成员：</p>
<pre class="crayon-plain-tag">set(V 1 2 3)    # V的值是1 2 3
command(${V})   # 等价于command(1 2 3)</pre>
<p>要把一个列表变量作为整体传递，只需要加上<span style="background-color: #c0c0c0;">双引号</span>即可： </p>
<pre class="crayon-plain-tag">command("${V}")   # 等价于command("1 2 3")</pre>
<p> CMake可以直接访问<span style="background-color: #c0c0c0;">环境变量</span>和<span style="background-color: #c0c0c0;">Windows注册表</span>，前者使用语法<pre class="crayon-plain-tag">$ENV{VAR}</pre> ，后者使用语法<pre class="crayon-plain-tag">[HKEY_CURRENT_USER\\SOFTWARE\\path;key]</pre> 。示例：</p>
<pre class="crayon-plain-tag"># 读取环境变量
message(STATUS "LD_LIBRARY_PATH=$ENV{LD_LIBRARY_PATH}" )
# 设置环境变量
set(ENV{PATH} "/home/alex/scripts")</pre>
<div class="blog_h3"><span class="graybg">CMake的优势</span></div>
<ol>
<li>支持多个底层构建工具，例如GNU Make、MSVC、XCode等等，可以生成这些构建工具需要的配置文件</li>
<li>通过分析环境变量、Windows注册表等，自动搜索构建所需的程序、库、头文件</li>
<li>支持创建复杂的命令</li>
<li>很方便的在共享库、静态库两种构建方式之间切换</li>
<li>自动<span style="background-color: #c0c0c0;">生成、维护C/C++文件</span>依赖关系，并且在大部分平台上支持<span style="background-color: #c0c0c0;">并行构建</span></li>
</ol>
<p>在开发跨平台软件时，CMake具有以下额外优势：</p>
<ol>
<li>可以测试机器字节序和其它硬件特性</li>
<li>统一的构建配置文件</li>
<li>支持依赖于机器特定信息的配置，例如文件的位置</li>
</ol>
<div class="blog_h2"><span class="graybg">安装CMake</span></div>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt-get install cmake
# Redhat
yum install cmake
# Mac OS X with Macports
sudo port install cmake
# Window https://cmake.org/files/v3.5/cmake-3.5.2-win32-x86.zip</pre>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<p>C++源码：</p>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;

using namespace std;

int main() {
    return 0;
}</pre>
<p>要通过CMake编译上述文件，需要在同一目录下放置CMakeLists.txt文件：</p>
<pre class="crayon-plain-tag"># 需要最小的CMake版本
cmake_minimum_required(VERSION 3.3)
# 工程的名称，会作为MSVS的Workspace的名字
project(intellij_taste)

# 全局变量：CMAKE_SOURCE_DIR CMake的起始目录，即源码的根目录
# 全局变量：PROJECT_NAME 工程的名称
# 全局变量：PROJECT_SOURCE_DIR 工程的源码根目录的完整路径

# 全局变量：构建输出目录。默认的，对于内部构建，此变量的值等于CMAKE_SOURCE_DIR；否则等于构建树的根目录
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/bin)  # ${}语法用于引用变量
# 全局变量：可执行文件的输出路径
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# 全局变量：库文件的输出路径
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# 设置头文件位置
include_directories("${PROJECT_SOURCE_DIR}")
# 设置C++标志位
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# 设置源文件集合
set(SOURCE_FILES main.cpp)
# 添加需要构建的可执行文件，第二个以及后续参数是用于构建此文件的源码文件
add_executable(intellij_taste ${SOURCE_FILES})</pre>
<p>在上述目录中执行下面两条命令，即可执行构建：</p>
<pre class="crayon-plain-tag"># 创建一个build子目录作为构建树
mkdir build &amp;&amp; cd build &amp;&amp; cmake .. &amp;&amp; cd ..

# 在build/bin子目录中生成可执行文件：
# cmake --build &lt;dir&gt; [options] [-- [native-options]]
cmake --build build -- -j3  # --表示把其余选项传递给底层构建工具

# 注意，亦可使用底层构建系统，例如make命令或者MSVC的IDE
cd build
make -j3</pre>
<div class="blog_h1"><span class="graybg">核心理念</span></div>
<p>CMake包含一系列重要的概念抽象，包括目标（Targets）、生成器（Generators）、命令（Commands）等，这些命令均被实现为C++类。理解这些概念后才能编写高效的CMakeLists文件。</p>
<p>下面列出这些概念之间的基本关系：</p>
<ol>
<li>源文件：对应了典型的C/C++源代码</li>
<li>目标：多个源文件联合成为目标，<span style="background-color: #c0c0c0;">目标通常是可执行文件或者库</span></li>
<li>目录：表示源码树中的一个目录，常常包含一个CMakeLists.txt文件，一或多个目标与之关联</li>
<li>本地生成器（Local generator）：每个目录有一个本地生成器，负责为此目录生成Makefiles，或者工程文件</li>
<li>全局生成器（Global generator）：所有本地生成器共享一个全局生成器，后者负责监管构建过程，全局生成器由CMake本身创建并驱动</li>
</ol>
<p>CMake的执行开始时，会创建一个cmake对象并把命令行参数传递给它。cmake对象管理整体的配置过程，持有构建过程的全局信息（例如缓存值）。cmake会依据用户的选择来创建合适的全局生成器（VS、Makefiles等等），并把构建过程的控制权转交给全局生成器（调用configure和generate方法）。</p>
<p>全局生成器负责管理配置信息，并生成所有Makefiles/工程文件。一般情况下全局生成器把具体工作委托给本地生成器执行，全局生成器为每个目录创建一个本地生成器。全局/本地生成器的分工取决于实现，例如：</p>
<ol>
<li>对于VS，全局生成器负责生成解决方案文件，本地生成器负责每个目标的工程文件</li>
<li>对于Makefiles，全局生成器生成总体的Makefile，本地生成器则负责生成大部分Makefile</li>
</ol>
<p>每个本地生成器包含一个cmMakefile对象，其中存放CMakeList.txt的解析结果。</p>
<p>CMake的每一个命令也被实现为C++类，该类主要包括两个成员：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">成员</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>InitialPass()</td>
<td>接受当前目录的cmMakefile对象、命令参数作为入参。命令的执行结果存放在cmMakefile对象中</td>
</tr>
<tr>
<td>LastPass()</td>
<td>在整个CMake工程所有命令的InitialPass()都执行后再执行。大部分命令不实现此方法</td>
</tr>
</tbody>
</table>
<p>下图显示cmake、生成器、cmMakefile、命令等类型的关系：<img class="aligncenter size-full wp-image-11546" src="https://blog.gmem.cc/wp-content/uploads/2015/05/cmake-class-diagram.png" alt="cmake-class-diagram" width="636" height="804" /></p>
<div class="blog_h2"><span class="graybg">目标</span></div>
<p>cmMakefile对象中存放的最重要的对象是目标（Targets），目标代表可执行文件、库、实用工具等。每个<pre class="crayon-plain-tag">add_library</pre> 、<pre class="crayon-plain-tag">add_executable</pre> 、<pre class="crayon-plain-tag">add_custom_target</pre> 命令都会创建一个目标。</p>
<div class="blog_h3"><span class="graybg">库目标</span></div>
<p>下面的语句创建一个库目标：</p>
<pre class="crayon-plain-tag"># 创建一个静态库，包含两个源文件
add_library(foo STATIC foo1.c foo2.c)</pre>
<p>上述命令声明的foo可以作为库名称在工程的任何地方使用。CMake知道如何将此名称转换为库文件。命令的（可选的）第二个参数声明库的类型，有效值包括： </p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">库类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>STATIC</td>
<td>目标必须构建为静态库</td>
</tr>
<tr>
<td>SHARED</td>
<td>目标必须构建为共享库</td>
</tr>
<tr>
<td>MODULE</td>
<td>目标必须构建为支持在运行时动态加载到可执行文件中的模块<br />对于除了Mac OS X之外的系统，此取值等价于SHARED</td>
</tr>
</tbody>
</table>
<p>如果不声明库类型，则CMake依据变量<pre class="crayon-plain-tag">BUILD_SHARED_LIBS</pre> 判断应该构建为共享库还是静态库，如果此变量不设置，构建为静态库。 </p>
<div class="blog_h3"><span class="graybg">可执行目标</span></div>
<p>与库目标类似，可执行目标也可以指定特定的选项，例如WIN32会导致操作系统调用WinMain而不是main函数。</p>
<div class="blog_h3"><span class="graybg">读写目标属性</span></div>
<p>使用<pre class="crayon-plain-tag">set_target_properties</pre> 或者<pre class="crayon-plain-tag">get_target_properties</pre> 命令，或者更通用的<pre class="crayon-plain-tag">set_property</pre> 、<pre class="crayon-plain-tag">get_property</pre> 命令，可以读写目标的属性，示例：</p>
<pre class="crayon-plain-tag"># 修改目录使用的头文件目录，注意，全局的include_directories会此目标忽略
set_property (TARGET jsonrpc
  PROPERTY INCLUDE_DIRECTORIES
    ${JSONCPP_INCLUDE_DIRS}
    ${Boost_INCLUDE_DIRS}
    ${CMAKE_CURRENT_SOURCE_DIR}
    ${CMAKE_CURRENT_SOURCE_DIR}/jsonrpc
)

# 同时设置多个属性
set_target_properties(jsonrpc PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR})</pre>
<p>常用目标属性如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>LINK_FLAGS</td>
<td>指定（传递给）链接（器的）标记。示例：<br />
<pre class="crayon-plain-tag">set_target_properties(
    cstudy PROPERTIES
    INCLUDE_DIRECTORIES /home/alex/.local/include
    # 使用定制的glibc库
    LINK_FLAGS "-Wl,-rpath=/home/alex/.local/lib  -Wl,--dynamic-linker=/home/alex/.local/lib/ld-linux-x86-64.so.2"
)</pre>
</td>
</tr>
<tr>
<td>COMPILE_FLAGS</td>
<td>指定（传递给）编译（器的）标记</td>
</tr>
<tr>
<td>INCLUDE_DIRECTORIES</td>
<td>指定目标需要引用的头文件目录</td>
</tr>
<tr>
<td>PUBLIC_HEADER</td>
<td>共享的库目标提供的公共头文件</td>
</tr>
<tr>
<td>VERSION<br />SOVERSION</td>
<td>
<p>对于共享库来说，VERSION、SOVERSION允许让你分别设置构建版本、API版本。通常SOVERSION更加稳定不变</p>
<p>如果设置了NO_SONAME属性，则SOVERSION属性被自动忽略</p>
</td>
</tr>
<tr>
<td>OUTPUT_NAME</td>
<td>
<p>目标输出文件名称</p>
</td>
</tr>
</tbody>
</table>
<p>全部目标属性请<a href="https://cmake.org/cmake/help/v3.0/manual/cmake-properties.7.html#properties-on-targets">参考官网</a>。</p>
<div class="blog_h3"><span class="graybg">链接到库</span></div>
<p>使用<pre class="crayon-plain-tag">target_link_libraries</pre> 命令，可以指定目标需要链接的库的列表。列表的元素可以是库、库的全路径、通过add_library命令添加的库名称。</p>
<p>对于声明的每个库，CMake会跟踪其依赖的所有其它库，这种依赖关系需要用上述命令来设置：</p>
<pre class="crayon-plain-tag">add_library(foo foo.cpp)
#foo库依赖于bar库
target_link_libraries(foo bar)

add_executable(foobar foobar.cpp)
#foobar显式依赖foo，隐式依赖bar，后两者都会被链接到foobar中
target_link_libraries(foobar foo)</pre>
<div class="blog_h2"><span class="graybg">源文件</span></div>
<p>和Target类似，源文件也被建模为C++类，也支持读写属性（通过set_source_files_properties、get_source_files_properties或更加一般的命令）。最常用属性包括：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>COMPILE_FLAGS</td>
<td>针对特定源文件的编译器标记，可以包含-D、-I之类的标记</td>
</tr>
<tr>
<td>GENERATED</td>
<td>指示此文件是否在构建过程中生成，这种文件在CMake首次运行时不存在，因而计算依赖关系时要特殊考虑</td>
</tr>
<tr>
<td>OBJECT_DEPENDS</td>
<td>添加此源文件额外依赖的其它文件。CMake会自动分析C、C++的源文件依赖，因而此选项很少使用</td>
</tr>
<tr>
<td>WRAP_EXCLUDE</td>
<td>CMake不直接使用该属性。但是某些命令和扩展读取该属性，判断何时/如何把C++类包装到其它语言，例如Python</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">目录、生成器、测试、属性</span></div>
<p>其它偶尔可能用到的CMake类型包括Directory、Generator、Test、Property等。Directory、Generator、Test的实例同样（与目录、源文件类似）关联属性。</p>
<p>属性是一种键值存储，它关联到一个对象。读写属性最一般的方法是上面提到的get/set_property命令。所有可用的属性可以通过<pre class="crayon-plain-tag">cmake -help-property-list</pre> 得到。</p>
<p>目录的属性包括：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ADDITIONAL_MAKE_CLEAN_FILES</td>
<td>指定一系列需要在make clean时清除掉的文件的列表<br />默认的CMake会清除所有生成的文件</td>
</tr>
<tr>
<td>EXCLUDE_FROM_ALL</td>
<td>指示此目录和子目录中所有的目标，是否应当从默认构建中排除<br />子目录的IDE工程文件/Makefile将从顶级IDE工程文件/Makefile中排除</td>
</tr>
<tr>
<td>LISTFILE_STACK</td>
<td>最要在调试CMake脚本时用到，列出当前正在被处理的文件的列表</td>
</tr>
</tbody>
</table>
<p>目录和生成器对象会在CMake处理你的源码树时自动创建。</p>
<div class="blog_h2"><span class="graybg">变量和缓存条目（Cache Entries）</span></div>
<div class="blog_h3"><span class="graybg">变量</span></div>
<p>CMakeLists中的变量和普通编程语言中的变量很类似，变量的值要么是单个值，要么是列表。CMake自动定义一系列重要的变量。</p>
<p>要引用变量，必须使用<pre class="crayon-plain-tag">${VARNAME}</pre> 语法，要设置变量的值，需要使用set命令。</p>
<p>CMake中变量的作用域和普通编程语言略有不同，当你设置一个变量后，变量对当前CMakeLists文件、当前函数、<span style="background-color: #c0c0c0;">以及子目录的CMakeLists</span>、任何通过<pre class="crayon-plain-tag">INCLUDE</pre> <span style="background-color: #c0c0c0;">包含进来的文件</span>、任何<span style="background-color: #c0c0c0;">调用的宏或函数</span>可见。</p>
<p>当处理一个子目录、调用一个函数时，CMake创建一个新的作用域，其复制当前作用域全部变量，在<span style="background-color: #c0c0c0;">子作用域中对变量的修改不会对父作用域产生影响</span>。要修改父作用域中的变量，可以在set时指定特殊选项：</p>
<pre class="crayon-plain-tag">set (name Alex PARENT_SCOPE)</pre>
<p>变量的值可以是一个列表，这样的变量可以被展开为多个值：</p>
<pre class="crayon-plain-tag">set(fruit apple peach strawberry )
foreach(f ${fruit})
    message("Do you want ${f}")
endforeach()</pre>
<p><a id="useful-vars"></a>常用变量如下表：</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>CMAKE_C_FLAGS</td>
<td>C编译标记，示例：<pre class="crayon-plain-tag">set(CMAKE_C_FLAGS "-std=c11 -pthread")</pre></td>
</tr>
<tr>
<td>CMAKE_CXX_FLAGS</td>
<td>C++编译标记</td>
</tr>
<tr>
<td>CMAKE_C_FLAGS_DEBUG</td>
<td>用于Debug配置的C编译标记，示例：<pre class="crayon-plain-tag">set(CMAKE_C_FLAGS_DEBUG "-g -O0")</pre></td>
</tr>
<tr>
<td>CMAKE_CXX_FLAGS_DEBUG</td>
<td>用于Debug配置的C++编译标记</td>
</tr>
<tr>
<td>CMAKE_C_FLAGS_RELEASE</td>
<td>用于Release配置的C编译标记</td>
</tr>
<tr>
<td>CMAKE_CXX_FLAGS_RELEASE</td>
<td>用于Release配置的C++编译标记</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">缓存条目</span></div>
<p>有些时候你可能期望<span style="background-color: #c0c0c0;">用户</span>通过CMake的UI<span style="background-color: #c0c0c0;">输入</span>一些变量的值， 这时<span style="background-color: #c0c0c0;">变量必须作为缓存条目</span>。当CMake运行时，它会向二进制目录输出缓存文件（Cache file），缓存文件中的变量值通过CMake的UI展示给用户。</p>
<p>使用这种缓存的<span style="background-color: #c0c0c0;">目的之一</span>是，存储用户的选项，避免重新运行CMake时，反复要求用户输入相同的信息。</p>
<p><pre class="crayon-plain-tag">option</pre> 命令可以创建一个Boolean变量（ON/OFF）并将其存储在缓存中：</p>
<pre class="crayon-plain-tag">option(USE_PNG "Do you want to use the png library?")</pre>
<p>用户可以通过UI设置USE_PNG的值，并且在未来这一值会保存在缓存中。使用CLion作为IDE时，可以在CMake窗口中点击Cache选项卡，查看或者编辑缓存条目：</p>
<p><img class="aligncenter wp-image-11640 size-full" src="https://blog.gmem.cc/wp-content/uploads/2015/05/Selection_002.png" alt="Selection_002" width="710" /></p>
<p>除了<pre class="crayon-plain-tag">option</pre> 命令之外，<pre class="crayon-plain-tag">find_file</pre> 也可以用来创建缓存条目。为<pre class="crayon-plain-tag">set</pre> 命令指定特殊参数，亦可创建缓存条目： </p>
<pre class="crayon-plain-tag"># CACHE选项表示此变量作为缓存条目
# ON为默认值
# BOOL为变量类型，支持BOOL、PATH、FILEPATH、STRING
set(USE_PNG ON CACHE BOOL "Do you want to use the png library?")</pre>
<p>缓存条目的<span style="background-color: #c0c0c0;">另外一个目的</span>是， 存储那些难以确定的关键变量，这些变量可能对用户不可见。通常这些变量是系统相关的，例如<pre class="crayon-plain-tag">CMAKE_WORDS_BIGENDIAN</pre> 。这类值可能需要CMake编译并运行一个程序来确定，一旦确定，即缓存。</p>
<p>位于缓存中的变量具有一个属性指示它是否为“进阶的”（advanced），默认的CMake GUI隐藏进阶条目。要标记一个缓存条目为进阶的，可以：</p>
<pre class="crayon-plain-tag">mark_as_advanced(VAR_NAME)</pre>
<p>某些情况下，你可能需要限制缓存条目的值范围在一个有限的集合中，这是可以设置条目的<pre class="crayon-plain-tag">STRINGS</pre> 属性，提供值列表。在GUI中，这种条目的字段会展示为下拉列表：</p>
<pre class="crayon-plain-tag"># 设置名为CRYPT_BACKEND的缓存条目的值为Open SSL
set(CRYPT_BACKEND "Open SSL" CACHE STRING)
# 设置上述缓存条目的取值范围
set_property(CACHE CRYPT_BACKEND PROPERTY STRINGS "Open SSL" "LibDES")</pre>
<p>即使变量存在于缓存，你仍然可以在CMakeLists中<span style="background-color: #c0c0c0;">覆盖它（改变作用域中此变量的值）</span>。只需要不带CACHE选项调用set命令，即可覆盖缓存中同名变量的值。 </p>
<p>另一方面，一旦变量值已经缓存，你一般无法在CMakeLists中改变缓存的值（与上述覆盖是两回事）。也就是说，当缓存中有VARNAME时，<pre class="crayon-plain-tag">set(VARNAME ON CACHE BOOL )</pre> 不会有任何作用。要<span style="background-color: #c0c0c0;">强制改变缓存中的值并覆盖</span>当前作用域的值，可以联合使用<pre class="crayon-plain-tag">FORCE</pre> 和<pre class="crayon-plain-tag">CACHE</pre>选项。</p>
<div class="blog_h2"><span class="graybg">构建配置</span></div>
<p>构建配置允许工程使用不同方式构建：debug、optimized或者任何其它标记。CMake默认支持四种构建配置：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">构建配置</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Debug</td>
<td>启用基本的调试（编译器的）标记</td>
</tr>
<tr>
<td>Release</td>
<td>基本的优化配置</td>
</tr>
<tr>
<td>MinSizeRel</td>
<td>生成最小化的，但不一定是最快的代码</td>
</tr>
<tr>
<td>RelWithDebugInfo</td>
<td>优化构建，但是同时携带调试信息</td>
</tr>
</tbody>
</table>
<p>依据生成器的不同，CMake处理构建配置的方式有所差异，CMake尽可能遵循底层本地构建系统的约定，这意味着使用Makefiles、VS时构建配置影响构建的方式有所不同：</p>
<ol>
<li>VS支持构建配置的概念，在IDE中你可以选择Debug、Release配置，CMake只需要对接到VS的构建配置即可</li>
<li>Makefile默认同时（CMake运行时）只能有一种配置被激活。使用<pre class="crayon-plain-tag">CMAKE_BUILD_TYPE</pre> 变量可以指定目标配置。如果此变量为空，则不给构建添加额外标记。如果此变量设置为上面四种构建配置之一，则相应的变量、规则——例如<pre class="crayon-plain-tag">CMAKE_CXX_FLAGS_&lt;CONFIGNAME&gt;</pre> 被添加到compile line中。可以使用下面的方式来分别基于Debug、Release配置进行构建：<br />
<pre class="crayon-plain-tag"># 创建工程目录的两个兄弟目录，CD到其中分别执行：
cmake ../project -DCMAKE_BUILD_TYPE:STRING=Debug
cmake ../project -DCMAKE_BUILD_TYPE:STRING=Release </pre>
</li>
</ol>
<div class="blog_h1"><span class="graybg">编写CMakeLists文件</span></div>
<p>CMake由CMakeLists.txt驱动，此文件包含构建需要的一切信息。</p>
<p>除了用于分隔命令参数，其余空白符一律被忽略。反斜杠可以用来指示转义字符。</p>
<div class="blog_h2"><span class="graybg">基本命令</span></div>
<table class=" full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>project</td>
<td>顶层CMakeLists.txt中应当包含的第一个命令，声明工程的名字和使用的编程语言：<br />
<pre class="crayon-plain-tag">project (projectname, [CXX], [C], [JAVA], [NONE])</pre></p>
<p>如果不指定语言，默认CMake启用C/C++，如果指定为CXX则C语言的支持自动加入</p>
<p>对于工程中出现的<span style="background-color: #c0c0c0;">每个</span>project命令，CMake会创建一个<span style="background-color: #c0c0c0;">顶级的IDE工程文件</span>（或Makefile文件） 。此工程文件中会包含：</p>
<ol>
<li>所有CMakeLists.txt中声明的目标</li>
<li>所有通过<pre class="crayon-plain-tag">add_subdirectory</pre> 命令添加的子目录。如果为命令指定<pre class="crayon-plain-tag">EXCLUDE_FROM_ALL</pre> 选项，则此工程文件/Makefile不会包含到顶级工程文件/Makefile中，对于那种需要从主构建流传中排除的子工程（例如examples子工程），这个选项有用</li>
</ol>
</td>
</tr>
<tr>
<td>set</td>
<td>设置变量值或列表</td>
</tr>
<tr>
<td>remove</td>
<td>从变量值的列表中移除一个单值</td>
</tr>
<tr>
<td>separate_arguments</td>
<td>基于空格，把单个字符串分隔为列表</td>
</tr>
<tr>
<td>add_executable</td>
<td rowspan="2">定义目标（可执行文件/库），以及目录由哪些源文件组成<br />对于VS，源文件将会出现在IDE中，但是默认的项目使用的头文件不会包含在IDE中，要改变此行为，只需要将头文件添加到源文件列表中</td>
</tr>
<tr>
<td>add_library</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">流程控制命令</span></div>
<p>和普通编程语言一样，CMake支持条件、循环控制结构，同时支持子过程（macro、function）</p>
<div class="blog_h3"><span class="graybg">if-else-endif</span></div>
<pre class="crayon-plain-tag">if (FOO)
else(FOO)
endif(FOO)
# 上面把if的条件在else、endif中重复，这是可选的。因此我们可以简单的写作：
if (FOO)
else()
endif()</pre>
<p>在else、endif上重复条件，有助于if-else-endif匹配检查，特别是多层嵌套时。</p>
<div class="blog_h3"><span class="graybg">elseif</span></div>
<p>CMake同样支持elseif：</p>
<pre class="crayon-plain-tag">if(MSVC80)
  #...
elseif(MSVC90)
  #...
elseif(APPLE)
  #...
endif()</pre>
<div class="blog_h3"><span class="graybg">条件表达式</span></div>
<p>条件命令支持受限的表达式语法，如下表所列：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">语法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>if ( variable )</td>
<td>
<p>当if命令参数的值不是：<span style="background-color: #c0c0c0;">0、FALSE、OFF、NO、NOTFOUND、*-NOTFOUND、IGNORE</span>时，表达式的值为真，注意<span style="background-color: #c0c0c0;">不区分大小写<br /></span>variable可以不用${}包围</p>
</td>
</tr>
<tr>
<td>if ( NOT variable )</td>
<td>上面取反<br />variable可以不用${}包围</td>
</tr>
<tr>
<td>if ( variable1 AND variable2 )</td>
<td>逻辑与，所有逻辑操作支持<span style="background-color: #c0c0c0;">用括号来提升优先级</span></td>
</tr>
<tr>
<td>if ( variable1 OR variable2 )</td>
<td>逻辑或</td>
</tr>
<tr>
<td>if ( num1 EQUAL num2 )</td>
<td>数字相等比较，其它操作符包括LESS、GREATER</td>
</tr>
<tr>
<td>if ( str1 STREQUAL str2 )</td>
<td>字典序相等比较，其它操作符包括STRLESS、STRGREATER</td>
</tr>
<tr>
<td>if ( v1 VERSION_EQUAL v2)</td>
<td><pre class="crayon-plain-tag">marjor[.minor[.patch[.tweak]]]</pre> 风格的版本号相等比较，其它操作符包括VERSION_LESS、VERSION_GREATER</td>
</tr>
<tr>
<td>if ( COMMAND  commandname )</td>
<td>如果指定的命令可以调用</td>
</tr>
<tr>
<td>if ( DEFINED variable )</td>
<td>如果指定的变量被定义，不管它的值真假</td>
</tr>
<tr>
<td>if ( EXISTS file-name )</td>
<td>如果指定的文件或者目录存在</td>
</tr>
<tr>
<td>if ( IS_DIRECTORY name )</td>
<td>如果给定的name是一个目录</td>
</tr>
<tr>
<td>if ( IS_ABSOLUTE name )</td>
<td>如果给定的name是一个绝对路径</td>
</tr>
<tr>
<td>if ( n1 IS_NEWER_TAN n2 )</td>
<td>如果文件n1的修改时间大于n2</td>
</tr>
<tr>
<td>if ( variable MATCHES regex )</td>
<td rowspan="2">如果给定的变量或者字符串匹配正则式：<br />
<pre class="crayon-plain-tag">set(name Alex)
if(${name} MATCHES A.*x)
    message(${name})
endif()</pre>
</td>
</tr>
<tr>
<td>if ( string MATCHES regex )</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">操作符优先级</span></div>
<p>CMake操作符优先级从高到底：</p>
<ol>
<li>括号分组：()</li>
<li>前缀一元操作符：EXISTS、COMMAND、DEFINED</li>
<li>比较操作符：EQUAL、LESS、GREATER及其变体，以及MATCHES</li>
<li>逻辑非：NOT</li>
<li>逻辑或于：AND、OR</li>
</ol>
<div class="blog_h3"><span class="graybg">foreach</span></div>
<pre class="crayon-plain-tag">foreach (item  list)
    # do something with item
endforeach (item)</pre>
<p>此命令用于迭代一个列表，第一个参数是每次迭代使用变量的名称，其余参数为被迭代的列表</p>
<p>注意，在循环内部，你可以使用迭代变量<span style="background-color: #c0c0c0;">构造另外一个变量的名字</span>，例如<pre class="crayon-plain-tag">${NAME_OF_${item}}</pre> </p>
<div class="blog_h3"><span class="graybg">while</span></div>
<p>此命令用于<span style="background-color: #c0c0c0;">基于条件的</span>迭代：</p>
<pre class="crayon-plain-tag">while(${COUNT} LESS 2000)
    set(TASK_COUNT, ${COUNT})
endwhile()</pre>
<div class="blog_h3"><span class="graybg">break</span></div>
<p>此命令用于中断foreach/while循环。</p>
<div class="blog_h3"><span class="graybg">function</span></div>
<p>CMake中的函数很类似于C/C++函数。你可以向函数传递参数，除了依据形参名外，你还可以使用<pre class="crayon-plain-tag">ARGC</pre> 、<pre class="crayon-plain-tag">ARGV</pre> 、<pre class="crayon-plain-tag">ARGN</pre> 、<pre class="crayon-plain-tag">ARG0</pre> 、<pre class="crayon-plain-tag">ARG1</pre> ...等形式，在函数内部访问入参。</p>
<p>函数内部是一个新作用域，类似于add_subdirectory生成的新作用域一样，函数<span style="background-color: #c0c0c0;">调用前的作用域被拷贝并传递到函数内部</span>，函数返回时，新作用域消失。</p>
<p>函数的<span style="background-color: #c0c0c0;">第一个形参是函数的名称</span>，其它参数构成传统的<span style="background-color: #c0c0c0;">形参列表</span>：</p>
<pre class="crayon-plain-tag">function(println msg)
    message(${msg} "\n")
    set ( msg ${msg} PARENT_SCOPE )  #设置父作用域中变量的值
endfunction()

println(Hello)</pre>
<div class="blog_h3"><span class="graybg">return</span></div>
<p>此命令拥有从函数中返回，或者在listfile命令中提前结束。</p>
<div class="blog_h3"><span class="graybg">macro</span></div>
<p>宏于函数类似，但是<span style="background-color: #c0c0c0;">宏不会创建新的</span>作用域。传递给宏的参数也不被作为变量看待，而是在<span style="background-color: #c0c0c0;">执行宏前替换为字符串</span>：</p>
<pre class="crayon-plain-tag">macro (println msg)      #同样的，括号中第一个项目是宏的名称
    message(${msg} "\n")
endmacro()</pre>
<p>对于宏， ARGC、ARG0、ARG1等也可以使用。ARG0代表传递给宏的第一个参数。</p>
<div class="blog_h2"><span class="graybg">检查CMake的版本</span></div>
<p>CMake是一个不断进化的工具，随着新版本的推出，会不断有新的命令被加入。很多时候，我们需要检查当前CMake版本是否支持某些特性。</p>
<p>我们可以使用if命令判断某个命令是否可用：</p>
<pre class="crayon-plain-tag">if(COMMAND some_new_command)
    #...
endif()</pre>
<p>或者直接检查CMake的版本：</p>
<pre class="crayon-plain-tag">if (${CMAKE_VERSION} VERSION_GREATER 1.6.1)
endif()</pre>
<p>另外，还可以声明要求的最低的CMake版本：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 2.8)</pre>
<div class="blog_h2"><span class="graybg">使用模块</span></div>
<p>所谓模块，仅仅是存放到一个文件中，一系列CMake命令的集合。我们可以用<pre class="crayon-plain-tag">include</pre> 命令将模块包含到CMakeLists.txt中。举例：</p>
<pre class="crayon-plain-tag"># 此模块用于查找TCL库
include (FindTCL)
# 找到后，将其加入到链接依赖中
target_link_libraries (FOO ${TCL_LIBRARY})</pre>
<p>包含一个模块时，可以使用绝对路径，或者是基于CMAKE_MODULE_PATH的相对路径，如果此变量未设置，默认为CMake的安装目录的Modules子目录。</p>
<p>模块依据用途的不同可以分为：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>查找类模块</td>
<td>
<p>查找软件元素——例如头文件、库——的位置</p>
<p>CMake提供了大量这类模块，如果目录库/头文件找不到，模块往往提供一个缓存条目，便于用户手工指定</p>
<p>下面是一个查找PNG模块的例子：</p>
<pre class="crayon-plain-tag"># png库依赖于zlib
include(FindZLIB)  # 查找zlib库
if (ZLIB_FOUND)    # 往往在找到后设置LIBNAME_FOUND变量
    # 查找头文件位置并存入变量
    find_path(PNG_PNG_INCLUDE_DIR png.h /usr/local/include /usr/include)
    # 查找库文件位置并存入变量    
    find_library(PNG_LIBRARY png /usr/lib /usr/local/lib)
    if (PNG_LIBRARY AND PNG_PNG_INCLUDE_DIR)
        # 合并ZLIB头文件和库到PNG的
        set(PNG_INCLUDE_DIR ${PNG_PNG_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR})
        set(PNG_LIBRARIES ${PNG_LIBRARY} ${ZLIB_LIBRARY})
        # 设置已找到标记
        set(PNG_FOUND YES)
    endif ()
endif ()</pre>
</td>
</tr>
<tr>
<td>系统探测模块</td>
<td>
<p>探测系统的特性，例如浮点数长度、对ASCI C++刘的支持
<p>很多这类模块具有Test、Check前缀，例如TestBigEndian、CheckTypeSize</p>
</td>
</tr>
<tr>
<td>实用工具模块</td>
<td>用于添加额外的功能，例如处理一个CMake工程依赖于其它CMake工程的情况</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">策略（Policies）</span></div>
<p>由于某些原因，在版本升级后，CMake可能不提供完全的向后兼容。这意味着使用新版的CMake处理基于旧版本的CMakeLists.txt时会出现问题。CMake引入策略这一特性，帮助用户和开发者处理此向后兼容问题。</p>
<p>策略机制实现以下目标：</p>
<ol>
<li>既有的工程能够用<span style="background-color: #c0c0c0;">任何</span>比CMakeLists作者使用的、更新版本的CMake构建。用户不应该需要修改CMakeLists代码，但是可能出现警告信息</li>
<li> 新特性的修正，老接口的Bug修复应当被执行，而非因向后兼容性的要求而搁置</li>
<li>任何对CMake的改变，会导致CMakeLists文件必须更改的，应当加以文档说明。每个这样的改变应当具有唯一的标识符以便查阅文档，改变仅<span style="background-color: #c0c0c0;">在工程提示自己支持的情况下</span>才启用</li>
<li>最终将会移除向后兼容性的代码，<span style="background-color: #c0c0c0;">不再支持古老版本</span>的CMake。因此而构建失败的工程必须得到有价值的错误提示</li>
</ol>
<p>CMake中的所有策略被分配一个<pre class="crayon-plain-tag">CMPNNNN</pre> 形式的名称，其中NNNN是一个整数值编号。策略同时支持<span style="background-color: #c0c0c0;">出于兼容性目的的旧行为，以及“正确的”新行为</span>。每个策略包含出现动机、新旧行为的详细说明文档。</p>
<div class="blog_h3"><span class="graybg">设置策略</span></div>
<p>可以在工程中对每个策略进行配置，设置其值为NEW或者OLD，CMake将遵从测量设置，从而表现出不同的构建行为。</p>
<p>设置策略有几种方式，最简单的是设置策略为特定的CMake版本：<pre class="crayon-plain-tag">cmake_policy(VERSION 2.6)</pre> 。这样所有2.6版本之前引入的策略都被标记为NEW，而2.6之后引入的策略则标记为“未设置”，以便产生警告信息。</p>
<p>注：cmake_minimum_required命令同样会设置策略，因此仅在需要定制子目录的策略时才以VERSION选项调用cmake_policy命令。</p>
<p>以SET选项调用cmake_policy可以明确的设置单个策略。以CMP0002为例，该策略的新行为要求所有逻辑目标具有全局独特的名字。下面的命令可以抑制存在重复目标名时的警告信息：</p>
<pre class="crayon-plain-tag">cmake_policy(SET CMP0002 OLD)</pre>
<div class="blog_h2"><span class="graybg">链接到库</span></div>
<pre class="crayon-plain-tag"># 设置库的寻找目录
link_directories(/path/to)
add_executable(myexe myexe.c)
target_link_libraries (myexe A B)

# 或者
add_executable(myexe myexe.c)
# 使用绝对路径
target_link_libraries (myexe /path/to/libA.so /path/to/libB.so )</pre>
<div class="blog_h3"><span class="graybg">链接到系统库</span></div>
<p>类Unix操作系统的系统库常常位于/usr/lib或者/lib目录。这些目录被链接器作为隐含的库搜索目录，因此<pre class="crayon-plain-tag">find_library(M_LIB m)</pre> 将从/usr/lib/libm.so定位到Math库。</p>
<p>问题是，某些平台会依据体系结构的不同，提供库的不同版本：</p>
<pre class="crayon-plain-tag"># IRIX机器
/usr/lib/libm.so         # ELF o32
/usr/lib32/libm.so       # ELF n32
/usr/lib64/libm.so       # ELF 64
# Solaris
/usr/lib/lim.so          # sparcv8架构
/usr/lib/sparcv9/lim.so  # sparcv9架构</pre>
<p>find_library命令不知道各种体系结构特定的系统如何定义上面的目录规则，因此此命令可能找到<span style="background-color: #c0c0c0;">不匹配的体系结构的</span>库文件。</p>
<p>此问题的一个解决办法是让<span style="background-color: #c0c0c0;">链接器自动寻找</span>库所在目录（不使用link_directories或者指定绝对路径），不幸的是，此办法无法区分库的动态、静态版本。CMake实际使用的妥协做法是：</p>
<ol>
<li><span style="background-color: #c0c0c0;">存在于隐含库搜索目录</span>中的库，且链接器<span style="background-color: #c0c0c0;">支持类似-Bstatic</span>的选项来指定使用静态库，使用-l选项传递库名称</li>
<li>其它情况下，传递库绝对路径给链接器</li>
</ol>
<div class="blog_h2"><span class="graybg">共享库和可加载模块</span></div>
<p>共享库和可加载模块有利于重用：</p>
<ol>
<li>缩短<span style="background-color: #c0c0c0;">compile/link/run周期</span></li>
<li>共享库重新构建时，依赖于它的共享库/可执行文件甚至<span style="background-color: #c0c0c0;">不需要重新构建</span></li>
<li>减少磁盘和内存消耗，因为同一共享库只需要一份</li>
</ol>
<p>相比静态库，共享库更像是可执行文件，大部分系统要求共享库上具有可执行权限。和可执行文件一样，共享库可以链接到其它共享库。</p>
<p>对于静态库来说，一个object文件是最小单元；而共享库（包括其依赖）本身是一个最小单元。链接器可以从静态库中挑出需要的object文件，但是对于共享库及其依赖的其它共享库，都需要存在。</p>
<p>共享库和静态库的另外一个不同是库的声明顺序，指定静态库时顺序很重要，因为大部分链接器仅仅遍历库列表一次来寻找符号，依赖其它静态库的静态库必须放在列表前面。</p>
<p>当决定在工程使用共享库时，开发者必须面对几个问题。</p>
<div class="blog_h3"><span class="graybg">共享库导出哪些符号</span></div>
<p>在大部分UNIX系统中，默认所有符号被导出。在Windows系统中，开发者必须明确告知编译器哪些符号<span style="background-color: #c0c0c0;">被导入（使用符号时）/导出（创建符号时）</span></p>
<p>当从UNIX移植项目到Windows平台时，你可以：</p>
<ol>
<li>创建一个额外的.def文件，或者</li>
<li>使用微软的C/C++语言扩展——<pre class="crayon-plain-tag">__declspec(dllexport)</pre> 、<pre class="crayon-plain-tag">__declspec(dllimport)</pre> 声明的符号分别被导出、导入</li>
</ol>
<p>如果一个源文件在创建、使用一个库时都需要使用，则必须使用宏来处理。CMake在Windows下构建共享库（DDL）时，会自动定义宏<pre class="crayon-plain-tag">${LIBNAME}_EXPORTS</pre> 。我们可以利用此宏：</p>
<pre class="crayon-plain-tag">#if defined(WIN32)
    #if defined(vtkCommon_EXPORTS)
        #define VTK_COMMON_EXPORT __declspec(dllexport)
    #else
        #define VTK_COMMON_EXPORT __declspec(dllimport)
    #endif
#else
    #define VTK_COMMON_EXPORT
#endif</pre>
<p>这样，VTK_COMMON_EXPORT在UNIX中为空白；在Windows下构建共享库时为__declspec(dllexport)。</p>
<p>UNIX和Windows存在一个重要的和符号需求相关的差异：<span style="background-color: #c0c0c0;">Windows上的DLL需要完全解析，也就是在创建时必须链接所有符号</span>；而<span style="background-color: #c0c0c0;">UNIX允许共享库在运行时</span>从可执行文件或者其它共享库中<span style="background-color: #c0c0c0;">获取符号</span>。因而在UNIX中，CMake会给可执行目标一个标记，<span style="background-color: #c0c0c0;">允许它被共享库调用</span>。</p>
<p>另外一个需要提及的关于C++全局对象的陷阱是，加载或者链接了C++共享库的main函数，必须基于C++的编译器来链接，否则cout之类的全局对象可能在使用时尚未初始化。</p>
<div class="blog_h3"><span class="graybg">共享库位置</span></div>
<p>由于链接到共享库的可执行文件必须在运行时能找到这些库，特殊的环境变量或者链接器标记必须被使用。</p>
<p>不同系统都提供了工具，用以查看可执行文件实际上使用的是哪个库：</p>
<ol>
<li>UNIX系统的<pre class="crayon-plain-tag">ldd</pre> 命令：显示可执行文件使用哪些库。在Mac OS X上使用<pre class="crayon-plain-tag">otool -L</pre> </li>
<li>Windows系统的<pre class="crayon-plain-tag">depends</pre> 程序，功能类似</li>
</ol>
<p>在很多UNIX系统中，可以使用环境变量<pre class="crayon-plain-tag">LD_LIBRARY_PATH</pre> 来告诉应用程序到哪里寻找库，而在Windows中，环境变量<pre class="crayon-plain-tag">PATH</pre> 同时用来寻找DLL和可执行文件。CMake会默认把运行时库的路径信息存放到可执行文件中，因此前述环境变量并不必须。但是某些时候你可能需要关闭这个特性，设置<pre class="crayon-plain-tag">CMAKE_SKIP_RPATH=false</pre> 即可。</p>
<div class="blog_h2"><span class="graybg">共享库版本化</span></div>
<p>关于soname的基础知识，参考<a href="/linux-programming-faq#soname">Linux编程知识集锦</a>。</p>
<p>CMake支持这种基于soname的版本号编码机制，只要底层平台支持soname，可以设置共享库目标的属性：</p>
<pre class="crayon-plain-tag"># VERSION，指定一个版本号，用于创建文件名
# SOVERSION，指定一个版本号，用于生成SONAME头
set_target_properties (x PROPERTIES VERSION 1.2 SOVERSION 4)</pre>
<p>设置上述属性后，安装共享库时会产生如下文件和符号链接：</p>
<pre class="crayon-plain-tag">libx.so.1.2
libx.so.4 -&gt; libx.so.1.2
libx.so -&gt; libx.so.4</pre>
<p>如果仅指定两个版本号中的一个，那么另外一个自动<span style="background-color: #c0c0c0;">与之相同</span>。 </p>
<div class="blog_h2"><span class="graybg">安装文件</span></div>
<p>软件通常被安装到和源码、构建树无关的位置上。CMake提供一个<pre class="crayon-plain-tag">install</pre> 命令，来说明一个工程如何被安装。正确使用这个命令后：</p>
<ol>
<li>对于基于Makefile的生成器，用户只需要执行<pre class="crayon-plain-tag">make install</pre> 或者<pre class="crayon-plain-tag">nmake install</pre> 即可完成安装</li>
<li>对于基于GUI的平台，例如XCode、VS，用户只需要构建INSTALL目标</li>
</ol>
<p>对install的每一次调用都会指定某些安装规则，这些规则会依据命令调用的顺序被执行。</p>
<div class="blog_h3"><span class="graybg">install命令</span></div>
<p>install命令提供了若干“签名”（类似于子命令），签名作为第一个参数传入，可用的签名包括：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">签名</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>install(TARGETS...)</td>
<td>安装工程中目标对应的二进制文件</td>
</tr>
<tr>
<td>install(FILES...)</td>
<td>一般性的文件安装，包括头文件、文档、软件需要的数据文件</td>
</tr>
<tr>
<td>install(PROGRAMS...)</td>
<td>安装不是由当前工程构建的文件，例如Shell脚本，与FILES签名类似，只是文件被授予可执行权限</td>
</tr>
<tr>
<td>install(DIRECTORY...)</td>
<td>安装一个完整的目录树，例如包含了图标、图片的资源目录</td>
</tr>
<tr>
<td>install(SCRIPT...)</td>
<td>指定一个用户提供的、在安装过程中（典型的是pre-install、post-install）执行的CMake脚本</td>
</tr>
<tr>
<td>install(CODE...)</td>
<td>与SCRIPT类似，只是脚本以内联字符串形式提供</td>
</tr>
</tbody>
</table>
<p>前四个签名都用于创建文件的安装规则，需要安装的目标、目录、文件<span style="background-color: #c0c0c0;">紧接着签名列出</span>。其余和安装相关的信息，以关键字参数的形式附加，大部分签名支持以下关键字：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">关键字</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>DESTINATION</td>
<td>
<p>说明在何处放置被安装的文件，后面必须紧跟一个目录，此目录可以指定为绝对路径。如果使用相对路径，则相对于安装时指定的前缀，前缀可能由缓存条目<pre class="crayon-plain-tag">CMAKE_INSTALL_PREFIX</pre> 指定。前缀的默认值：</p>
<ol>
<li>UNIX：/usr/local</li>
<li>Windows：系统盘符:\Program Files\工程名称</li>
</ol>
</td>
</tr>
<tr>
<td>PERMISSIONS</td>
<td>
<p>说明如何设置被安装文件的权限（UNIX文件模式），仅在需要覆盖签名默认权限的情况下使用，可用的权限为：[OWNER|GROUP|WORLD][READ|WRITE|EXECUTE]、SETUID、SETGID</p>
<p>某些平台不完整支持上述权限，这种情况下自动忽略此关键字</p>
</td>
</tr>
<tr>
<td>CONFIGURATIONS</td>
<td>
<p>指定规则应用到的构建配置（Release、Debug...）的列表</p>
<p>没有应用到的构建配置，不会执行此命令调用产生的规则</p>
</td>
</tr>
<tr>
<td>COMPONENT</td>
<td>
<p>指定规则应用到的组件。某些工程把安装划分为多个组件，以便分别打包</p>
<p>例如某个工程可能包含三个组件：</p>
<ol>
<li>Runtime：包含运行软件需要的文件</li>
<li>Development：包含基于软件进行开发需要的文件</li>
<li>Documentation：包含软件的手册和帮助文档</li>
</ol>
<p>没有应用到的组件，不会执行此命令调用产生的规则</p>
<p>默认情况下，会安装所有组件，因而此关键字不产生任何影响。如果要安装特定组件，必须手工调用安装脚本</p>
</td>
</tr>
<tr>
<td>OPTIONAL</td>
<td>指示如果期望的待安装文件不存在时，不是一个错误，仅仅忽略之</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">TARGETS签名</span></div>
<p>以此签名调用install命令，以便构建过程中创建的库、可执行文件。详细调用格式为：</p>
<pre class="crayon-plain-tag">install ( TARGETS
    targets...  # 基于add_executable/add_library创建的目标的列表
    [
        # 通过TARGETS签名安装的文件可以分为三类：
        # ARCHIVE 静态库（UNIX/Cygwin/MinGW的.A、Windows的.LIB）
        #         DLL的可链接（Linkable）导入库（Cygwin/MinGW的.DLL.A、Windows的.LIB）
        # LIBRARY 可加载模块、共享库（.SO）
        # RUNTIME 可执行文件、动态链接库（.DLL）
        # 如果指定下面一行的某个关键字，则后续的关键字仅针对特定类型的文件，否则针对所有文件
        [ARCHIVE|LIBRARY|RUNTIME|FRAMEWORK|BUNDLE|PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
        [DESTINATION &lt;dir&gt;]
        [PERMISSIOS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT component]
        [OPTIONAL]
        [EXPORT &lt;export name&gt;]
        # 下面的关键字仅用于LIBRARY类型，仅针对支持namelink、版本化共享库的平台
        # 对于符号链接lib&lt;name&gt;.so -&gt; lib&lt;name&gt;.so.1，后者是soname，前者称为namelink，namelink用于在链接时-l选项找到共享库的位置
        # NAMELINK_ONLY导致仅仅共享库的namelink被安装；NAMELINK_SKIP导致除了namelink之外的文件被安装
        # 如果不指定，那么namelink、共享库的文件都被安装
        [NAMELINK_ONLY|NAMELINK_SKIP]
    ] [
        ... #仅需要针对不同类型（ARCHIVE|LIBRARY|RUNTIME...）分别设置关键字时，才会出现
    ]
)</pre>
<p>注意上面代码中关于文件分类的规则，把同属于共享库的.SO、.DLL分别划分到LIBRART、RUNTIME是有意的设计，因为Windows平台下，DLL通常和EXE存放在一个目录，这样才能确保DLL能够被找到并加载。下面的调用确保共享库目标mySharedLib产生的所有文件<span style="background-color: #c0c0c0;">在所有平台上均安装到期望的位置</span>：</p>
<pre class="crayon-plain-tag">install ( TARGETS myExecutable mySharedLib myStaticLib myPlugin
    RUNTIME DESTINATION bin             COMPONENT Runtime
    LIBRARY DESTINATION lib             COMPONENT Runtime
    ARCHIVE DESTINATION lib/myproject   Component Development    #静态库只有在二次开发时才需要
)</pre>
<div class="blog_h3"><span class="graybg">FILES签名</span></div>
<p>很多工程可能需要安装与目标无关的任何文件，这时可以使用一般目的的FILES签名：</p>
<pre class="crayon-plain-tag">install (FILES files...     #需要被安装的文件的列表，如果是相对路径，相对于当前Source目录
    DESTINATION &lt;dir&gt;       #目标位置，如果是相对路径，相对于安装Prefix
    [PERMISSIOS permissions...] #默认权限644
    [CONFIGURATIONS [Debug|Release|...]]
    [COMPONENT component]
    [RENAME &lt;name&gt;]  #为文件指定新的名称，要求文件列表只有一个元素
    [OPTIONAL]
)</pre>
<div class="blog_h3"><span class="graybg">PROGRAMS签名</span></div>
<p>某些工程可能安装额外的助手程序——Shell脚本或者Python脚本。这时可以使用PROGRAMS签名。此签名和FILES一样，只是默认权限为755。</p>
<div class="blog_h3"><span class="graybg">DIRECTORY签名</span></div>
<p>有时候我们需要安装包含了大量资源文件的整个目录，此时使用DIRECTORY签名： </p>
<pre class="crayon-plain-tag">install (DIRECTORY dirs...   # 需要被安装的目录的列表，如果是相对路径，相对于当前Source目录
    # 目标位置，此目录确保被创建。如果设置为share/myproject，则：
    # data/icons 被安装到/share/myproject/icons，注意输入目录的所有祖先目录被忽略
    # data/ 被安装到/share/myproject，注意结尾的斜杠，会导致此目录下所有内容被安装，因此data/类似于data/*
    DESTINATION &lt;dir&gt;   
    # 默认权限：文件与FILES一样644，目录与PROGRAMS一样755，下面两个关键字用于修改默认行为
    [FILE_PERMISSIOS permissions...]
    [DIRECTORY_PERMISSIOS permissions...]
    # 和文件来源保持一致的权限
    [USE_SOURCE_PERMISSIOS]
    [CONFIGURATIONS [Debug|Release|...]]
    [COMPONENT component]
    [
        # 排除某些文件，或者为某些文件指定特殊的权限
        # PATTERN用于UNIX风格通配符匹配；REGEX用于正则式匹配
        [PATTERN &lt;pattern&gt; | REGEX &lt;regex&gt;]
        # 是否把匹配的文件排除，不安装
        [EXCLUDE]
        # 设置匹配文件的权限
        [PERMISSIOS permissions...]
    ]
    [
    ...  #排除或者chmod其它匹配文件
    ]
)</pre>
<div class="blog_h3"><span class="graybg">SCRIPT/CODE签名</span></div>
<p>拷贝文件到安装树下（Installation tree）不是安装过程的唯一内容，有时候需要执行特定的逻辑。这时可以使用SCRIPT或者CODE签名：</p>
<pre class="crayon-plain-tag">install(SCRIPT scr.cmake)   # scr.cmake为某个CMake脚本名称
install(CODE "message(Hello)") #直接跟着脚本内容</pre>
<p>注意脚本不是在CMakeLists.txt处理过程中，而是在安装过程中执行，因而在脚本中不能访问CMakeLists.txt定义的变量。尽管如此，<pre class="crayon-plain-tag">CMAKE_INSTALL_PREFIX</pre> 、<pre class="crayon-plain-tag">CMAKE_INSTALL_CONFIG_NAME</pre>  、<pre class="crayon-plain-tag">CMAKE_INSTALL_COMPONENT</pre> 会被设置为真实的安装前缀、构建配置、组件类型。</p>
<div class="blog_h2"><span class="graybg">安装依赖的共享库</span></div>
<p>OS自带的、第三方提供的或者工程本身生成的共享库，是某些可执行文件能够运行的前提条件。由OS提供的自然不需要额外安装；工程本身产生的库由add_library命令说明，一般通过install命令安装到系统。需要额外考虑的是<span style="background-color: #c0c0c0;">第三方库</span>。</p>
<p>CMake提供两个模块，用于简化共享库的处理。</p>
<div class="blog_h3"><span class="graybg">GetPrerequisites.cmake</span></div>
<p>使用该模块的<pre class="crayon-plain-tag">get_prerequisites()</pre> 函数，可以分析一个可执行文件的依赖。将可执行文件的路径传递给此函数，其会输出运行此文件必须的依赖库的列表，<span style="background-color: #c0c0c0;">包括传递性依赖</span>。该函数使用各平台上的Native工具：dumpbin（Windows）、otool（Mac）、ldd（Linux）进行依赖分析。</p>
<div class="blog_h3"><span class="graybg">BundleUtilities.cmake</span></div>
<p>使用该模块的<pre class="crayon-plain-tag">fixup_bundle()</pre> 函数，可以依据可执行文件的相对位置，拷贝和修复共享库（依赖）。</p>
<p>对于Mac的bundle应用，需要的共享库会被嵌入到bundle中，并调用install_name_tool生成一个自包含bundle。</p>
<p>对于Widnows，需要的共享库会被拷贝到exe所在目录，可执行文件运行时会自动寻找并加载。</p>
<p>要使用fixup_bundle()函数，首先安装某个可执行目标，然后创建一个可以在安装时执行的CMake脚本，在此脚本中调用：</p>
<pre class="crayon-plain-tag">include (BundleUtilities)
# 安装树中的可执行文件的路径
set (bundle "${CMAKE_INSTALL_PREFIX}/myExecutable@CMAKE_EXECUTABLE_SUFFIX@")
# 无法通过依赖分析到达的依赖库的列表
set (other_libs "")
# 可以寻找到前置依赖库的目录的列表
set (dirs "@LIBRARY_OUTPUT_PATH@")

# 调用
fixup_bundle("${bundle}" "${other_libs}" "${dirs}")</pre>
<div class="blog_h2"><span class="graybg">导入和导出目标</span></div>
<p>CMake 2.6开始，支持在两个CMake工程之间导入导出目标。</p>
<div class="blog_h3"><span class="graybg">导入</span></div>
<p>导入目标这一机制，用于将项目外部的磁盘文件转换为<span style="background-color: #c0c0c0;">逻辑的CMake目标</span>。在调用add_executable、add_library命令时，传递<pre class="crayon-plain-tag">IMPORTED</pre> 选项，即可定义导入目标。CMake<span style="background-color: #c0c0c0;">不会为导入目标生成构建文件</span>，导入目标仅仅用于便利的引用外部的可执行文件和库。</p>
<p>下面的例子定义了一个导入的可执行文件，仅仅将其作为命令调用：</p>
<pre class="crayon-plain-tag"># 声明一个名为generator的导入目标
add_executable(generator IMPORTED)
# 设置目标的实际位置
set_property(TARGET generator PROPERTY IMPORT_LOCATION "/path/to/generator")
# 调用自定义命令，即添加一条定制的构建规则
# 底层构建系统执行类似这样的命令/path/to/generator /project/binary/dir/generated.c
add_custom_command(OUTPUT generated.c COMMAND generator generated.c)</pre>
<p>下面的例子定义了一个导入的库，并与之链接：</p>
<pre class="crayon-plain-tag">add_library(foo IMPORTED)

# Linux
set_property(TARGET foo PROPERTY IMPORTED_LOCATION "/path/to/libfoo.a")
# Windows下需要同时导入.lib和.dll
set_property(TARGET foo PROPERTY IMPORTED_LOCATION "/path/to/libfoo.dll")
set_property(TARGET foo PROPERTY IMPORTED_IMPLIB "/path/to/libfoo.lib")
# 具有多个构建配置的库，可以作为单个目标导入
set_property(TARGET foo PROPERTY IMPORTED_LOCATION_RELEASE "/path/to/libfoo.a")
set_property(TARGET foo PROPERTY IMPORTED_LOCATION_DEBUG   "/path/to/debug/libfoo.a")
add_executable(myexe src1.c)
target_link_libraries(myexe foo)</pre>
<div class="blog_h3"><span class="graybg">导出</span></div>
<p>尽管导入机制很有用，但是作为导入者来说，你必须知道目标在磁盘的位置。</p>
<p>使用导出机制，可以在提供目标文件的同时，提供一个文件，帮助其它工程导入。联合使用<pre class="crayon-plain-tag">install(TARGETS)</pre> 和<pre class="crayon-plain-tag">install(EXPORTS)</pre> 可以在安装目标的同时，把CMake文件也安装到机器上：</p>
<pre class="crayon-plain-tag">add_executable(generator generator.c)
# EXPORT选项导致生成一个助手文件，该文件是一个CMake脚本，可以让其它工程方便的导入generator
install(TARGET generator DESTINATION lib/myporj/generators EXPORT myproj-targets)
# 安装助手文件
install(EXPORT myproj-targets DESTINATION lib/myproj)</pre>
<p>助手文件的内容可以是：</p>
<pre class="crayon-plain-tag"># get_filename_component命令拥有得到一个全路径的某个部分
# 第一个参数：结果变量；第二个参数：待解析的路径；第三个参数，需要得到的部分，可以是DIRECTORY/NAME/EXT/PATH...
# CMAKE_CURRENT_LIST_FILE当前正在处理文件的路径
get_filename_component(_self "${CMAKE_CURRENT_LIST_FILE}" PATH)
# 解析出安装前缀的绝对路径
get_filename_component(PREFIX "${_self}/../.." ABSOLUTE)
# 添加导入目标
add_executable(generator IMPORTED)
# 通过计算出的路径引用目标
set_property(TARGET generator PROPERTY IMPORTED_LOCATION "${PREFIX}/lib/myproj/generators/generator")</pre>
<p>注意上面这个脚本依据自身位置动态计算出目标位置，即使移动安装目录，也不会失效。 </p>
<p>其它工程只需要包含助手文件即可：</p>
<pre class="crayon-plain-tag">include(/lib/myproj/myproj-targets.cmake)
# generator已经导入
add_custom_command(OUTPUT generated.c COMMAND generator generated.c)</pre>
<p>注意，单个助手文件可以容纳多个目标，甚至这些目标不在同一个目录中：</p>
<pre class="crayon-plain-tag"># A/CMakeLists.txt
add_executable(generator generator.c)
install(TARGETS generator DESTINATION lib/myproj/generators EXPORT myproj-targets)
# B/CMakeLists.txt
add_library(foo STATIC foo1.c)
install(TARGETS foo DESTINATION lib EXPORT myproj-targets)  #导出为同一个EXPORT

# CMakeLists.txt
add_subdirectory(A)
add_subdirectory(B)
install(EXPORT myproj-targets DESTINATION lib/myproj)</pre>
<div class="blog_h3"><span class="graybg">从构建树导出</span></div>
<p>典型情况下，在第三方工程需要导入之前，当前工程已经构建并安装，因此导出一般是基于安装树的。</p>
<p>CMake直接从构建树导出 ，这样第三方工程可以参考构建树来导入，这样就可以避免安装当前工程了。</p>
<p>使用<pre class="crayon-plain-tag">export</pre> 命令可以直接从构建树生成一个助手文件：</p>
<pre class="crayon-plain-tag">add_executable(generator generator.c)
export (TARGETS generator FILE myproj-exports.cmake)</pre>
<p>第三方工程可以include当前工程构建树下的myproj-exprots.cmake文件，其中包含导入generator需要的全部信息。</p>
<p>这种导出方式在交叉编译场景下可以用到。</p>
<div class="blog_h1"><span class="graybg">系统探测</span></div>
<p>系统探测，即检测在其上构建的系统的各种环境信息，是构建跨平台库或者应用程序的关键因素。</p>
<div class="blog_h2"><span class="graybg">使用头文件和库</span></div>
<p>很多C/C++程序依赖于外部的库，然后在编译和链接一个工程时，如何找到已经存在的头文件和库并不容易。因为开发程序的机器，和构建并安装程序的机器中，库的安装位置可能不一样。CMake提供多种特性，辅助开发者把外部库集成到工程中。 </p>
<p>与集成外部库相关的命令包括：<pre class="crayon-plain-tag">find_library</pre> 、<pre class="crayon-plain-tag">find_path</pre> 、<pre class="crayon-plain-tag">find_program</pre> 、<pre class="crayon-plain-tag">find_package</pre> 。对于大部分C/C++库，使用前两个命令一般足够和系统上已安装的库进行链接，这两个命令分别可以用来定位库文件、头文件所在目录。举例：</p>
<pre class="crayon-plain-tag"># 寻找一个库
find_library(
        TIFF_LIBRARY
        NAMES tiff tiff2  #只需要库的basename，不需要平台特定的前缀、后缀。前面的库优先
        #额外的路径，支持Windows注册表条目，例如[HKEY_CURRENT_USER\\Software\\Path;Build1]
        PATHS /usr/local/lib /usr/lib       #前面的路径优先
)
# 寻找一般性的文件，仅支持一个待查找文件，支持多个路径
find_path(
        TIFF_INCLUDES
        tiff.h
        /usr/local/include /usr/include
)
include_directories(${TIFF_INCLUDES})
add_executable(mytiff mytiff.c)
target_link_libraries(mytiff ${TIFF_LIBRARY})</pre>
<p>注意：</p>
<ol>
<li>find_*命令总是会寻找PATH环境变量</li>
<li>find_*命令会自动创建对应的缓存条目（文件没找到的情况下值为<pre class="crayon-plain-tag">VAR-NOTFOUND</pre> ），便于用户手工修改。这样即使CMake没有找到文件，用户还可以手工的修复</li>
</ol>
<div class="blog_h2"><span class="graybg">系统属性</span></div>
<p>在跨平台软件中，应当避免使用平台特定的代码，例如：</p>
<pre class="crayon-plain-tag">// 基于系统的判断
#ifdef defined(SUN) &amp;&amp; defined(HPUX)
    foobar();
#endif</pre>
<p> 这会降低代码的可移植性，每当需要支持新的系统，都要改变代码。即便非要使用宏，也最好<span style="background-color: #c0c0c0;">使用基于特性，而不是基于系统</span>的判断。可以改造上述代码为：</p>
<pre class="crayon-plain-tag">#ifdef HAS_FOOBAR_FUNC
    forbar();
#endif</pre>
<p>通过<pre class="crayon-plain-tag">try_compile</pre> 、<pre class="crayon-plain-tag">try_run</pre> 命令，CMake可以用来自动生成类似上面的HAS_***宏定义。这些命令编译/执行一小段代码，以探测系统特性：</p>
<pre class="crayon-plain-tag">try_compile(
        HAS_FOOBAR_FUNC
        ${CMAKE_BINARY_DIR}
        ${PROJECT_SOURCE_DIR}/testFoobar.c  #尝试调用forbar()函数
)</pre>
<p>如果编译成功，则CMake变量HAS_FOOBAR_FUNC为真。我们可以通过<pre class="crayon-plain-tag">add_definitions</pre> 命令或者配置头文件（更好），来设置HAS_FOOBAR_FUNC为宏定义。</p>
<p>如果单纯的编译并不够，还需要获知探测代码的运行结果，可以使用：</p>
<pre class="crayon-plain-tag">int main() {
    union {
        int i;
        char c;
    } u;
    u.i = 65;
    exit( u.c == 'A' );
} </pre><br />
<pre class="crayon-plain-tag">try_run(
        RUN_RESULT_VAR  #尝试运行的返回结果
        COMPILE_RESULT_VAR  #编译结果
        ${CMAKE_BINARY_DIR}
        ${PROJECT_SOURCE_DIR}/Modules/TestByteOrder.c
        OUTPUTVAR OUTPUT  #运行的任何输出
)</pre>
<p>运用上述命令的运行结果，可以根据字节序的不同来定制构建过程或者设置宏定义。对于较小的测试程序，可以不特定编写文件，使用<pre class="crayon-plain-tag">file</pre> 命令即可：</p>
<pre class="crayon-plain-tag">file(WRITE ${CMAKE_BINARY_DIR}/tmp/testc "int main(){return 0;}")</pre>
<div class="blog_h3"><span class="graybg">预定义try_*宏</span></div>
<p>在CMake/Modules中预定义了若干CMake用，可简化日常工作。这些宏常常需要查看当前<pre class="crayon-plain-tag">CMAKE_REQUIRED_FLAGS</pre> 、<pre class="crayon-plain-tag">CMAKE_REQUIRED_LIBRARIES</pre> 变量的值，以便添加额外的编译标记，或者链接以测试：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 30%; text-align: center;">模块</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CheckFunctionExists.cmake</td>
<td>检查一个特定的C函数是否存在于系统中。接受两个参数，第一个参数是待测试的函数名，第二个参数是存放测试结果的变量<br />该宏会查看上述两个变量</td>
</tr>
<tr>
<td>CheckIncludeFile.cmake</td>
<td>检查一个头文件是否存在于系统中。第一个参数是头文件名称，第二个参数是存放测试结果的变量，第三个参数是可选的编译标记，如果不指定，使用CMAKE_REQUIRED_FLAGS</td>
</tr>
<tr>
<td>CheckIncludeFileCXX.cmake</td>
<td>与上面类似，但是用于C++程序。第一个参数是头文件名称，第二个参数是存放测试结果的变量，第三个参数是可选的编译标记</td>
</tr>
<tr>
<td>CheckLibraryExists.cmake</td>
<td>检查一个库是否存在于系统。接受4个参数：待测试库名称、库中待测试函数名称、库的寻找位置、测试结果<br />该宏会查看上述两个变量</td>
</tr>
<tr>
<td>CheckSymbolExists.cmake</td>
<td>检查某个符号是否在头文件中定义。接受3个参数：待测试符号名称、尝试包含的头文件列表、测试结果<br />该宏会查看上述两个变量</td>
</tr>
<tr>
<td>CheckTypeSize.cmake</td>
<td>确定某个类型的长度（字节数）。接受2个参数：待测试类型、测试结果<br />该宏会查看上述两个变量</td>
</tr>
<tr>
<td>CheckVariableExists.cmake</td>
<td>检查某个全局变量是否存在。接受2个参数：待测试全局变量名称、测试结果。仅用于C变量<br />该宏会查看上述两个变量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">查找包</span></div>
<p>CMake提供<pre class="crayon-plain-tag">find_package(Package [version])</pre> 命令来查找符合<span style="background-color: #c0c0c0;">CPack</span>包规则的软件包。</p>
<p>该命令可以在两个模式下运行：</p>
<ol>
<li>Module模式：此模式下CMake会依次扫描<pre class="crayon-plain-tag">CMAKE_MODULE_PATH</pre>、CMake安装目录。尝试寻找到一个名称为<pre class="crayon-plain-tag">Find&lt;Package&gt;.cmake</pre> 的查找模块。如果找到则加载之，并调用其来寻找目标包的全部组件。查找模块针对特定包编写，它了解此包的全部版本，能找到包的库或者其它文件。CMake提供了很多常用的查找模块</li>
<li>Config模式：如果Module模式下没有定位到查找模块，命令自动切换到Config模式（你也可以显式的调用该模式）。在该模式下，命令会寻找<span style="background-color: #c0c0c0;">包配置文件（package configuration file）</span>：目标包提供的、一个名为<pre class="crayon-plain-tag">&lt;Package&gt;Config[Version].cmake</pre> 或者<pre class="crayon-plain-tag">&lt;package&gt;-config[-version].cmake</pre> 的文件。只要给出包的名称，命令就知道从何处寻找包配置文件，可能的位置是<pre class="crayon-plain-tag">&lt;prefix&gt;/lib/&lt;package&gt;/&lt;package&gt;-config.cmake</pre> </li>
</ol>
<div class="blog_h3"><span class="graybg">内置查找模块</span></div>
<p>CMake的内置查找模块，在找到包后，一般会定义一系列的变量供当前工程使用：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 30%; text-align: center;">变量名称约定</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>&lt;PKG&gt;_INCLUDE_DIRS</td>
<td>包的头文件所在目录</td>
</tr>
<tr>
<td>&lt;PKG&gt;_LIBRARIES</td>
<td>包提供的库的完整路径</td>
</tr>
<tr>
<td>&lt;PKG&gt;_DEFINITIONS</td>
<td>使用包时，编译代码需要用的宏定义</td>
</tr>
<tr>
<td>&lt;PKG&gt;_EXECUTABLE</td>
<td>包提供的PKG工具所在目录</td>
</tr>
<tr>
<td>&lt;PKG&gt;_&lt;TOOL&gt;_EXECUTABLE</td>
<td>包提供的TOOL工具所在目录</td>
</tr>
<tr>
<td>&lt;PKG&gt;_ROOT_DIR</td>
<td>PKG包的安装根目录</td>
</tr>
<tr>
<td>&lt;PKG&gt;_VERSION_&lt;VER&gt;</td>
<td>如果PKG的VER版本被找到，则定义为真</td>
</tr>
<tr>
<td>&lt;PKG&gt;_&lt;CMP&gt;_FOUND</td>
<td>如果PKG的CMP组件被找到，则定义为真</td>
</tr>
<tr>
<td>&lt;PKG&gt;_FOUND</td>
<td>如果PKG被找到则定义为真</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">为编译传递参数</span></div>
<p>要传递参数给编译器，可以指定命令行，或者使用一个预先配置好的头文件。</p>
<div class="blog_h3"><span class="graybg">add_definitions</span></div>
<p>调用<pre class="crayon-plain-tag">add_definitions</pre> 命令，可以向编译器传递宏定义：</p>
<pre class="crayon-plain-tag">#定义一个布尔的缓存条目
option(DENIG_BUILD "Enable debug messages")
if (DEBUG_BUILD)
    #添加宏定义
    add_definitions(-DDEBUG_MSG)
endif ()</pre>
<p>如果要细粒度的控制宏定义，可以设置目录、目标、源文件的<pre class="crayon-plain-tag">COMPILE_DEFINITIONS</pre> 属性：</p>
<pre class="crayon-plain-tag">add_library(mylib src1.c src2.c)
# 可以添加APPEND选项，追加值而不是覆盖
set_property(DIRECTORY PROPERTY COMPILE_DEFINITIONS A AV=1)
set_property(TARGET mylib PROPERTY COMPILE_DEFINITIONS B BV=2)
set_property(SOURCE src1.c PROPERTY COMPILE_DEFINITIONS C CV=3)
# 执行上述命令后，编译参数分别为：
# src1.c -DA -DAV=1 -DB -DBV=2 -DC -DCV=3
# src2.c -DA -DAV=1 -DB -DBV=2 
# main.c -DA -DAV=1</pre>
<div class="blog_h3"><span class="graybg">配置头文件</span></div>
<p>这种方式更可维护，大部分工程应当使用该方式。应用程序只需要引入预先配置好的头文件即可，不必编写复杂的CMake规则。</p>
<p>我们可以把头文件看作一种配置文件，而要生成配置文件，可使用<pre class="crayon-plain-tag">configure_file(input output [@ONLY])</pre> 命令，此命令需要一个“输入文件”，输入文件可以包含三种变量定义方式：</p>
<pre class="crayon-plain-tag">// 第一种方式
#cmakedefine VARIABLE
// 如果VARIABLE为真，则输出：
#define VARIABLE
// 否则输出：
/* #undef VARIABLE */

//第二种方式，直接输出变量的值。如果confgure_file命令传递@ONLY选项，则这种方式不能使用
${VARIABLE}

//第三种方式，直接输出变量的值
@VARIABLE@</pre>
<p>配置文件应当输出到二进制树，而不是源码树，避免代码污染。因为单个CMake的源码树可以供多种构建树或平台使用，它们生成的配置文件常常是不一样的。你可能需要用include_directories命令将配置文件所在目录作为头文件目录。</p>
<div class="blog_h2"><span class="graybg">创建包配置文件</span></div>
<p>除了头文件以外，<pre class="crayon-plain-tag">configure_file</pre> 命令亦可用来生成包的配置文件。在“查找包”一节我们已经讨论过，包配置文件供其它工程发现本包。</p>
<div class="blog_h1"><span class="graybg">定制命令与目标</span></div>
<p>很多时候，“构建”一个工程不仅仅是简单的编译、链接、拷贝，额外的工作——例如利用文档工具生成文档—— 需要在构建过程中完成。</p>
<p>通过定制命令和目标，CMake可以被扩展以支持任意的任务（或者说规则）。</p>
<div class="blog_h2"><span class="graybg">可移植性</span></div>
<p>定制命令时，面临的一个重要问题是可移植性：</p>
<ol>
<li>各平台上用于完成一项任务的工具不同，以复制文件为例，UNIX使用cp命令，Windows则使用copy命令</li>
<li>目标在各平台上的名字不同，例如库x在UNIX上可能叫libx.so，Windows上则叫x.dll</li>
</ol>
<p>CMake提供了两个主要工具，解决上面两个可移植性问题。</p>
<div class="blog_h3"><span class="graybg">cmake -E命令</span></div>
<p>使用<pre class="crayon-plain-tag">cmake -E arguments</pre> 调用，可以执行一些跨平台的操作，在CMakeLists.txt中可以通过定制命令来调用cmake命令，cmake这个可执行文件可以用变量<pre class="crayon-plain-tag">CMAKE_COMMAND</pre> 引用。</p>
<p>支持的操作（arguments）包括：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">操作</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>chdir dir command args</td>
<td>改变当前目录为dir然后执行指定的命令</td>
</tr>
<tr>
<td>copy file destination</td>
<td>拷贝文件</td>
</tr>
<tr>
<td>copy_if_different infile outfile</td>
<td>如果两个文件不一样，则从infile拷贝到outfile</td>
</tr>
<tr>
<td>copy_directory source destination</td>
<td>拷贝source目录（包括子目录）中全部文件到destination目录</td>
</tr>
<tr>
<td>remove file1 file2...</td>
<td>从磁盘上删除文件</td>
</tr>
<tr>
<td>echo string</td>
<td>打印到标准输出</td>
</tr>
<tr>
<td>time command args</td>
<td>运行一个命令并且计算耗时</td>
</tr>
</tbody>
</table>
<p>CMake不限制你仅使用cmake命令，事实上你可以使用任何命令，但是要注意可移植性问题。一个通用的实践是，通过<pre class="crayon-plain-tag">find_program</pre> 找到一个程序，然后在定制命令中调用之。</p>
<div class="blog_h3"><span class="graybg">系统特征变量</span></div>
<p>CMake提供了一系列预定义的变量，描述系统的特征：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>EXE_EXTENSION</td>
<td>可执行文件的扩展名，Windows平台是.exe，UNIX是空</td>
</tr>
<tr>
<td>CMAKE_CFG_INTDIR</td>
<td>诸如VS、XCode这样的开发环境，根据构建配置的不同，使用不同的子目录，例如Debug、Release<br />在一个库、可执行文件、目标文件上执行一个命令时，你往往需要知道它们的完整路径<br />改变了在UNIX上通常是<pre class="crayon-plain-tag">./</pre> 而VS则是<pre class="crayon-plain-tag">$(INTDIR)/</pre> </td>
</tr>
<tr>
<td>CMAKE_CURRENT_BINARY_DIR</td>
<td>与当前CMakeList文件关联的输出目录的完整路径<br />可能与PROJECT_BINARY_DIR（当前工程二进制树的顶级目录）不同</td>
</tr>
<tr>
<td>CMAKE_CURRENT_SOURCE_DIR</td>
<td>与当前CMakeList文件关联的源码目录的完整路径<br />可能与PROJECT_SOURCE_DIR（当前工程源码树的顶级目录）不同</td>
</tr>
<tr>
<td>EXECUTABLE_OUTPUT_PATH</td>
<td>某些工程指定可执行文件需要生成到的目录，该变量指示其完整路径</td>
</tr>
<tr>
<td>LIBRARY_OUTPUT_PATH</td>
<td>某些工程指定库文件需要生成到的目录，该变量指示其完整路径</td>
</tr>
<tr>
<td>CMAKE_SHARED_MODULE_PREFIX</td>
<td rowspan="2">共享模块文件的前后缀</td>
</tr>
<tr>
<td>CMAKE_SHARED_MODULE_SUFFIX</td>
</tr>
<tr>
<td>CMAKE_SHARED_LIBRARY_PREFIX</td>
<td rowspan="2">共享库文件的前后缀</td>
</tr>
<tr>
<td>CMAKE_SHARED_LIBRARY_SUFFIX</td>
</tr>
<tr>
<td>CMAKE_LIBRARY_PREFIX</td>
<td rowspan="2">静态库文件的前后缀</td>
</tr>
<tr>
<td>CMAKE_LIBRARY_SUFFIX</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">在目标上使用add_custom_command</span></div>
<p>add_custom_command有两个主要的签名：TARGET、OUTPUT，分别用于为目标或者文件添加额外的规则。其中TARGET签名语法如下：</p>
<pre class="crayon-plain-tag">add_custom_command(
    #目标的名称
    TARGET target  
    #执行触发时机：
    #pre_build，在目标任何依赖文件被构建之前执行
    #pre_link，在所有依赖已经构建好，但是尚未链接时执行
    #post_build，在目标已经构建好后执行
    PRE_BUILD | PRE_LINK | POST_BUILD
    # command为可执行文件的名称
    COMMAND command [ARGS arg1 arg2 ... ]
    [COMMAND command [ARGS arg1 arg2 ... ] ...]
    # 注释，在定制命令运行时打印
    [COMMENT comment]
)</pre>
<p>下面是一个例子，在目标构建好后立即拷贝之： </p>
<pre class="crayon-plain-tag">add_executable(myExe my.c)
get_target_property(EXE_LOC myExe LOCATION)
add_custom_command(
    TARGET myExe
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} ARGS -E copy ${EXE_LOC} /QC/files
)</pre>
<div class="blog_h2"><span class="graybg">使用add_custom_command生成文件</span></div>
<p>add_custom_command的另外一个用途是指定生成一个文件的规则。这种情况下，已有的用于生成目标文件的规则被替换掉。语法如下：</p>
<pre class="crayon-plain-tag">add_custom_command (
    # 指定命令运行生成的结果文件，最好指定完整路径
    OUTPUT output1 [output2 ...]
    # 需要执行的命令
    COMMAND command [ARGS [args ...]]
    [ COMMAND command [ARGS [args ...]] ...]
    # 主要用于VS
    [MAIN_DEPENDENCY depend]
    # 命令依赖于的文件，最好指定完整路径（依赖是目标则不必），这些文件中的任何一个变化后，命令都需要重新执行
    [DEPENDS [depends ...]]
    [COMMENT comment]
)</pre>
<p>在某些库的构建过程中，例如TIFF，会先编译并构建一个可执行文件，再用此可执行文件生成还有系统特定信息的源码，而此源码参与库的最终构建。这种场景下，可以使用add_custom_command来生成源码。</p>
<div class="blog_h2"><span class="graybg">添加定制目标</span></div>
<p>CMake支持除了库、可执行文件之外的，更一般概念上的目标，称为定制目标。生成文档、运行测试、更新Web服务器都可以抽象为目标。</p>
<p>要添加定制目标，需要调用下面的命令：</p>
<pre class="crayon-plain-tag">add_custom_target(
    # name为目标的名称，如果使用Makefile生成器，你可以调用make name来生成此目标
    # ALL，表示该目标包含在ALL_BUILD目标中，自动构建
    name [ALL]
    # 执行的命令
    [command arg arg ...]
    # 此目标依赖的文件的列表，最好指定完整路径（依赖是目标则不必），这些文件可以是add_custom_command(OUTPUT)生成的
    [DEPENDS dep dep ...]
)</pre>
<div class="blog_h1"><span class="graybg">使用CMake进行交叉编译</span></div>
<p>所谓交叉编译，就是指软件在一个平台（Build host）上构建，而在另外一个平台（Target Platform）上运行。目标平台往往是另外一个OS甚至没有OS，也常常使用与构建平台不一样的硬件，这导致目标平台根本不能运行开发环境。交叉编译的典型应用是嵌入式开发，程序需要在路由器、传感器之类的特殊硬件上运行。</p>
<p>交叉编译依赖于<span style="background-color: #c0c0c0;">工具链</span>。工具链是针对目标平台的一整套工具，包括<span style="background-color: #c0c0c0;">编译器、链接器</span>，以及<span style="background-color: #c0c0c0;">目标平台的全套头文件、库</span>。</p>
<p>从2.6开始，CMake完整的支持交叉编译，包括Linux-Windows交叉编译，或者PC-嵌入设备交叉编译。在交叉编译场景下，会面临以下问题：</p>
<ol>
<li>CMake无法自动的检测目标平台</li>
<li>CMake无法在默认系统目录寻找库、头文件</li>
<li>构建出来的可执行文件无法运行</li>
</ol>
<p>CMake区分构建平台、运行平台的信息，让给用户可以解决交叉编译相关的问题，避免例如运行虚拟机的额外需求。</p>
<div class="blog_h2"><span class="graybg">工具链文件</span></div>
<p>通过一个所谓工具链（Toolchain）文件，我们可以告知CMake<span style="background-color: #c0c0c0;">关于目标平台的任何必要信息</span>。CMakeList.txt必须被调整以适应目标平台和构建平台具有不同属性的情况。下面是一个Linux下基于MinGW交叉编译器，交叉编译Windows程序使用的工具链文件：</p>
<pre class="crayon-plain-tag">#目标平台的名称
set( CMAKE_SYSTEM_NAME Windows )

#指定C/C++编译器为交叉编译器，只有交叉编译器才知道如何构建目标平台上的二进制文件
set( CMAKE_C_COMPILER i586-mingw32msvc-gcc )
set( CMAKE_CXX_COMPILER i586-mingw32msvc-g++ )

#指定目标平台环境的位置，这一位置在构建平台中，但是存放的是目标平台需要的头文件、库
set( CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc /home/alex/mingw-install )

#调整find_***命令的行为
#仅在构建平台上寻找程序
set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER )
#仅在目标平台环境中寻找头文件、库
set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY )
set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY )</pre>
<p>通过下面的命令，可以指示CMake使用上述工具链文件：</p>
<pre class="crayon-plain-tag">cd src/build
cmake -DCMAKE_TOOLCHAIN_FILE=~/TC-mingw.cmake ...</pre>
<p>CMAKE_TOOLCHAIN_FILE必须在最初运行时设置，后续其值会保存为缓存条目。每个目标平台一般只需要一个工具链文件。</p>
<div class="blog_h3"><span class="graybg">工具链文件变量</span></div>
<p>在工具链文件中，可能需要设置以下变量：</p>
<table class=" full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CMAKE_SYSTEM_NAME</td>
<td>
<p>该变量必须设置，指示目标平台的名称。典型的名称如Linux、Windows，如果目标平台是无OS的嵌入式平台，设置为Generic。此名称会用来生成平台文件的名称，例如Linux.cmake、Windows-gcc.cmake</p>
<p>手工设置此变量后，CMake认为当前是在做交叉编译，自动设置变量<pre class="crayon-plain-tag">CMAKE_CROSSCOMPILING</pre> 为真</p>
</td>
</tr>
<tr>
<td>CMAKE_SYSTEM_VERSION</td>
<td>可选的，目标平台的版本</td>
</tr>
<tr>
<td>CMAKE_SYSTEM_PROCESSOR</td>
<td>
<p>可选的，目标平台处理器或者硬件的名称。CMake用此变量加载文件：<pre class="crayon-plain-tag">${CMAKE_SYSTEM_NAME}-COMPILER_ID-${CMAKE_SYSTEM_PROCESSOR}.cmake</pre> ，该文件用来修改设置，例如目标的编译标记</p>
<p>仅在你需要为目标平台指定特殊的编译设置时，才需要设置该变量</p>
</td>
</tr>
<tr>
<td>CMAKE_C_COMPILER</td>
<td>
<p>指定C编译器的完整路径或者名字。如果指定为完整路径，这CMake倾向于到对应目录寻找binutils、linker、C++编译器等其它内容。如果指定的编译器是一个GNU交叉编译器，则CMake会自动寻找到对应的C++编译器，例如从arm-elf-gcc找到arm-elf-c++</p>
<p>C编译器亦可通过环境变量CC设置</p>
</td>
</tr>
<tr>
<td>CMAKE_CXX_COMPILER</td>
<td>
<p>指定C++编译器的完整路径或者名字。对于GNU工具链，只需要设置CMAKE_C_COMPILER，此变量不必设置</p>
<p>C++编译器亦可通过环境变量CXX设置</p>
</td>
</tr>
<tr>
<td>CMAKE_FIND_ROOT_PATH</td>
<td>
<p>指定一组包含了目标平台环境的目录，这些目录供所有find_**命令使用</p>
<p>假设目标平台环境安装在/opt/eldk/ppc_74xx，设置变量为此路径。find_library寻找jpeg库时会最终定位到/opt/eldk/ppc_74xx/lib/libjpeg.so</p>
</td>
</tr>
<tr>
<td>CMAKE_FIND_ROOT_PATH_MODE_PROGRAM</td>
<td rowspan="3">
<p>分别设置find_program、find_library、find_include命令的默认行为。可以设置为：</p>
<ol>
<li>NEVER CMAKE_FIND_ROOT_PATH对命令无效</li>
<li>ONLY 仅在CMAKE_FIND_ROOT_PATH目录中搜索</li>
<li>BOTH 默认值，都搜索</li>
</ol>
</td>
</tr>
<tr>
<td>CMAKE_FIND_ROOT_PATH_MODE_LIBRARY</td>
</tr>
<tr>
<td>CMAKE_FIND_ROOT_PATH_MODE_INCLUDE</td>
</tr>
</tbody>
</table>
<p>注：形如CMAKE_SYSTEM_XXX的变量，总是在描述目标平台。如果要描述当前构建平台，可以使用相应的CMAKE_HOST_SYSTEM_XXX变量。</p>
<div class="blog_h2"><span class="graybg">系统探测</span></div>
<p>CMake提供了一些变量，用于粗粒度的测试系统特征：</p>
<ol>
<li>目标平台类型指示变量：UNIX、WIN32、APPLE</li>
<li>构建平台类型指示变量：CMAKE_HOST_UNIX、 CMAKE_HOST_WIN32、CMAKE_HOST_APPLE</li>
</ol>
<p>更加细化的测试变量，可以使用上节提到的CMAKE_SYSTEM_XXX、CMAKE_HOST_SYSTEM_XXX变量。</p>
<div class="blog_h3"><span class="graybg">编译检查</span></div>
<p>CMake中使用CHECK_INCLUDE_FILES、CHECK_C_SOURCE_RUNS等宏来测试平台属性，这些宏通常使用try_compile、try_run命令。</p>
<p>try_run无法正常运行，因为交叉编译出的可执行文件不能在构建平台上运行。try_run被调用时，它首先尝试编译，如果成功它会检查CMAKE_CROSSCOMPILING变量，该变量为真的话它不会尝试运行，而是设置两个缓存变量，供用户后续修改。考虑下面这个例子：</p>
<pre class="crayon-plain-tag">try_run(
        SHARED_LIBRARY_PATH_TYPE
        SHARED_LIBRARY_PATH_INFO_COMPILED
        ${PROJECT_BINARY_DIR}/CMakeTmp
        ${PROJECT_SOURCE_DIR}/CMake/SharedLPathInfo.cxx
        OUTPUT_VARIABLE OUTPUT
        ARGS "LDPATH"
)</pre>
<p>如果SharedLPathInfo.cxx编译成功，SHARED_LIBRARY_PATH_INFO_COMPILED被设置为真。而交叉编译时无法运行可执行文件，因此CMake创建一个缓存条目：<pre class="crayon-plain-tag">SHARED_LIBRARY_PATH_TYPE=PLEASE_FILL_OUT-FAILED_TO_RUN</pre> ，该条目必须被手工的设置为SharedLPathInfo的在目标平台上的退出码。如果指定了OUTPUT_VARIABLE选项，CMake还会创建一个缓存条目<pre class="crayon-plain-tag">SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT=PLEASE_FILL_OUT-NOTFOUND</pre> ，该条目必须手工设置为SharedLPathInfo在目标平台上的标准输出/错误。</p>
<p>要手工运行，可以考虑构建目录下的<pre class="crayon-plain-tag">cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE</pre> 到目标平台下执行。</p>
<p>除了设置缓存条目外，还可以把运行结果记录到<pre class="crayon-plain-tag">${CMAKE_BINARY_DIR}/TryRunResults.cmake</pre> 中，该文件由CMake自动创建，其中包含<span style="background-color: #c0c0c0;">所有CMake无法确定的变量</span>，并记录可执行文件位置、源码位置、运行参数等信息。我们可以根据运行结果填充这些变量的值，然后为cmake调用缓存：</p>
<pre class="crayon-plain-tag">cmake -C ~/TryRunResults-myproj-eldk-ppc.cmake</pre>
<div class="blog_h2"><span class="graybg">交叉编译HelloWorld</span></div>
<p>本节示例一个完整的交叉编译的过程。</p>
<div class="blog_h3"><span class="graybg">寻找工具链</span></div>
<p>交叉编译的第一步是寻找合适的工具链，如果你已经安装好工具链，则可以跳过这一步。</p>
<p>不同工程——包括基于Linux的PDA和嵌入式设备厂商的——在Linux上处理交叉编译的途径不同，它们有自己的构建流程和工具链。CMake可以使用这些工具链，只要它们是基于普通文件系统的。</p>
<p>一个提供较为完整的目标平台环境的工具链套件是Embedded Linux Development Toolkit（ELDK），该套件支持ARM、PowerPC、MIPS等目标平台。ELDK或者其它工具链可以被安装在构建平台的任意位置，例如：</p>
<pre class="crayon-plain-tag"># 工具链
/home/alex/eldk-mips/usr/bin
# 目标平台环境
/home/alex/eldk-mpis/mips_4KC/</pre>
<div class="blog_h3"><span class="graybg">创建工具链文件</span></div>
<p>下载并安装好工具链后，下一步就是编写工具链文件，注意我们在上面提到过，每个工具链只需要一个这样的文件：</p>
<pre class="crayon-plain-tag">set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_C_COMPILER /home/alex/eldk-mips/usr/bin/mips_4KC-gcc)
set(CMAKE_CXX_COMPILER /home/alex/eldk-mips/usr/bin/mips_4KC-g++)
set(CMAKE_FIND_ROOT_PATH /home/alex/eldk-mips/mips-4KC /home/alex/eldk-mips-extra-install)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE  ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY  ONLY)</pre>
<p>工具链文件可以存放在任何地方，推荐将其存放在统一的目录，方便其它工程重用。 </p>
<div class="blog_h3"><span class="graybg">工程文件</span></div>
<pre class="crayon-plain-tag">int main(){
    exit(0)
}</pre><br />
<pre class="crayon-plain-tag">project(Hello)
add_executable(Hello Hello.c)</pre>
<div class="blog_h3"><span class="graybg">执行构建</span></div>
<pre class="crayon-plain-tag">mkdir eldk-mips
cd eldk-mips
# 调用cmake，指定工具链文件
cmake -DCMAKE_TOOLCHAIN_FILE=~/CPP/cmake/toolchains/ToolChain-eldk-mips4K.cmake ..
# 生成器已经生成Makefile，可以构建
make VERBOSE=1</pre>
<div class="blog_h2"><span class="graybg">微控制器的交叉编译</span></div>
<p>不但支持具有OS的目标平台的交叉编译，CMake还可以针对那些没有OS的微控制器进行交叉编译。</p>
<p>本节的例子使用SDCC编译器，该编译器可以运行于主流系统下，支持8/16位微控制器的交叉编译。</p>
<p>首先，还是编写工具链文件：</p>
<pre class="crayon-plain-tag">set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR i8501)
set(CMAKE_C_COMPILER "D:/CPP/sdcc/bin/sdcc.exe")</pre>
<p>对于没有OS的目标平台，其系统名称设置为Generic。CMake假设Generic平台<span style="background-color: #c0c0c0;">不支持共享库</span>。 </p>
<p>很多微控制器工程不需要依赖任何外部库，因此往往不需要设置影响find_**的变量。</p>
<p>CMakeLists文件：</p>
<pre class="crayon-plain-tag"># 明确声明使用C语言，因为SDCC不支持C++
project (Blink C)
add_library(blink blink.c)
add_executable(hello main.c)JSONCPP_INCLUDE_DIRS
target_link_libraries(hello blink)</pre>
<p>执行构建：</p>
<pre class="crayon-plain-tag">rem 使用MS NMake生成器驱动构建
cmake -G "NMake Makefiles" -DCMAKE_TOOLCHAIN_FILE=D:/CPP/cmake/toolchains/Toolchain-sdcc.cmake ..
rem 执行构建
namke</pre>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg"><a id="cmake"></a>cmake</span></div>
<p>此命令是cmake的命令行接口</p>
<div class="blog_h3"><span class="graybg">命令格式</span></div>
<pre class="crayon-plain-tag">cmake [options] &lt;path-to-source&gt;
cmake [options] &lt;path-to-existing-build&gt;</pre>
<div class="blog_h3"><span class="graybg">命令选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-C &lt;initial-cache&gt;</td>
<td>预加载一个脚本，以生成缓存</td>
</tr>
<tr>
<td>-D &lt;var&gt;:&lt;type&gt;=&lt;value&gt;</td>
<td>定义一个cmake缓存条目</td>
</tr>
<tr>
<td>-U &lt;globbing_expr&gt;</td>
<td>从缓存中移除匹配的条目</td>
</tr>
<tr>
<td>-G &lt;generator-name&gt;</td>
<td>指定一个生成器（构建系统）</td>
</tr>
<tr>
<td>-T &lt;toolset-name&gt;</td>
<td>指定生成器支持的工具集</td>
</tr>
<tr>
<td>-Wno-dev</td>
<td>抑制开发者警告</td>
</tr>
<tr>
<td>-E</td>
<td>cmake命令模式</td>
</tr>
<tr>
<td>-i</td>
<td>以向导模式运行</td>
</tr>
<tr>
<td>-L[A][H]</td>
<td>列出所有非进阶缓存变量</td>
</tr>
<tr>
<td>--build &lt;dir&gt;</td>
<td>在dir中构建二进制树</td>
</tr>
<tr>
<td>--find-package</td>
<td>以类似于pkg-config的方式来查找包</td>
</tr>
<tr>
<td>--graphviz=[file]</td>
<td>生成依赖的graphviz</td>
</tr>
<tr>
<td>--system-information [file]</td>
<td>输出系统信息</td>
</tr>
<tr>
<td>--debug-trycompile</td>
<td>不删除trycompile构建树</td>
</tr>
<tr>
<td>--debug-output </td>
<td>以调试模式运行</td>
</tr>
<tr>
<td>--trace</td>
<td>以trace模式运行（更多调试信息）</td>
</tr>
<tr>
<td>--warn-uninitialized</td>
<td>对未初始化值进行警告</td>
</tr>
<tr>
<td>--warn-unused-vars</td>
<td>对未使用变量进行警告</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>
<pre class="crayon-plain-tag">cmake 
    -DCMAKE_BUILD_TYPE:STRING=Debug           # 构建配置
    -DCMAKE_INSTALL_PREFIX:STRING=/usr        # 安装位置</pre>
<p>其它变量参考<a href="#useful-vars">这里</a>。 </p>
<div class="blog_h3"><span class="graybg">如何启用C++ 11支持</span></div>
<p>可以使用CMake 3.1引入的变量：</p>
<pre class="crayon-plain-tag">set(CMAKE_CXX_STANDARD 11) </pre>
<p>如果要对老版本CMake兼容，参考下面的宏：</p>
<pre class="crayon-plain-tag">macro(use_cxx11)
  if (CMAKE_VERSION VERSION_LESS "3.1")
    if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
      set (CMAKE_CXX_FLAGS "-std=gnu++11 ${CMAKE_CXX_FLAGS}")
    endif ()
  else ()
    set (CMAKE_CXX_STANDARD 11)
  endif ()
endmacro(use_cxx11)</pre>
<div class="blog_h3"><span class="graybg">如何添加C/C++宏定义 </span></div>
<p>全局性宏定义：</p>
<pre class="crayon-plain-tag"># 定义宏MACRO_NAME，注意前面的-D
add_definitions(-DMACRO_NAME)
# 赋值
add_definitions(-DMACRO_NAME=${value})</pre>
<p>针对某个目标：</p>
<pre class="crayon-plain-tag"># 仅仅能用于 add_executable() 或 add_library() 添加的目标

# 格式：
target_compile_definitions(&lt;target&gt;
  # PUBLIC、INTERFACE可以将宏定义传递给target的PUBLIC、INTERFACE条目
  &lt;INTERFACE|PUBLIC|PRIVATE&gt; [items1...]
  [&lt;INTERFACE|PUBLIC|PRIVATE&gt; [items2...] ...])

# 示例：
target_compile_definitions(hello PRIVATE A=1 B=0)

# 你也可以直接设置目标属性  COMPILE_DEFINITIONS</pre>
<div class="blog_h3"><span class="graybg">如何使用不同版本的GCC</span></div>
<p>使用环境变量：</p>
<pre class="crayon-plain-tag">export CC=/home/alex/CPP/lib/gcc/7.2.0/bin/gcc
export CXX=/home/alex/CPP/lib/gcc/7.2.0/bin/g++</pre>
<p>在基于CLion开发时，将上述内容添加到clion.sh脚本中 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/cmake">CMake学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/cmake/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Eclipse 4.3.2开发环境搭建</title>
		<link>https://blog.gmem.cc/eclipse-kepler-sr2-setup</link>
		<comments>https://blog.gmem.cc/eclipse-kepler-sr2-setup#comments</comments>
		<pubDate>Mon, 27 Oct 2014 02:34:40 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[CDT]]></category>
		<category><![CDATA[Eclipse]]></category>
		<category><![CDATA[JDT]]></category>
		<category><![CDATA[PDT]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=2201</guid>
		<description><![CDATA[<p>安装Eclipse 4.3.2 下载地址列表：http://www.eclipse.org/downloads/packages/release/Kepler/SR2 对于Java开发，建议选择：Eclipse IDE for Java EE Developers如果同时需要使用BIRT进行报表开发，建议选择：Eclipse IDE for Java and Report Developers对于C/C++开发，建议选择：Eclipse IDE for C/C++ Developers 根据你的操作系统，选择合适的版本进行下载，下载完毕后，解压到：D:\JavaEE\ide\eclipse\4.3.2（以下称：%ECLIPSE_HOME%），编辑%ECLIPSE_HOME%eclipse.ini，参考下面的例子修改： [crayon-69d3dc89ae991697521588/] 建立Eclipse的快捷方式：D:JavaEE\ide\eclipse\4.3.2\eclipse.exe <a class="read-more" href="https://blog.gmem.cc/eclipse-kepler-sr2-setup">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/eclipse-kepler-sr2-setup">Eclipse 4.3.2开发环境搭建</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">安装Eclipse 4.3.2</span></div>
<p>下载地址列表：<a href="http://www.eclipse.org/downloads/packages/release/Kepler/SR2" target="_blank">http://www.eclipse.org/downloads/packages/release/Kepler/SR2 </a></p>
<p>对于Java开发，建议选择：Eclipse IDE for Java EE Developers<br />如果同时需要使用BIRT进行报表开发，建议选择：Eclipse IDE for Java and Report Developers<br />对于C/C++开发，建议选择：Eclipse IDE for C/C++ Developers</p>
<div>根据你的操作系统，选择合适的版本进行下载，下载完毕后，解压到：D:\JavaEE\ide\eclipse\4.3.2（以下称：%ECLIPSE_HOME%），编辑%ECLIPSE_HOME%eclipse.ini，参考下面的例子修改：<br />
<pre class="crayon-plain-tag">-vm D:/JavaEE/jdk/x64/1.7/bin/javaw.exe
-startup
plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20140116-2212
-product
org.eclipse.epp.package.reporting.product
--launcher.defaultAction
openFile
-showsplash
org.eclipse.platform
--launcher.defaultAction
openFile
--launcher.appendVmargs
-vmargs
-Dosgi.requiredJavaVersion=1.6
-Xms1536m
-Xmx1536m
-XX:PermSize=256m
-XX:MaxPermSize=256m
-Xmn576m
-Xverify:none
-XX:ParallelGCThreads=8
-XX:+DisableExplicitGC
-Duser.name="WangZhen"</pre></p>
<p>建立Eclipse的快捷方式：D:JavaEE\ide\eclipse\4.3.2\eclipse.exe -nl "en_US"</p>
</div>
<div class="blog_h2"><span class="graybg">安装JDK</span></div>
<p>到Oracle网站下载1.4-1.8版本的JDK，分别安装到D:\JavaEE\jdk\x64|x86的1.4-1.8子目录下</p>
<p>Linux下的JDK安装步骤，可以参考：<a href="/eclipse-for-ubuntu14" target="_blank">Ubuntu14.04下Eclipse开发环境的搭建</a></p>
<div id="install-eclipse-plugins" class="blog_h2"><span class="graybg">安装Eclipse插件</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 插件名称</td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 180px;">m2eclipse</td>
<td>
<p>Maven的Eclipse插件</p>
<p>Update Site：http://download.eclipse.org/technology/m2e/releases</p>
<p>注意：取消选择“Show only the latest versions of available software”并安装1.4.x版本，因为1.5.x版本需要Luna才能支持</p>
</td>
</tr>
<tr>
<td>m2e-wtp</td>
<td>
<p>m2eclipse插件与WTP平台的集成</p>
<p>Update Site：http://download.eclipse.org/m2e-wtp/releases/kepler/</p>
</td>
</tr>
<tr>
<td>JRebel for Eclipse</td>
<td>
<p>JRebel与Eclipse的集成，JRebel提供快速开发的支持，可以免重启配置多种框架组件和热部署Java类</p>
<p>Update Site：http://update.zeroturnaround.com/update-site</p>
</td>
</tr>
<tr>
<td>Subclipse</td>
<td>
<p>与SVN的集成</p>
<p>Update Site：http://subclipse.tigris.org/update_1.10.x</p>
</td>
</tr>
<tr>
<td>AJDT</td>
<td>
<p>对AspectJ开发的支持<br />Update Site：<br />http://download.eclipse.org/tools/ajdt/43/update<br />http://download.eclipse.org/tools/ajdt/43/dev/update/</p>
<p>AJDT Configurator，支持 aspectj-maven-plugin与Eclipse的对接<br />Update Site：http://dist.springsource.org/release/AJDT/configurator/</p>
</td>
</tr>
<tr>
<td>CDT</td>
<td>
<p>对C/C++开发的支持</p>
<p>Update Site：http://download.eclipse.org/tools/cdt/releases/kepler</p>
</td>
</tr>
<tr>
<td>PDT</td>
<td>
<p>对PHP开发的支持</p>
<p>Update Site：http://download.eclipse.org/tools/pdt/updates/3.3.1</p>
</td>
</tr>
<tr>
<td>Spket IDE</td>
<td>
<p>ExtJS的编辑器插件 </p>
<p>Update Site：http://www.agpad.com/update/</p>
</td>
</tr>
<tr>
<td>Eclipse HTML Editor</td>
<td>
<p>HTML、JSP等代码的编辑器插件</p>
<p>该插件依赖GEF框架：http://download.eclipse.org/tools/gef/updates/releases/</p>
<p>到http://amateras.sourceforge.jp/cgi-bin/fswiki_en/wiki.cgi?page=EclipseHTMLEditor 下载插件并放到%ECLIPSE_HOME%\dropins下</p>
</td>
</tr>
<tr>
<td>EclEmma</td>
<td>
<p>代码覆盖率插件</p>
<p>Update Site：http://update.eclemma.org/</p>
</td>
</tr>
<tr>
<td>Jeeeyul's Themes</td>
<td>
<p>主题插件，可以美化Eclipse UI</p>
<p>Update Site：http://eclipse.jeeeyul.net/update/</p>
</td>
</tr>
<tr>
<td>Eclipse Class Decompiler</td>
<td>
<p>反编译插件</p>
<p>http://feeling.sourceforge.net/update</p>
</td>
</tr>
<tr>
<td>Atlassian Connector</td>
<td>
<p>JIRA的Mylyn连接器</p>
<p>http://update.atlassian.com/atlassian-eclipse-plugin/rest/e3.7</p>
</td>
</tr>
</tbody>
</table>
<div id="workspace-config" class="blog_h2"><span class="graybg">Eclipse工作区配置</span></div>
<p>打开Eclipse，进入Window – Preferences，参考下表进行配置</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">配置项 </td>
<td style="text-align: center;">配置说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>
<p>勾选：Always run in background、Show heap status</p>
</td>
</tr>
<tr>
<td>General - Web Browser</td>
<td>
<p>点选 Use external web browser</p>
</td>
</tr>
<tr>
<td>
<p>General - Appearance</p>
</td>
<td>
<p>修改Theme为：JeeeUl's themes - Custom Theme</p>
<p>展开节点：Color and Fonts，右侧树展开到：Basic - Text Fonts，修改字体为：XSung Mono.Lanting 11</p>
<p>展开节点：JeeeUl's Themes - Presets，导入：<a href="/uploads/JavaEE/config/WangZhen.epf" target="_blank">WangZhen.epf</a></p>
<p>展开节点：JeeeUl's Themes，点击右侧窗体右上角的向下小箭头，选择User Preset - WangZhen</p>
</td>
</tr>
<tr>
<td>General - Workspace</td>
<td>
<p>勾选：Refresh using native hooks or polling</p>
<p>Text file encoding：UTF-8</p>
<p>New text file line delimiter：Windows</p>
</td>
</tr>
<tr>
<td>General - Editors</td>
<td>
<p>展开节点：Text Editors，按如下设置：</p>
<p>Undo history size: 1000，勾选：Insert spaces for tabs、Show line numbers</p>
<p>展开节点：Text Editors - Spelling，取消勾选：Enable spell checking</p>
<p>展开节点：File Associations，修改编辑器关联性：</p>
<ol>
<li>*.js：Spket JavaScript Editor</li>
</ol>
</td>
</tr>
<tr>
<td>General - Startup and...</td>
<td>勾选：Refresh workspace on startup</td>
</tr>
<tr>
<td>General - Keys</td>
<td>解除Alt + /与Word Completion的关联，设置Content Assist的快捷键为Alt + /</td>
</tr>
<tr>
<td>Java - Code Style</td>
<td>
<p>展开节点：Organize Import</p>
<p>Number of static imports needed for.* (e.g. 'java.Iang.Math.*'):设置为1</p>
<p>展开节点：Code Templates，点击右侧Import按钮，导入：<a href="/uploads/JavaEE/config/java-code-templates.xml" target="_blank">java-code-templates.xml</a></p>
<p>展开节点：Formatter，点击右侧Import按钮，导入：<a href="/uploads/JavaEE/config/java-formatter.xml" target="_blank">java-formatter.xml</a></p>
</td>
</tr>
<tr>
<td>Java - Installed JREs</td>
<td>
<p>依次添加上面安装的JDK 1.4-1.8，以JDK1.6为例：</p>
<p>JRE HOME：D:\JavaEE\jdk\x64\1.6</p>
<p>JRE_NAME：1.6</p>
<p>Default VM args：-Xms64m -Xmx1024m -XX:PermSize=64m -XX:MaxPermSize=256m</p>
</td>
</tr>
<tr>
<td>Java - Appearance</td>
<td>
<p>展开节点：Members Sort Order，勾选：Sort members in same category by visibility，把Private排到最下面</p>
</td>
</tr>
<tr>
<td>JavaScript - Code Style</td>
<td>
<p>展开节点：Formatter，点击右侧Import按钮，导入：<a href="/uploads/JavaEE/config/js-formatter.xml" target="_blank">js-formatter.xml</a></p>
</td>
</tr>
<tr>
<td>Amateras - Formatter</td>
<td>
<p>取消勾选：Use tab as indentation</p>
<p>Indentation size:4</p>
<p>Maximum line width:128</p>
</td>
</tr>
<tr>
<td>PHP - Code Style</td>
<td>
<p>展开节点：Formatter，点击右侧Import按钮，导入：<a href="/uploads/JavaEE/config/php-formatter.xml" target="_blank">php-formatter.xml</a></p>
</td>
</tr>
<tr>
<td style="width: 180px;">C/C++ - Code Style</td>
<td>
<p>展开节点：Formatter，点击右侧Import按钮，导入：<a href="/uploads/CPP/config/c-formatter.xml" target="_blank">c-formatter.xml</a></p>
</td>
</tr>
<tr>
<td>
<p>Spket - Editors </p>
</td>
<td>
<p>勾选：Convert tabs to spaces</p>
<p>展开节点：JavaScript Editor - Formatter，点击右侧Import按钮，导入：<a href="/uploads/JavaEE/config/spket-js-formatter.xml" target="_blank">spket-js-formatter.xml</a></p>
</td>
</tr>
<tr>
<td>
<p>Spket - JavaScript Profiles</p>
</td>
<td>
<ol>
<li>点击New，输入框中填写：EXT JS，确定</li>
<li>选中EXT JS，点击右侧按钮Default</li>
<li>点击Add Library，选择ExtJS，确定</li>
<li>选中ExtJS，点击Add File，添加<a href="/uploads/JavaEE/config/extjs/sdk.jsb3" target="_blank">sdk.jsb3</a>，确定后，点选All Debug子节点</li>
<li>选中ExtJS，点击Add File，添加<a href="/uploads/JavaEE/config/extjs/ext-all-debug-w-comments.js" target="_blank">ext-all-debug-w-comments.js</a></li>
</ol>
</td>
</tr>
<tr>
<td>Server</td>
<td>
<p>展开节点：Runtime Environments，点击添加，分别添加Tomcat6、7，名称保持默认</p>
<p>Tomcat从官网下载，并放入D:\JavaEE\container\tomcat\目录下，以版本号为子目录名</p>
<p>打开视图Servers，右键New -Server，创建服务器实例，Server name命名规则：Runtime Env名称:端口，创建6060 8080 9090三个实例。完毕后，双击Servers配置：</p>
<ol>
<li>启用JRebel支持：勾选：Enable JRebel agent、Enable debug logging</li>
<li>端口修改以防冲突对于6060、9090两个实例，将端口中的8分别替换为6、9</li>
<li>Timeout配置为450、150</li>
</ol>
</td>
</tr>
<tr>
<td>Run/Debug - Console</td>
<td>
<p>Console buffer size (characters): 800000</p>
<p>Displayed tab width: 4</p>
</td>
</tr>
<tr>
<td>Web</td>
<td>
<p>展开节点：CSS Files - Editor，设置Line Width为128，点选Indent using spaces，Indentation size为4</p>
<p>展开节点：HTML Files - Editor，设置同上</p>
<p>展开节点：JSP Files，设置Encoding为ISO 10646（UTF-8）</p>
</td>
</tr>
<tr>
<td>XML</td>
<td>展开节点：XML Files - Editor，设置Line Width为128，点选Indent using spaces，Indentation size为4</td>
</tr>
<tr>
<td>Subclipse</td>
<td>打开透视图：SVN Repository Exploring，添加必要的SVN仓库地址</td>
</tr>
<tr>
<td>JRebel</td>
<td>
<p>打开透视图：JRebel Config Center。</p>
<p>点击右侧Activate/Update License，输入你的License</p>
<p>点击Advanced选项卡：</p>
<ol>
<li>Logging Level设置为Debug，勾选Show JRebel messages in standard output</li>
<li>Log File设置为：D:\JavaEE\temp\jrebel.log</li>
<li>Eclipse Notifications：全部取消勾选</li>
<li>Privacy Settings：全部取消勾选</li>
</ol>
</td>
</tr>
<tr>
<td>Maven</td>
<td>
<p>勾选：Download Artifact Sources、Download Artifact JavaDoc</p>
<p>展开节点：Archetypes，右侧点击Add Remote Catalog，添加公司的原型库，例如：http://192.168.0.200:8801/nexus/service/local/repositories/releases/content/archetype-catalog.xml</p>
<p>展开节点：Installations，点击右侧Add按钮，添加D:\JavaEE\maven\3.0.5，并设为Eclipse默认Maven</p>
<p>展开节点：User Settings，修改设置为：<a href="/uploads/JavaEE/config/maven-settings.xml" target="_blank">maven-settings.xml</a>，点击Update Settings</p>
<p>打开视图：Maven Repositories，更新所有索引</p>
</td>
</tr>
<tr>
<td>Mylyn - Tasks</td>
<td>
<p>Synchronize with repositories every 5 </p>
<p>Week Start：Monday</p>
<p>打开Task Repositories视图，右键Add Task Repository，类型选择JIRA，根据公司配置输入JIRA连接信息</p>
</td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/eclipse-kepler-sr2-setup">Eclipse 4.3.2开发环境搭建</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/eclipse-kepler-sr2-setup/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于MinGW的海康视频监控开发</title>
		<link>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw</link>
		<comments>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw#comments</comments>
		<pubDate>Thu, 15 May 2014 03:38:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Graphic]]></category>
		<category><![CDATA[MinGW]]></category>
		<category><![CDATA[Multimedia]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=3535</guid>
		<description><![CDATA[<p>工程配置 项 说明  集成开发环境 Eclipse 4.3.2 + MinGW工具链（TDM-GCC 4.8） 工程配置 工程类型：C++ Project宏定义：_WIN32、UNICODE头文件路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\include依赖库：HCNetSDK库路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\dllLinker flags：-mwindows 基于SDK直接解码的直播样例 [crayon-69d3dc89aeb8b764525251/]</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw">基于MinGW的海康视频监控开发</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>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">项</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>集成开发环境</td>
<td>Eclipse 4.3.2 + MinGW工具链（TDM-GCC 4.8）</td>
</tr>
<tr>
<td>工程配置</td>
<td>工程类型：C++ Project<br />宏定义：_WIN32、UNICODE<br />头文件路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\include<br />依赖库：HCNetSDK<br />库路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\dll<br />Linker flags：-mwindows</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基于SDK直接解码的直播样例</span></div>
<pre class="crayon-plain-tag">/*
 * SDKDirectPlayBack.cpp
 *
 *  Created on: May 14, 2014
 *      Author: WangZhen
 */
#include &lt;stdio.h&gt;
#include &lt;windows.h&gt;
#include "HCNetSDK.h"
#include &lt;time.h&gt;

class RealPlayInfo
{
    public:
        LONG lUserID;
        LONG lRealPlayHandle;
        RealPlayInfo() :
                lUserID( -1 ), lRealPlayHandle( -1 )
        {
        }
        ;
        bool isSucceed()
        {
            return this-&gt;lUserID &gt;= 0;
        }
};
void CALLBACK g_ExceptionCallBack( DWORD dwType, LONG lUserID, LONG lHandle, void* pUser )
{
    switch ( dwType )
    {
        case EXCEPTION_RECONNECT :
            printf( "Reconnecting...%d\n", time( NULL ) );
            break;
        default :
            break;
    }
}

RealPlayInfo* StartRealPlay( HWND hWnd, char* ip, int port = 8000, int chnl = 1, char* user = "admin", char* pswd = "12345" )
{
    RealPlayInfo* info = new RealPlayInfo;
    NET_DVR_DEVICEINFO_V30 struDeviceInfo;
    info-&gt;lUserID = NET_DVR_Login_V30( ip, 8000, user, pswd, &amp;struDeviceInfo ); //登录
    if ( info-&gt;lUserID &lt; 0 )
    {
        printf( "Failed to logged on to %s, Error Code: %d\n", ip, NET_DVR_GetLastError() );
        return info;
    }
    else
    {
        printf( "Logon successful.\n" );
    }
    NET_DVR_SetExceptionCallBack_V30( 0, NULL, g_ExceptionCallBack, NULL );
    //启动预览并设置回调数据流
    NET_DVR_PREVIEWINFO struPlayInfo = { 0 };
    //使用SDK直接解码
    struPlayInfo.hPlayWnd = hWnd;
    //预览通道号
    struPlayInfo.lChannel = chnl;
    //0主码流，1子码流
    struPlayInfo.dwStreamType = 0;
    //0 TCP，1 UDP，2 多播，3 RTP，4 RTP/RTSP，5 RSTP/HTTP
    struPlayInfo.dwLinkMode = 0;
    //0- 非阻塞取流，1- 阻塞取流
    struPlayInfo.bBlocked = 1;
    info-&gt;lRealPlayHandle = NET_DVR_RealPlay_V40( info-&gt;lUserID, &amp;struPlayInfo, NULL, NULL );
    if ( info-&gt;lRealPlayHandle &lt; 0 )
    {
        printf( "Failed to start RealPlay.\n" );
        NET_DVR_Logout( info-&gt;lUserID );
        return info;
    }
    else
    {
        printf( "RealPlay started successfully.\n" );
    }
    return info;
}
void StopRealPlay( RealPlayInfo* info )
{
    if ( !info-&gt;isSucceed() ) return;
    printf( "Stopping RealPlay.\n" );
    //停止预览
    NET_DVR_StopRealPlay( info-&gt;lRealPlayHandle );
    //注销用户
    NET_DVR_Logout( info-&gt;lUserID );
}

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ); //窗口过程声明

int main()
{
    HINSTANCE hInstance = GetModuleHandle( NULL );
    static TCHAR szAppName[] = TEXT( "HikSDKDirectPlayBack" );
    HWND hWnd;
    MSG msg;
    WNDCLASS wndclass;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION );
    wndclass.hCursor = LoadCursor( NULL, IDC_ARROW );
    wndclass.hbrBackground = ( HBRUSH ) GetStockObject( BLACK_BRUSH );
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;
    RegisterClass( &amp;wndclass );
    int w = 640;
    int h = 480;
    hWnd = CreateWindow(
            szAppName,
            TEXT("MinGW海康SDK直播实例"),
            WS_OVERLAPPEDWINDOW,
            (GetSystemMetrics(SM_CXSCREEN) - w)/2,
            (GetSystemMetrics(SM_CYSCREEN) - h)/2,
            w,
            h,
            NULL,
            NULL,
            hInstance,
            NULL
    );
    printf( "Main window created.\n" );
    printf( "Prepare to render main window.\n" );
    ShowWindow( hWnd, 10 );
    UpdateWindow( hWnd );
    while ( GetMessage( &amp;msg, NULL, 0, 0 ) )
    {
        TranslateMessage( &amp;msg );
        DispatchMessage( &amp;msg );
    }
    return msg.wParam;
}
//窗口过程定义
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam )
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rect;
    static RealPlayInfo* info;
    switch ( message )
    {
        case WM_CREATE :
            NET_DVR_Init();
            NET_DVR_SetConnectTime( 2000, 1 );
            NET_DVR_SetReconnect( 10000, true );
            info = StartRealPlay( hwnd, "192.168.0.196" );
            return 0;
        case WM_DESTROY :
            PostQuitMessage( 0 );
            StopRealPlay( info );
            NET_DVR_Cleanup();
            return 0;
    }
    return DefWindowProc( hwnd, message, wParam, IParam );
}</pre> 
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw">基于MinGW的海康视频监控开发</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
