<?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>Tony Bai &#187; Kernel</title>
	<atom:link href="http://tonybai.com/tag/kernel/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Thu, 30 Apr 2026 23:46:25 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>当 Go 遇上 GPU：用 CUDA 释放千倍算力的实战指南</title>
		<link>https://tonybai.com/2026/01/21/integrating-cuda-in-go/</link>
		<comments>https://tonybai.com/2026/01/21/integrating-cuda-in-go/#comments</comments>
		<pubDate>Tue, 20 Jan 2026 23:31:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Block]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[CUDA]]></category>
		<category><![CDATA[DeviceMemory]]></category>
		<category><![CDATA[GlobalMemory]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherCon2025]]></category>
		<category><![CDATA[GPU]]></category>
		<category><![CDATA[Grid]]></category>
		<category><![CDATA[HighPerformanceComputing]]></category>
		<category><![CDATA[HostMemory]]></category>
		<category><![CDATA[HPC]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[MatrixMultiplication]]></category>
		<category><![CDATA[NsightSystems]]></category>
		<category><![CDATA[nvcc]]></category>
		<category><![CDATA[NVIDIA]]></category>
		<category><![CDATA[ParallelComputing]]></category>
		<category><![CDATA[PCIe]]></category>
		<category><![CDATA[purego]]></category>
		<category><![CDATA[register]]></category>
		<category><![CDATA[runtimeKeepAlive]]></category>
		<category><![CDATA[SamBurns]]></category>
		<category><![CDATA[SharedMemory]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[Tiling]]></category>
		<category><![CDATA[全局内存]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[寄存器]]></category>
		<category><![CDATA[并行计算]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[显存]]></category>
		<category><![CDATA[矩阵乘法]]></category>
		<category><![CDATA[算力]]></category>
		<category><![CDATA[高性能计算]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5751</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/21/integrating-cuda-in-go 大家好，我是Tony Bai。 长期以来，高性能计算（HPC）和 GPU 编程似乎是 C++ 开发者的专属领地。Go 语言虽然在并发和服务端开发上表现卓越，但在触及 GPU 算力时，往往显得力不从心。 然而，在最近的 GopherCon 2025 上，软件架构师 Sam Burns 打破了这一刻板印象。他展示了如何通过 Go 和 CUDA 的结合，让 Gopher 也能轻松驾驭 GPU 的海量核心，实现惊人的并行计算能力。 本文将带你深入这场演讲的核心，从 GPU 的独特架构到内存模型，再通过一个完整的、可运行的矩阵乘法示例，手把手教你如何用 Go 驱动 NVIDIA 显卡释放澎湃算力。 为什么 Go 开发者需要关注 GPU？ 在摩尔定律逐渐失效的今天，CPU 的单核性能提升已遇瓶颈。虽然 CPU 拥有极低的延迟、卓越的分支预测能力和巨大的缓存，但它的核心数量（通常在几十个量级）限制了其处理大规模并行任务的能力。 相比之下，GPU (Graphics Processing Unit) 走的是另一条路。它拥有成千上万个核心。虽然单个 GPU 核心的频率较低，且缺乏复杂的逻辑控制能力，但它们能同时处理海量简单的计算任务。这使得 GPU 成为以下场景的绝佳选择： 图形处理与视频转码 AI 模型推理与训练（神经网络本质上就是大规模矩阵运算） [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/21/integrating-cuda-in-go">本文永久链接</a> &#8211; https://tonybai.com/2026/01/21/integrating-cuda-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p>长期以来，高性能计算（HPC）和 GPU 编程似乎是 C++ 开发者的专属领地。Go 语言虽然在并发和服务端开发上表现卓越，但在触及 GPU 算力时，往往显得力不从心。</p>
<p>然而，在最近的 GopherCon 2025 上，软件架构师 Sam Burns 打破了这一刻板印象。他<a href="https://www.youtube.com/watch?v=d1R8BS-ccNk">展示了如何通过 Go 和 CUDA 的结合</a>，让 Gopher 也能轻松驾驭 GPU 的海量核心，实现惊人的并行计算能力。</p>
<p>本文将带你深入这场演讲的核心，从 GPU 的独特架构到内存模型，再通过一个<strong>完整的、可运行的矩阵乘法示例</strong>，手把手教你如何用 Go 驱动 NVIDIA 显卡释放澎湃算力。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/distributed-system-guide-qr.png" alt="img{512x368}" /></p>
<h2>为什么 Go 开发者需要关注 GPU？</h2>
<p>在摩尔定律逐渐失效的今天，CPU 的单核性能提升已遇瓶颈。虽然 CPU 拥有极低的延迟、卓越的分支预测能力和巨大的缓存，但它的核心数量（通常在几十个量级）限制了其处理大规模并行任务的能力。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-2.png" alt="" /></p>
<p>相比之下，GPU (Graphics Processing Unit) 走的是另一条路。它拥有成千上万个核心。虽然单个 GPU 核心的频率较低，且缺乏复杂的逻辑控制能力，但它们能同时处理海量简单的计算任务。这使得 GPU 成为以下场景的绝佳选择：</p>
<ul>
<li><strong>图形处理与视频转码</strong></li>
<li><strong>AI 模型推理与训练</strong>（神经网络本质上就是大规模矩阵运算）</li>
<li><strong>物理模拟与科学计算</strong>（如流体力学、分子动力学）</li>
<li><strong>密码学与哈希碰撞</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-3.png" alt="" /></p>
<p>通过 Go 语言集成 CUDA，我们可以在享受 Go 语言高效开发体验（构建 API、微服务、调度逻辑）的同时，将最繁重的“脏活累活”卸载给 GPU，实现 <strong>CPU 负责逻辑，GPU 负责算力</strong> 的完美分工。</p>
<h2>GPU架构与CUDA编程模型速览——理解 GPU 的“兵团”</h2>
<p>在编写代码之前，我们需要理解 GPU 的独特架构。Sam Burns 用一个形象的比喻描述了 GPU 的线程模型。如果说 CPU 是几位精通各种技能的“专家”，那么 GPU 就是一支纪律严明、规模庞大的“兵团”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-4.png" alt="" /></p>
<p>而指挥这支兵团的指令集，我们称之为 <strong>“内核” (Kernel)</strong>。</p>
<h3>0. 什么是 Kernel？</h3>
<p>此 Kernel 非彼 Kernel（操作系统内核）。在 CUDA 语境下，<strong>Kernel 是一个运行在 GPU 上的函数</strong>。</p>
<p>当我们“启动”一个 Kernel 时，GPU 并不是简单地调用这个函数一次，而是<strong>同时启动成千上万个线程</strong>，每个线程都在独立执行这份相同的代码逻辑。每个线程通过读取自己独一无二的 ID（threadIdx），来决定自己该处理数据的哪一部分（比如图像的哪个像素，或矩阵的哪一行）。</p>
<h3>1. 线程模型：从 Thread 到 Grid</h3>
<p>理解了 Kernel，我们再看它是如何被调度执行的。CUDA 编程模型将计算任务分解为三个层级：</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-5.png" alt="" /></p>
<ul>
<li>线程 (Thread)：GPU 工作的最小单位。它类似于 CPU 的线程，但极其轻量。每个线程都有自己的 ID，负责处理数据的一小部分（例如图像中的一个像素，或矩阵中的一个元素）。</li>
<li>块 (Block)：一组线程的集合。一个 Block 内的线程运行在同一个流式多处理器 (SM) 上。<strong>关键点在于：同一个 Block 内的线程可以通过极快的“共享内存”进行协作和同步（__syncthreads()）</strong>。</li>
<li>网格 (Grid)：所有执行同一个内核函数（Kernel）的 Block 的集合。Grid 涵盖了整个计算任务。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-6.png" alt="" /></p>
<h3>2. 内存模型：速度与容量的权衡</h3>
<p>GPU 的内存架构比 CPU 更为复杂，理解它对于性能优化至关重要：</p>
<ul>
<li>寄存器 (Registers)：最快。每个线程私有，用于存储局部变量。数量有限，用多了会溢出到慢速内存。</li>
<li>共享内存 (Shared Memory)：极快（L1 缓存级别）。属于 Block 私有，是线程间通信的桥梁。<strong>优化 CUDA 程序的核心往往在于如何高效利用共享内存来减少全局内存访问。</strong></li>
<li>全局内存 (Global Memory)：较慢（显存，如 24GB GDDR6X）。所有线程可见，容量大但延迟高。</li>
<li>常量内存 (Constant Memory)：快（有缓存）。用于存储只读参数，适合广播给所有线程。</li>
</ul>
<p>编写高效 CUDA 代码的秘诀，就是尽可能让数据停留在寄存器和共享内存中，减少对全局内存的访问。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/integrating-cuda-in-go-7.png" alt="" /></p>
<h2>Go + CUDA 实战——跨越鸿沟</h2>
<p>理解了原理，现在让我们动手。我们将构建一个完整的 Go 项目，利用 GPU 并行计算两个矩阵的乘积。这个过程需要借助 <strong>CGO</strong> 作为桥梁。</p>
<h3>1. 项目目录结构</h3>
<pre><code class="text">go-cuda-cgo-demo/
├── main.go       # Go 主程序 (CGO 入口，负责内存分配和调度)
├── matrix.cu     # CUDA 内核代码 (在 GPU 上运行的 C++ 代码)
└── matrix.h      # C 头文件 (声明导出函数，供 CGO 识别)
</code></pre>
<h3>2. 编写 CUDA 内核 (matrix.cu)</h3>
<p>这是在 GPU 上运行的核心代码。我们定义一个 matrixMulKernel，每个线程利用自己的坐标 (x, y) 计算结果矩阵中的一个元素。</p>
<pre><code class="cpp">// matrix.cu
#include &lt;cuda_runtime.h&gt;
#include &lt;stdio.h&gt;

// CUDA Kernel: 每个线程计算 C[row][col] 的值
__global__ void matrixMulKernel(float *a, float *b, float *c, int width) {
    // 根据 Block ID 和 Thread ID 计算当前线程的全局坐标
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;

    if (row &lt; width &amp;&amp; col &lt; width) {
        float sum = 0;
        // 计算点积
        for (int k = 0; k &lt; width; k++) {
            sum += a[row * width + k] * b[k * width + col];
        }
        c[row * width + col] = sum;
    }
}

extern "C" {
    // 供 Go 调用的 C 包装函数
    // 负责显存分配、数据拷贝和内核启动
    void runMatrixMul(float *h_a, float *h_b, float *h_c, int width) {
        int size = width * width * sizeof(float);
        float *d_a, *d_b, *d_c;

        // 1. 分配 GPU 显存 (Device Memory)
        cudaMalloc((void **)&amp;d_a, size);
        cudaMalloc((void **)&amp;d_b, size);
        cudaMalloc((void **)&amp;d_c, size);

        // 2. 将数据从 Host (CPU内存) 复制到 Device (GPU显存)
        // 这一步通常是性能瓶颈，应尽量减少
        cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
        cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);

        // 3. 定义 Grid 和 Block 维度
        // 每个 Block 包含 16x16 = 256 个线程
        dim3 threadsPerBlock(16, 16);
        // Grid 包含足够多的 Block 以覆盖整个矩阵
        dim3 numBlocks((width + threadsPerBlock.x - 1) / threadsPerBlock.x,
                       (width + threadsPerBlock.y - 1) / threadsPerBlock.y);

        // 4. 启动内核！成千上万个线程开始并行计算
        matrixMulKernel&lt;&lt;&lt;numBlocks, threadsPerBlock&gt;&gt;&gt;(d_a, d_b, d_c, width);

        // 5. 将计算结果从 Device 传回 Host
        cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);

        // 6. 释放 GPU 内存
        cudaFree(d_a);
        cudaFree(d_b);
        cudaFree(d_c);
    }
}
</code></pre>
<h3>3. 定义 C 头文件 (matrix.h)</h3>
<pre><code class="c">// matrix.h
#ifndef MATRIX_H
#define MATRIX_H

void runMatrixMul(float *a, float *b, float *c, int width);

#endif
</code></pre>
<h3>4. 编写 Go 主程序 (main.go)</h3>
<p>在 Go 代码中，我们准备数据，并通过 CGO 调用 runMatrixMul。</p>
<pre><code class="go">// go-cuda-cgo-demo/main.go
package main

/*
#cgo LDFLAGS: -L. -lmatrix -L/usr/local/cuda/lib64 -lcudart
#include "matrix.h"
*/
import "C"
import (
    "fmt"
    "math/rand"
    "time"
    "unsafe"
)

const width = 1024 // 矩阵大小 1024x1024，共 100万次计算

func main() {
    size := width * width
    h_a := make([]float32, size)
    h_b := make([]float32, size)
    h_c := make([]float32, size)

    // 初始化矩阵数据
    rand.Seed(time.Now().UnixNano())
    for i := 0; i &lt; size; i++ {
        h_a[i] = rand.Float32()
        h_b[i] = rand.Float32()
    }

    fmt.Printf("Starting Matrix Multiplication (%dx%d) on GPU...\n", width, width)
    start := time.Now()

    // 调用 CUDA 函数
    // 使用 unsafe.Pointer 获取切片的底层数组指针，传递给 C
    C.runMatrixMul(
        (*C.float)(unsafe.Pointer(&amp;h_a[0])),
        (*C.float)(unsafe.Pointer(&amp;h_b[0])),
        (*C.float)(unsafe.Pointer(&amp;h_c[0])),
        C.int(width),
    )

    // 注意：在更复杂的场景中，需要使用 runtime.KeepAlive(h_a)
    // 来确保 Go GC 不会在 CGO 调用期间回收切片内存。

    elapsed := time.Since(start)
    fmt.Printf("Done. Time elapsed: %v\n", elapsed)

    // 简单验证：检查左上角元素
    fmt.Printf("Result[0][0] = %f\n", h_c[0])
}
</code></pre>
<h3>5. 编译与运行</h3>
<p>前提：确保你的机器安装了 NVIDIA Driver 和 CUDA Toolkit。nvcc是CUDA编译器工具链，可以将基于CUDA的代码翻译为GPU机器码。</p>
<p><strong>步骤一：编译 CUDA 代码</strong></p>
<pre><code class="bash">nvcc -c matrix.cu -o matrix.o
ar rcs libmatrix.a matrix.o
</code></pre>
<p><strong>步骤二：编译 Go 程序</strong></p>
<pre><code class="bash"># 链接本地的 libmatrix.a 和系统的 CUDA 运行时库
go build -o gpu-cgo-demo main.go
</code></pre>
<p><strong>步骤三：运行</strong></p>
<pre><code class="bash">./gpu-cgo-demo
</code></pre>
<p><strong>预期输出：</strong></p>
<pre><code class="text">Starting Matrix Multiplication (1024x1024) on GPU...
Done. Time elapsed: 611.815451ms
Result[0][0] = 262.440918
</code></pre>
<h2>性能优化——从能用到极致</h2>
<p>代码跑通只是第一步。Sam 推荐使用 NVIDIA 的 <strong>Nsight Systems</strong> (nsys) 来进行性能分析。你会发现，虽然 GPU 计算极快，但<strong>PCIe 总线的数据传输往往是最大的瓶颈</strong>。</p>
<p><strong>优化黄金法则：</strong></p>
<ol>
<li><strong>减少传输</strong>：PCIe 很慢。尽量一次性将所有数据传给 GPU，让其进行多次计算，最后再取回结果。</li>
<li><strong>利用共享内存 (Shared Memory)</strong>：Block 内的共享内存比全局显存快得多。在矩阵乘法中，可以利用它实现<strong>分块算法 (Tiling)</strong>，将小块矩阵加载到共享内存中复用，从而大幅减少显存带宽压力。</li>
</ol>
<h2>小结：Gopher 的新武器</h2>
<p>Go + CUDA 的组合，为 Go 语言打开了一扇通往高性能计算的大门。它证明了 Go 不仅是编写微服务的利器，同样可以成为驾驭底层硬件、构建计算密集型应用的强大工具。如果你正在处理大规模数据，不妨尝试将计算任务卸载给 GPU，你会发现，那个熟悉的蓝色 Gopher，也能拥有令人惊叹的爆发力。</p>
<p>资料链接：</p>
<ul>
<li>https://www.youtube.com/watch?v=d1R8BS-ccNk</li>
<li>https://sam-burns.com/posts/gophercon-25-go-faster/#gophercon-2025-new-york</li>
</ul>
<p>本文涉及的示例源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-gpu">这里</a>下载。</p>
<h2>附录：告别 CGO？尝试 PureGo 的无缝集成</h2>
<p>虽然 CGO 是连接 Go 和 C/C++ 的标准桥梁，但它也带来了编译速度变慢、工具链依赖等问题。有没有一种更“纯粹”的 Go 方式？</p>
<p>答案是有的。借助 <strong>PureGo</strong> 库，我们可以在不开启 CGO 的情况下，直接加载动态链接库 (.so / .dll) 并调用其中的符号。</p>
<p>让我们看看如何用 PureGo 重写上面的 main.go。</p>
<h3>1. 准备动态库</h3>
<p>首先，我们需要将 CUDA 代码编译为共享对象 (.so)，而不是静态库。</p>
<pre><code class="bash"># 编译为共享库 libmatrix.so
nvcc -shared -Xcompiler -fPIC matrix.cu -o libmatrix.so
</code></pre>
<h3>2. 编写 PureGo 版主程序 (go-cuda-purego-demo/main.go)</h3>
<pre><code class="go">// go-cuda-purego-demo/main.go
package main

import (
    "fmt"
    "math/rand"
    "runtime"
    "time"

    "github.com/ebitengine/purego"
)

const width = 1024

func main() {
    // 1. 加载动态库
    // 注意：在运行时，libmatrix.so 和 libcuder.so 必须在 LD_LIBRARY_PATH 中
    libMatrix, err := purego.Dlopen("libmatrix.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
    if err != nil {
        panic(err)
    }

    // 还需要加载 CUDA 运行时库，因为 libmatrix 依赖它
    _, err = purego.Dlopen("/usr/local/cuda/lib64/libcudart.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
    if err != nil {
        panic(err)
    }

    // 2. 注册 C 函数符号
    var runMatrixMul func(a, b, c *float32, w int)
    purego.RegisterLibFunc(&amp;runMatrixMul, libMatrix, "runMatrixMul")

    // 3. 准备数据 (与 CGO 版本相同)
    size := width * width
    h_a := make([]float32, size)
    h_b := make([]float32, size)
    h_c := make([]float32, size)

    rand.Seed(time.Now().UnixNano())
    for i := 0; i &lt; size; i++ {
        h_a[i] = rand.Float32()
        h_b[i] = rand.Float32()
    }

    fmt.Println("Starting Matrix Multiplication via PureGo...")
    start := time.Now()

    // 4. 直接调用！无需 CGO 类型转换
    runMatrixMul(&amp;h_a[0], &amp;h_b[0], &amp;h_c[0], width)

    // 5. 极其重要：保持内存存活
    // PureGo 调用是纯汇编实现，Go GC 无法感知堆栈上的指针引用
    // 必须显式保活，否则在计算期间 h_a 等可能被 GC 回收！
    runtime.KeepAlive(h_a)
    runtime.KeepAlive(h_b)
    runtime.KeepAlive(h_c)

    fmt.Printf("Done. Time: %v\n", time.Since(start))
    fmt.Printf("Result[0][0] = %f\n", h_c[0])
}
</code></pre>
<h3>3. 运行</h3>
<pre><code class="bash"># 无需 CGO，直接在go-cuda-purego-demo下运行
# 确保当前目录在 LD_LIBRARY_PATH 中
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
CGO_ENABLED=0 go run main.go
Starting Matrix Multiplication via PureGo...
Done. Time: 584.397195ms
Result[0][0] = 260.088806
</code></pre>
<p><strong>优势</strong>：</p>
<ul>
<li>编译飞快：没有 CGO 的编译开销。</li>
<li>零外部依赖：编译环境不需要安装 GCC 或 CUDA Toolkit，只要运行时环境有 .so 即可。这对于在轻量级 CI/CD 环境中构建分发包非常有用。</li>
</ul>
<p><strong>注意</strong>：PureGo 方案虽然优雅，但也失去了 CGO 的部分类型安全检查，且需要开发者更小心地管理内存生命周期 (runtime.KeepAlive)。</p>
<hr />
<p><strong>你的“算力”狂想</strong></p>
<p>Go + GPU 的组合，打破了我们对 Go 应用场景的想象边界。在你的业务场景中，有没有哪些计算密集型的任务（比如图像处理、复杂推荐算法、密码学计算）是目前 CPU 跑不动的？你是否会考虑用这种“混合动力”方案来重构它？</p>
<p><strong>欢迎在评论区分享你的脑洞或实战计划！</strong> 让我们一起探索 Go 的算力极限。</p>
<p><strong>如果这篇文章为你打开了高性能计算的大门，别忘了点个【赞】和【在看】，并转发给那个天天喊着“CPU 跑满了”的同事！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/21/integrating-cuda-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2025年最佳机器人Linux操作系统——顶级发行版与最新进展！</title>
		<link>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/</link>
		<comments>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/#comments</comments>
		<pubDate>Sun, 17 Aug 2025 14:49:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[DDS]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Fedora]]></category>
		<category><![CDATA[Gazebo]]></category>
		<category><![CDATA[Humble]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[LIDAR]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LTS]]></category>
		<category><![CDATA[MoveIt!]]></category>
		<category><![CDATA[NASA]]></category>
		<category><![CDATA[NVIDIA]]></category>
		<category><![CDATA[OpenCV]]></category>
		<category><![CDATA[OpenEmbedded]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[reddit]]></category>
		<category><![CDATA[ROS]]></category>
		<category><![CDATA[ROS2]]></category>
		<category><![CDATA[Stackoverflow]]></category>
		<category><![CDATA[TensorFlow]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UbuntuCore]]></category>
		<category><![CDATA[Yocto]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[仿真]]></category>
		<category><![CDATA[具身智能]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[机器人]]></category>
		<category><![CDATA[机器人操作系统]]></category>
		<category><![CDATA[树莓派]]></category>
		<category><![CDATA[波士顿动力]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5048</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025 大家好，我是Tony Bai。 如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。 在本文中，我们将探讨2025年最佳的机器人Linux操作系统，帮助你为你的项目找到完美的发行版——无论你是从事工业自动化、人工智能驱动的机器人技术，还是业余爱好者的创作。我们还将介绍专注于机器人的Linux发行版的最新发展，让你保持领先。 1. Ubuntu机器人操作系统 机器人技术正以前所未有的速度发展，改变着医疗、自动化、制造乃至太空探索等行业。任何机器人系统的基础都是其操作系统，它决定了系统的效率、安全性和性能。 Ubuntu机器人操作系统 截至2025年，Ubuntu已成为机器人领域的最佳Linux操作系统。凭借其与机器人操作系统（ROS）的无缝集成、优化的实时性能以及对AI驱动机器人技术的扩展支持，Ubuntu成为开发者、研究人员和行业的首选。 为什么Ubuntu是机器人领域的最佳Linux操作系统 Ubuntu在机器人领域的主导地位并非偶然——它建立在多年的持续发展和强大的社区支持之上。以下是Ubuntu脱颖而出的一些关键原因： 1. 与ROS（机器人操作系统）的无缝集成 ROS已成为使用最广泛的机器人中间件，提供了一系列工具和库，帮助开发者构建复杂的机器人应用程序。由于ROS最初就是为Ubuntu设计的，因此集成非常无缝。 ROS 2与Ubuntu：到2025年，Ubuntu为ROS 2提供了内置支持，ROS 2提供了实时功能、安全增强和对多机器人系统更好的支持。 预装ROS软件包：Ubuntu通过预配置的软件包简化了ROS的安装，为开发者节省了大量时间。 强大的开发者社区：由于Ubuntu是机器人领域使用最多的操作系统，因此有庞大的支持网络可用于故障排除、教程和协作。 2. 针对嵌入式和边缘设备进行优化 并非所有机器人系统都是大型工业机器——许多现代机器人是需要轻量级和高效软件的小型嵌入式设备。Ubuntu Core是Ubuntu的最小化版本，专为边缘计算和嵌入式机器人技术而优化。 基于事务的更新：Ubuntu Core提供自动、故障安全的更新，确保机器人系统保持最新状态，而不会有破坏功能的风险。 注重安全的设计：Ubuntu Core包含内置的安全功能，如应用程序沙箱和验证启动机制，这对于在敏感环境中运行的机器人至关重要。 低系统资源占用：凭借其轻量级的特性，Ubuntu Core能在小型机器人硬件上高效运行，包括树莓派（Raspberry Pi）、NVIDIA Jetson和定制AI板卡。 3. 安全性与长期维护 安全性是机器人技术中的一个主要问题，尤其是在医疗和国防等行业。Ubuntu背后的公司Canonical提供扩展安全维护（ESM），确保基于Ubuntu的机器人系统获得长期的安全更新。 定期安全补丁：这可以防止可能被黑客利用的漏洞，使Ubuntu成为机器人项目最安全的选择之一。 行业采用：许多航空航天、汽车和工业自动化公司因其安全优先的方法而信任Ubuntu。 4. 硬件兼容性与行业采用 Ubuntu支持广泛的硬件，从AI驱动的机械臂到自动驾驶无人机。无论你是在开发工业机器人还是个人助理机器人，Ubuntu都为大量的传感器、执行器和计算单元提供驱动程序、库和支持。 可与流行的硬件平台配合使用，例如： NVIDIA Jetson AI驱动的机器人套件 树莓派（用于小型机器人项目） Intel RealSense（用于3D深度感应机器人） 定制的基于ARM的机器人系统 因为Ubuntu是一个开源操作系统，制造商也可以为其特定的机器人应用定制内核并进行优化。 Ubuntu机器人技术的最新发展（2025年） 过去一年，Ubuntu的机器人技术生态系统取得了显著进步。以下是2025年一些最激动人心的更新： 1. [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025">本文永久链接</a> &#8211; https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025</p>
<p>大家好，我是Tony Bai。</p>
<p>如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。</p>
<p>在本文中，我们将探讨2025年最佳的机器人Linux操作系统，帮助你为你的项目找到完美的发行版——无论你是从事工业自动化、人工智能驱动的机器人技术，还是业余爱好者的创作。我们还将介绍专注于机器人的Linux发行版的最新发展，让你保持领先。</p>
<h2>1. Ubuntu机器人操作系统</h2>
<p>机器人技术正以前所未有的速度发展，改变着医疗、自动化、制造乃至太空探索等行业。任何机器人系统的基础都是其操作系统，它决定了系统的效率、安全性和性能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-2.png" alt="" /><br />
<center>Ubuntu机器人操作系统</center></p>
<p>截至2025年，Ubuntu已成为机器人领域的最佳Linux操作系统。凭借其与机器人操作系统（ROS）的无缝集成、优化的实时性能以及对AI驱动机器人技术的扩展支持，Ubuntu成为开发者、研究人员和行业的首选。</p>
<h3>为什么Ubuntu是机器人领域的最佳Linux操作系统</h3>
<p>Ubuntu在机器人领域的主导地位并非偶然——它建立在多年的持续发展和强大的社区支持之上。以下是Ubuntu脱颖而出的一些关键原因：</p>
<p><strong>1. 与ROS（机器人操作系统）的无缝集成</strong></p>
<p>ROS已成为使用最广泛的机器人中间件，提供了一系列工具和库，帮助开发者构建复杂的机器人应用程序。由于ROS最初就是为Ubuntu设计的，因此集成非常无缝。</p>
<ul>
<li>ROS 2与Ubuntu：到2025年，Ubuntu为ROS 2提供了内置支持，ROS 2提供了实时功能、安全增强和对多机器人系统更好的支持。</li>
<li>预装ROS软件包：Ubuntu通过预配置的软件包简化了ROS的安装，为开发者节省了大量时间。</li>
<li>强大的开发者社区：由于Ubuntu是机器人领域使用最多的操作系统，因此有庞大的支持网络可用于故障排除、教程和协作。</li>
</ul>
<p><strong>2. 针对嵌入式和边缘设备进行优化</strong></p>
<p>并非所有机器人系统都是大型工业机器——许多现代机器人是需要轻量级和高效软件的小型嵌入式设备。Ubuntu Core是Ubuntu的最小化版本，专为边缘计算和嵌入式机器人技术而优化。</p>
<ul>
<li>基于事务的更新：Ubuntu Core提供自动、故障安全的更新，确保机器人系统保持最新状态，而不会有破坏功能的风险。</li>
<li>注重安全的设计：Ubuntu Core包含内置的安全功能，如应用程序沙箱和验证启动机制，这对于在敏感环境中运行的机器人至关重要。</li>
<li>低系统资源占用：凭借其轻量级的特性，Ubuntu Core能在小型机器人硬件上高效运行，包括树莓派（Raspberry Pi）、NVIDIA Jetson和定制AI板卡。</li>
</ul>
<p><strong>3. 安全性与长期维护</strong></p>
<p>安全性是机器人技术中的一个主要问题，尤其是在医疗和国防等行业。Ubuntu背后的公司Canonical提供扩展安全维护（ESM），确保基于Ubuntu的机器人系统获得长期的安全更新。</p>
<ul>
<li>定期安全补丁：这可以防止可能被黑客利用的漏洞，使Ubuntu成为机器人项目最安全的选择之一。</li>
<li>行业采用：许多航空航天、汽车和工业自动化公司因其安全优先的方法而信任Ubuntu。</li>
</ul>
<p><strong>4. 硬件兼容性与行业采用</strong></p>
<p>Ubuntu支持广泛的硬件，从AI驱动的机械臂到自动驾驶无人机。无论你是在开发工业机器人还是个人助理机器人，Ubuntu都为大量的传感器、执行器和计算单元提供驱动程序、库和支持。</p>
<p>可与流行的硬件平台配合使用，例如：</p>
<ul>
<li>NVIDIA Jetson AI驱动的机器人套件</li>
<li>树莓派（用于小型机器人项目）</li>
<li>Intel RealSense（用于3D深度感应机器人）</li>
<li>定制的基于ARM的机器人系统</li>
</ul>
<p>因为Ubuntu是一个开源操作系统，制造商也可以为其特定的机器人应用定制内核并进行优化。</p>
<h3>Ubuntu机器人技术的最新发展（2025年）</h3>
<p>过去一年，Ubuntu的机器人技术生态系统取得了显著进步。以下是2025年一些最激动人心的更新：</p>
<p><strong>1. 针对机器人技术的实时内核增强</strong></p>
<p>实时性能在机器人技术中至关重要，微秒之差可能决定机器人是平稳运行还是彻底失败。2025年，Ubuntu引入了改进的实时内核支持，确保机器人应用满足低延迟处理要求。</p>
<ul>
<li>更快的响应时间：改进后的内核确保机器人的运动和决策能够无延迟地发生。</li>
<li>为多任务机器人提供更好的调度：对于同时执行多项操作的工业机器人非常有用。</li>
<li>增强的稳定性：减少机器人功能中的意外崩溃和延迟。</li>
</ul>
<p><strong>2. AI与机器学习集成</strong></p>
<p>现代机器人依赖于AI驱动的决策，Ubuntu已采取重要措施来优化机器人在机器学习方面的能力。</p>
<ul>
<li>内置的AI库，如TensorFlow、PyTorch和OpenCV，都为Ubuntu进行了预配置。</li>
<li>ROS 2现在包含了基于AI的运动规划和计算机视觉改进。</li>
<li>边缘AI支持：机器人可以在本地处理AI任务，而不是依赖云计算，从而减少延迟并改善实时决策。</li>
</ul>
<p><strong>3. 扩展对机器人硬件的支持</strong></p>
<p>Ubuntu已扩大其硬件支持范围，包括更多的工业机械臂、自动驾驶车辆和人形机器人。开发者现在可以将Ubuntu用于更广泛的机器人组件，包括：</p>
<ul>
<li>用于自动驾驶机器人的LIDAR传感器</li>
<li>用于云连接机器人的5G连接支持</li>
<li>用于基于感知的机器人的高级摄像头和深度感应模块</li>
</ul>
<p>通过这种扩展的兼容性，Ubuntu可以加快机器人应用程序的原型设计和部署。</p>
<p><strong>机器人社区对Ubuntu的评价</strong></p>
<p>机器人社区因其可靠性、灵活性和强大的开发者生态系统而广泛接受<a href="https://ubuntu.com/robotics">Ubuntu</a>。</p>
<ul>
<li>许多机器人专家认为精通Linux是必备技能，因为大多数机器人工具都是为Ubuntu构建的。</li>
<li>在Reddit和Stack Overflow等论坛的讨论中，经常强调Ubuntu相比其他操作系统选项提供了更好的支持、库和长期稳定性。</li>
<li>NASA、特斯拉和波士顿动力等公司都使用Ubuntu进行机器人研究和开发。</li>
</ul>
<p><strong>Ubuntu是机器人技术的未来</strong></p>
<p>凭借以下优势，Ubuntu已在2025年牢固确立了其作为最佳机器人Linux操作系统的地位：</p>
<ul>
<li>无缝的ROS 2集成</li>
<li>支持实时计算</li>
<li>AI和机器学习优化</li>
<li>增强的安全性和长期维护</li>
<li>广泛的行业采用</li>
</ul>
<p>无论你是在构建自动驾驶无人机、工业机器人，还是以研究为中心的AI驱动机器人系统，Ubuntu都为成功提供了最佳基础。</p>
<p>如果你计划进入机器人领域，学习Ubuntu、ROS和AI驱动的机器人开发是你能做出的最明智的决定。</p>
<h2>2. Debian机器人操作系统</h2>
<p>在快速发展的机器人世界中，选择正确的操作系统可以决定一个项目的成败。机器人工程师、研究人员和爱好者需要一个不仅稳定可靠，而且配备最新工具和库以支持开发的操作系统。在2025年，Debian机器人操作系统已成为机器人领域最佳的基于Linux的操作系统，提供了无与伦比的稳定性、灵活性和尖端软件支持的组合。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-3.png" alt="" /><br />
<center>Debian机器人操作系统</center></p>
<h3>为什么选择Debian用于机器人技术？</h3>
<p>Debian长期以来以其对自由和开源软件的承诺而闻名，这使其成为机器人开发者的一个有吸引力的选择。与专有系统不同，Debian确保了对庞大工具库的无限制访问，允许开发者在没有许可限制的情况下进行实验、创新和协作。</p>
<p>以下是Debian在2025年成为机器人领域首选Linux发行版的原因：</p>
<ul>
<li>稳定性和可靠性：Debian以其严格的测试过程而闻名。每个稳定版本都经过广泛审查，确保机器人应用程序平稳、一致地运行。</li>
<li>全面的软件包仓库：Debian维护着最大的软件仓库之一，其中包括数千个专门为机器人应用设计的软件包。</li>
<li>社区支持：一个强大而活跃的Debian社区为持续的改进、错误修复和功能增强做出贡献，使机器人开发者更容易解决问题和改进他们的项目。</li>
<li>安全性和性能：Debian增强的安全功能确保机器人系统免受潜在威胁，这在工业自动化和自主系统等关键应用中尤为重要。</li>
</ul>
<p><strong>与ROS的无缝集成</strong></p>
<p>机器人操作系统（ROS）是现代机器人开发的支柱。它提供了必要的工具、库和驱动程序，帮助开发者高效地创建复杂的机器人应用程序。Debian与ROS的深度集成确保了无缝的开发体验，允许用户在没有兼容性问题的情况下利用ROS的功能。</p>
<p>Debian的包管理系统使安装ROS变得简单直接。Debian科学团队积极维护一个专门用于机器人相关软件包的仓库，确保用户始终能访问到最新版本的基本工具。</p>
<p>对于那些从事高级机器人系统开发的开发者来说，Debian对ROS 2（ROS的下一代版本）的支持确保了与更新框架的兼容性、增强的实时性能和改进的安全功能。</p>
<h3>Debian机器人技术的最新发展</h3>
<p>Debian机器人技术在2025年持续发展，取得了显著进步。以下是一些最新的更新：</p>
<p><strong>1. 扩展的机器人软件包仓库</strong></p>
<p>Debian科学团队一直在积极扩展机器人软件包仓库。此次更新包括了流行工具的新的和改进的版本，例如：</p>
<ul>
<li>Gazebo – 一款强大的仿真工具，用于在虚拟环境中测试机器人应用。</li>
<li>MoveIt! – 一个广泛用于机械臂和操纵器的运动规划框架。</li>
<li>OpenCV – 这个计算机视觉库的最新版本现已针对机器人应用中的更佳性能进行了优化。</li>
<li>Navigation Stack – 升级的模块，用于改进自主机器人的路径规划和避障功能。</li>
</ul>
<p>通过这些更新，开发者无需安装第三方仓库即可访问最前沿的工具。</p>
<p><strong>2. 实时内核支持</strong></p>
<p>实时处理对于机器人技术至关重要，精确的计时和快速的响应率是必不可少的。Debian现在正式支持实时Linux内核（RT-PREEMPT），允许开发者以最小的延迟运行对时间敏感的机器人应用程序。</p>
<p>这项更新对于工业机器人、机器人手术和自主无人机尤其有益，因为在这些领域，即使是毫秒级的延迟也可能导致严重问题。</p>
<p><strong>3. 增强的安全功能</strong></p>
<p>随着机器人更多地融入工业和智能环境，安全风险也随之增加。作为回应，Debian为机器人系统引入了先进的安全功能，包括：</p>
<ul>
<li>强制访问控制（MAC） – 强制执行严格的安全策略，以防止对机器人系统的未授权访问。</li>
<li>安全启动支持 – 确保只有经过验证和信任的软件才能在机器人硬件上运行。</li>
<li>自动安全更新 – 实时保护机器人应用免受漏洞和新兴威胁的侵害。</li>
</ul>
<p>凭借这些增强功能，Debian机器人操作系统现在成为依赖机器人进行自动化、医疗和国防的行业的一个更安全的选择。</p>
<p><strong>社区与支持</strong></p>
<p>Debian最大的优势之一是其社区驱动的开发模式。与专有机器人软件不同，Debian受益于全球数千名开发者和研究人员对其改进的贡献。Debian科学邮件列表、论坛和Git仓库是宝贵的资源，用户可以在这些地方讨论问题、分享解决方案和协作项目。</p>
<p>Debian科学团队还确保Debian机器人操作系统与最新的技术进步保持同步，使初学者和专家都能更容易地开始机器人开发。</p>
<h3>为什么在2025年选择Debian机器人操作系统？</h3>
<p><a href="https://wiki.debian.org/DebianScience/Robotics/ROS">Debian机器人操作系统</a>不仅仅是一个操作系统；它是一个生态系统，使开发者、研究人员和企业能够充满信心地构建先进的机器人系统。从其无缝的ROS集成和实时内核支持，到其强大的安全功能和广泛的软件包仓库，Debian为2025年的机器人开发提供了一切所需。</p>
<p>无论你是从事自主机器人、工业自动化还是AI驱动的机器人应用，Debian机器人操作系统都提供了一个稳定、安全和强大的基础，以构建机器人技术的未来。</p>
<p>你在项目中使用Debian机器人操作系统吗？在下面的评论中分享你的想法和经验吧！</p>
<h2>3. 基于ROS的发行版 (ROS 2)</h2>
<p>机器人操作系统（ROS）一直是机器人行业的变革者，为开发机器人应用程序提供了一个强大而灵活的框架。多年来，ROS 2已发展成为致力于尖端机器人解决方案的开发者、研究人员和公司的首选。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-4.png" alt="" /><br />
<center>基于ROS的发行版 (ROS 2)</center></p>
<p>当我们进入2025年，ROS 2发行版已经成熟，提供了改进的实时能力、增强的安全性和更广泛的兼容性。如果你正在寻找最佳的机器人Linux操作系统，本指南将带你了解最新的ROS 2发行版、它们的特性以及运行它们的理想Linux发行版。</p>
<h3>理解ROS 2发行版</h3>
<p>ROS 2发行版是ROS 2框架的定期发布版本，包含了最新的改进、安全补丁和功能升级。</p>
<p>每个发行版都有一个定义的生命周期，通常每两年提供一次长期支持（LTS），而非LTS版本则作为实验性功能的测试平台。选择正确的ROS 2发行版取决于项目稳定性要求、硬件兼容性和功能需求等因素。</p>
<p><strong>2025年的关键ROS 2发行版</strong></p>
<p><strong>1. Jazzy Jalisco (LTS) – 2024年5月23日发布</strong></p>
<p>最新的LTS版本<a href="https://docs.ros.org/en/rolling/Releases/Release-Jazzy-Jalisco.html">Jazzy Jalisco</a>，将获得五年的支持，使其成为工业应用和长期项目的最佳选择。它引入了：</p>
<ul>
<li>针对时间敏感机器人操作的先进实时能力</li>
<li>通过加密通信和认证功能增强的安全性</li>
<li>扩展了对不同硬件平台的兼容性</li>
<li>更好的中间件支持，以提高性能和可伸缩性</li>
</ul>
<p><strong>2. Iron Irwini (非LTS) – 2023年5月23日发布</strong></p>
<p>虽然<a href="https://docs.ros.org/en/iron/Releases/Release-Iron-Irwini.html">Iron Irwini</a>不是LTS版本（支持期1.5年），但它充当了新创新的试验场。希望尝试尖端机器人功能的开发者可以从中受益：</p>
<ul>
<li>更快的开发周期和频繁的更新</li>
<li>实验性的中间件改进</li>
<li>提前接触可能包含在未来LTS版本中的功能</li>
</ul>
<p><strong>3. Humble Hawksbill (LTS) – 2022年5月23日发布</strong></p>
<p><a href="https://docs.ros.org/en/foxy/Releases/Release-Humble-Hawksbill.html">Humble Hawksbill</a>在2025年仍然是一个受欢迎的选择，因为它将获得支持直到2027年。它在以下方面发挥了关键作用：</p>
<ul>
<li>改进中间件通信协议</li>
<li>改进工具和调试能力</li>
<li>在基于ARM的平台上有更好的性能</li>
</ul>
<p>对于在Humble上启动的项目，迁移到Jazzy Jalisco可以确保长期稳定性。</p>
<h3>ROS 2的最新发展（2025年）</h3>
<p>ROS 2持续发展，为机器人技术生态系统带来了几项关键改进：</p>
<p><strong>1. 实时支持</strong></p>
<p>凭借改进的实时调度，ROS 2现在可以处理更复杂的机器人任务，并具有确定性的性能。</p>
<p><strong>2. 安全性增强</strong></p>
<p>通过安全的通信协议和更好的认证机制，ROS 2现在比以往任何时候都更安全，解决了工业机器人和自动驾驶汽车中的安全问题。</p>
<p><strong>3. 跨平台兼容性</strong></p>
<p>虽然Ubuntu仍然是主要的操作系统，但ROS 2已将其支持扩展到Debian、Fedora、Windows甚至macOS。</p>
<p><strong>4. 更好的中间件性能</strong></p>
<p>DDS（数据分发服务）中的中间件改进增强了大型机器人系统中的延迟、可靠性和可伸缩性。</p>
<p>随着机器人技术的不断进步，ROS 2仍然是行业领先的框架，其中Jazzy Jalisco（LTS）是2025年的首选。</p>
<p>对于ROS 2的最佳Linux操作系统，Ubuntu 22.04 LTS作为最稳定和得到最广泛支持的选项脱颖而出。然而，开发者也可以灵活选择Debian、Fedora和Arch Linux。</p>
<p>随着在实时性能、安全性和跨平台支持方面的持续改进，ROS 2正在塑造2025年及以后机器人技术的未来。保持对最新发展的了解，可以确保你的机器人项目保持未来竞争力。</p>
<h2>4. Fedora机器人操作系统</h2>
<p>在不断发展的机器人世界中，选择正确的操作系统对于无缝开发和部署至关重要。截至2025年，Fedora机器人操作系统凭借其专门的工具、强大的社区支持和对开源原则的承诺，已成为机器人领域最强大的Linux发行版之一。无论你是尝试自主机器人的业余爱好者，还是开发工业自动化解决方案的专业人士，Fedora机器人操作系统都提供了一个量身定制的综合平台，以满足你的需求。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-5.png" alt="" /><br />
<center>Fedora机器人操作系统</center></p>
<h3>为什么Fedora机器人操作系统脱颖而出</h3>
<p>Fedora机器人操作系统是Fedora项目的一个专门分支，专为机器人专家设计。它提供了一套精心策划的软件包，涵盖了机器人技术的各个方面，从仿真到硬件接口。以下是Fedora机器人操作系统在2025年广受欢迎的关键原因：</p>
<p><strong>1. 全面的软件套件</strong></p>
<p>Fedora机器人操作系统包含一套广泛的预装软件包，使开发者可以轻松上手，而无需花费数小时设置环境。Fedora机器人操作系统中的一些核心工具包括：</p>
<ul>
<li>Gazebo – 一款强大的3D机器人模拟器，使开发者能够在虚拟环境中测试机器人应用程序。</li>
<li>OpenCV – 广泛用于图像处理和机器学习任务的计算机视觉库。</li>
<li>Arduino IDE – 用于编程微控制器的流行开发环境。</li>
<li>Player/Stage – 在学术界和研究中广泛使用的仿真工具。</li>
<li>Gazebo, V-REP, and Webots – 先进的机器人仿真软件，用于训练AI模型和在虚拟环境中测试算法。</li>
</ul>
<p><strong>2. 与ROS（机器人操作系统）的无缝集成</strong></p>
<p>Fedora机器人操作系统的最大优势之一是其与ROS的无缝集成，ROS是使用最广泛的机器人软件框架。ROS提供了一些基本服务，例如：</p>
<ul>
<li>硬件抽象 – 使控制传感器、电机和执行器变得更加容易。</li>
<li>底层设备控制 – 提供对机器人硬件组件的直接访问。</li>
<li>进程间通信 – 促进不同机器人模块和进程之间的无缝通信。</li>
</ul>
<p>Fedora机器人操作系统预配置了最新版本的ROS 2，确保与尖端的机器人应用兼容。这种集成使开发者能够利用广泛的ROS生态系统，包括库、驱动程序和可视化工具。</p>
<p><strong>3. 强大的社区支持</strong></p>
<p>Fedora机器人操作系统得益于一个由开发者、研究人员和机器人爱好者组成的活跃社区。Fedora机器人特别兴趣小组（SIG）致力于确保Fedora用户能够获得最新的机器人软件和更新。该小组积极维护Fedora的机器人软件包，提供教程，并帮助用户解决问题。</p>
<h3>Fedora机器人技术的最新发展</h3>
<p>Fedora机器人团队一直积极地将该领域的最新进展融入其中。2025年一些最显著的更新包括：</p>
<p><strong>1. 增强的仿真工具</strong></p>
<p>仿真在机器人开发中起着至关重要的作用，它允许开发者在物理机器人上部署算法之前进行测试。Fedora机器人操作系统通过集成以下内容显著改善了其仿真能力：</p>
<ul>
<li>Ignition Gazebo – 一款提供高保真物理和传感器仿真的高级模拟器。</li>
<li>AI驱动的仿真环境 – 支持基于机器学习的仿真，机器人可以在其中学习并适应环境。</li>
</ul>
<p><strong>2. 改进的硬件支持</strong></p>
<p>随着机器人硬件的迅速扩展，Fedora机器人操作系统已包括对以下内容的支持：</p>
<ul>
<li>新的机器人传感器和执行器 – 确保软件和硬件组件之间的无缝通信。</li>
<li>树莓派和Jetson Nano优化 – Fedora机器人操作系统现在在低功耗硬件上运行更高效，非常适合DIY机器人项目。</li>
<li>扩展的驱动程序支持 – Fedora机器人操作系统现在包括用于机械臂、激光雷达传感器和人形机器人的额外驱动程序。</li>
</ul>
<p><strong>3. 教育资源和教程</strong></p>
<p>了解到机器人技术对初学者可能具有挑战性，Fedora机器人操作系统在教育资源上投入了大量资金。这些资源包括：</p>
<ul>
<li>分步教程 – 涵盖从设置开发环境到编程机器人运动的所有内容。</li>
<li>交互式学习模块 – 用户可以在虚拟训练环境中练习为不同的机器人任务编写代码。</li>
<li>在线社区论坛和黑客马拉松 – 为开发者提供协作、学习和分享见解的空间。</li>
</ul>
<p><strong>为什么开发者更喜欢Fedora机器人操作系统而非其他Linux发行版</strong></p>
<p>机器人社区经常争论用于开发的最佳操作系统。虽然像Ubuntu和Debian这样的其他Linux发行版被广泛使用，但Fedora机器人操作系统具有明显的优势：</p>
<ul>
<li>最新的内核和软件包 – Fedora以跟上最新技术而闻名，确保开发者能够访问尖端功能。</li>
<li>为性能和安全优化 – Fedora的安全特性使其成为工业和研究应用的首选。</li>
<li>使用DNF实现无缝包管理 – Fedora的包管理系统效率高，减少了在其他发行版中经常遇到的依赖问题。</li>
</ul>
<p>此外，基于Linux的操作系统通常比Windows更受机器人开发者的青睐，因为它们提供：</p>
<ul>
<li>更好地控制操作系统功能 – 直接访问系统资源。</li>
<li>更简便的依赖管理 – 简化了机器人库的安装。</li>
<li>开源的灵活性 – 可根据项目需求进行完全定制。</li>
</ul>
<p><a href="https://fedoraproject.org/wiki/Robotics">Fedora机器人操作系统</a>无疑是2025年最佳的机器人Linux发行版之一。凭借其广泛的软件套件、强大的ROS集成、改进的硬件支持和活跃的社区，它为机器人专家开发、测试和部署他们的项目提供了一个理想的环境。</p>
<p>随着机器人技术的不断发展，Fedora机器人操作系统仍然致力于走在创新的前沿，使其成为有抱负的和专业的机器人专家的首选。如果你正在寻找一个强大、可靠且面向未来的机器人Linux操作系统，Fedora机器人操作系统是完美的选择。</p>
<h2>5. 用于机器人的OpenEmbedded Linux (Yocto)</h2>
<p>机器人领域正以前所未有的速度发展，人工智能、自动化和边缘计算的进步推动了对强大且可定制的操作系统的需求。在2025年，由Yocto项目驱动的OpenEmbedded Linux，作为机器人领域最佳的基于Linux的操作系统之一脱颖而出。它提供灵活性、可扩展性和优化性能的能力，使其成为从事机器人应用的开发者的首选。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-6.png" alt="" /><br />
<center>用于机器人的OpenEmbedded Linux (Yocto)</center></p>
<p>如果你参与机器人开发——无论是在工业自动化、自动驾驶汽车、无人机还是AI驱动的机器人系统中——了解OpenEmbedded Linux和Yocto的能力将至关重要。在这篇博文中，我们将深入探讨OpenEmbedded Linux（Yocto）如何成为机器人的理想操作系统，探索其最新发展，并讨论其对机器人行业的影响。</p>
<h3>什么是OpenEmbedded Linux和Yocto项目？</h3>
<p><strong>OpenEmbedded Linux</strong></p>
<p>OpenEmbedded是一个开源的构建框架和交叉编译环境，专为创建针对嵌入式设备的Linux发行版而设计。与Ubuntu或Fedora等通用Linux发行版不同，OpenEmbedded允许开发者专门为他们的硬件和应用需求定制和优化他们的Linux构建。</p>
<p><strong>Yocto项目</strong></p>
<p>Yocto项目由Linux基金会于2010年发起，是一个与OpenEmbedded协同工作的合作项目，旨在简化和标准化为嵌入式和物联网设备定制Linux发行版的开发。以BitBake为核心构建系统，Yocto项目为开发者提供工具、模板和最佳实践，以创建最小化、高效且针对硬件优化的基于Linux的操作系统。</p>
<p>对于机器人开发者来说，OpenEmbedded和Yocto项目的结合使他们能够创建为机器人应用量身定制的轻量、快速且针对特定硬件的Linux发行版。</p>
<h3>为什么OpenEmbedded Linux (Yocto)是2025年机器人的理想选择</h3>
<p><strong>高度定制化与模块化</strong></p>
<ul>
<li>与传统的Linux发行版（预装了软件和功能）不同，OpenEmbedded Linux让开发者可以构建一个只包含其机器人系统所需内容的发行版。</li>
<li>这种模块化的方法确保了一个优化且轻量级的操作系统，从而提升性能。</li>
</ul>
<p><strong>硬件抽象与兼容性</strong></p>
<ul>
<li>机器人项目通常涉及各种各样的硬件组件，从传感器和执行器到专用处理器和AI加速器。</li>
<li>OpenEmbedded的基于层的结构使开发者能够创建板级支持包（BSP），从而可以轻松地与不同的硬件架构集成。</li>
</ul>
<p><strong>长期支持与安全性</strong></p>
<ul>
<li>Yocto项目定期发布带有安全补丁的LTS（长期支持）版本，使其成为机器人应用的一个安全稳定的选择。</li>
<li>安全性是机器人技术中的一个主要问题，尤其是在自主系统和工业自动化中，而OpenEmbedded Linux提供了安全启动、内核加固和访问控制策略等功能。</li>
</ul>
<p><strong>更好的资源效率</strong></p>
<ul>
<li>机器人应用通常在低功耗和资源受限的硬件上运行。</li>
<li>OpenEmbedded Linux允许开发者创建极简的Linux构建，减少系统开销并最大化效率。</li>
</ul>
<p><strong>强大的社区与行业采用</strong></p>
<ul>
<li>Yocto项目得到了嵌入式Linux社区和英特尔、高通、恩智浦和德州仪器等主要行业参与者的强力支持。</li>
<li>这意味着为机器人开发者提供了持续的改进、广泛的文档和长期的可靠性。</li>
</ul>
<h3>OpenEmbedded Linux在机器人技术领域的最新发展（2025年）</h3>
<p><strong>1. 上游Linux对机器人硬件的支持</strong></p>
<p>在2025年，像Linaro和高通这样的公司通过将对高通机器人RB5等机器人平台的支持上游化，为OpenEmbedded Linux做出了重大贡献。这一发展确保了下一代机器人系统更好的兼容性、实时处理和AI集成。</p>
<p><strong>2. 改进的培训与学习资源</strong></p>
<p>随着基于Yocto的Linux系统的日益普及，Bootlin和Yocto项目社区等组织推出了新的培训项目、研讨会和在线课程。这些资源使开发者更容易为机器人项目学习、实施和优化OpenEmbedded Linux。</p>
<p><strong>3. 扩展的AI与机器学习能力</strong></p>
<p>OpenEmbedded Linux在集成AI和机器学习框架（如TensorFlow Lite和ROS 2（机器人操作系统））方面取得了重大改进。这使得机器人系统能够执行边缘AI推理、实时决策和高级自动化。</p>
<p><strong>4. 全行业采用与标准化</strong></p>
<p>许多机器人公司和研究机构已转向使用基于Yocto的Linux发行版作为其嵌入式机器人平台。这一转变正在帮助创建一个更加标准化的软件生态系统，减少碎片化并改善机器人设备间的兼容性。</p>
<h3>OpenEmbedded Linux在机器人技术中的应用</h3>
<p><strong>工业自动化</strong></p>
<p>OpenEmbedded Linux正在为需要高性能计算、实时处理和强大安全功能的新一代自动化制造机器人提供动力。</p>
<p><strong>自动驾驶汽车与无人机</strong></p>
<p>机器人公司正在使用基于Yocto的Linux来开发自主无人机和自动驾驶汽车，确保低延迟通信和AI驱动的导航。</p>
<p><strong>医疗机器人</strong></p>
<p>医疗机器人，如手术机器人和康复设备，受益于OpenEmbedded Linux提供安全、实时和稳定操作系统环境的能力。</p>
<p><strong>AI驱动的家庭与服务机器人</strong></p>
<p>智能助手、配送机器人和其他AI驱动的机器人解决方案利用OpenEmbedded Linux进行定制化的AI模型和实时的语音/图像处理。</p>
<h3>OpenEmbedded Linux是2025年最佳的机器人操作系统吗？</h3>
<p>随着机器人行业的不断扩大，对可定制、轻量级和高性能操作系统的需求比以往任何时候都更加关键。由Yocto项目驱动的OpenEmbedded Linux无疑是2025年机器人领域最佳的Linux操作系统。</p>
<p>其提供针对特定硬件的优化、实时处理、安全性和AI集成的能力，使其成为全球机器人专家、工程师和开发者的首选。随着持续的进步和行业采用，OpenEmbedded Linux必将在未来几年塑造机器人技术的未来。</p>
<p>如果你正在开发一个机器人项目，并且需要一个可扩展且高效的Linux操作系统，那么OpenEmbedded Linux (Yocto)是2025年的最佳选择。</p>
<p><strong>下一步是什么？</strong></p>
<p>探索OpenEmbedded Linux：<a href="https://www.openembedded.org/wiki/Main_Page">https://www.openembedded.org/wiki/Main&#95;Page</a></p>
<p>了解更多关于Yocto项目的信息：<a href="https://techrefreshing.com/best-linux-os-for-robotics-in-2025/www.yoctoproject.org">www.yoctoproject.org</a></p>
<p>开始开发：<a href="https://docs.yoctoproject.org/">Yocto文档</a></p>
<p>你在机器人项目中使用OpenEmbedded Linux吗？在下面的评论中分享你的想法和经验吧！</p>
<h2>结论</h2>
<p>在2025年选择最佳的机器人Linux操作系统取决于你的具体需求。如果你需要一个支持良好、对初学者友好的选项，Ubuntu机器人操作系统是你的不二之选。对于稳定性和长期项目，Debian机器人操作系统是一个绝佳的选择。那些从事AI驱动或实验性机器人技术的人应该考虑Fedora机器人操作系统，而嵌入式系统开发者可以依赖基于Yocto的Linux发行版。随着ROS 2、AI和实时内核优化的不断进步，Linux仍然是塑造机器人技术未来的首选操作系统。</p>
<h2><strong>免责声明</strong></h2>
<p>本文中的信息基于截至2025年的最新可用更新。2025年最佳的机器人Linux操作系统可能因特定的硬件、软件更新和项目要求而异。在选择操作系统之前，请务必验证其与你的机器人框架的兼容性。本文仅供参考，不构成专业建议。</p>
<p>本文翻译自文章《<a href="https://techrefreshing.com/best-linux-os-for-robotics-in-2025/">Best Linux OS for Robotics in 2025</a>》- https://techrefreshing.com/best-linux-os-for-robotics-in-2025/</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？</title>
		<link>https://tonybai.com/2025/08/16/brand-new-os-impossible/</link>
		<comments>https://tonybai.com/2025/08/16/brand-new-os-impossible/#comments</comments>
		<pubDate>Sat, 16 Aug 2025 00:03:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BSD]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[JS]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[node.js]]></category>
		<category><![CDATA[open]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[POSIX]]></category>
		<category><![CDATA[Printf]]></category>
		<category><![CDATA[proc]]></category>
		<category><![CDATA[Read]]></category>
		<category><![CDATA[Redox]]></category>
		<category><![CDATA[relibc]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[write]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[补丁]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5040</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/16/brand-new-os-impossible 大家好，我是Tony Bai。 对于许多心怀浪漫主义的开发者来说，“从零开始编写一个属于自己的操作系统”，或许是技术生涯中最终极、最性感的梦想。这几乎是现代编程世界的“创世纪”，是掌控计算机每一个比特的至高权力。 然而，最近一位名为 Wildan M 的工程师，在他的一篇博文中，用一次亲身参与 Redox OS 项目的经历，给我们所有人泼了一盆冷水。他的结论简单而又颠覆： 现在，从零开始编写一个全新的、能被广泛采用的操作系统，已几乎是一项不可能完成的任务。 而其真正的难点，并非我们想象中那个神秘而复杂的内核，而在于内核之外，那座看不见的、庞大到令人绝望的“冰山”。 冰山一角：内核，那个“最简单”的部分 故事的主角是 Redox OS，一个雄心勃勃的项目。它旨在用内存安全的 Rust 语言，构建一个现代的、基于微内核架构的、可以替代 Linux 和 BSD 的完整操作系统。 当我们谈论“写一个 OS”时，我们通常指的是编写内核。那么 Redox OS 的内核有多复杂呢？文章给出了惊人的数据： * 代码量： 约 3 万行 (30k LoC)。 * 启动速度： 大多数情况下，不到 1 秒。 在短短十年间，Redox 团队已经完成了动态链接、Unix 套接字等核心功能。这无疑是令人敬佩的工程壮举。但 Wildan 指出，这仅仅是浮出水面的冰山一角。一个能启动的内核，距离一个“能用”的操作系统，还有着遥远的距离。 冰山之下：生态移植的“五层地狱” 当作者兴致勃勃地想为 Redox OS 贡献力量，尝试将一些现代程序（如 Go, Node.js, Rust [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/brand-new-os-impossible-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/16/brand-new-os-impossible">本文永久链接</a> &#8211; https://tonybai.com/2025/08/16/brand-new-os-impossible</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多心怀浪漫主义的开发者来说，“从零开始编写一个属于自己的操作系统”，或许是技术生涯中最终极、最性感的梦想。这几乎是现代编程世界的“创世纪”，是掌控计算机每一个比特的至高权力。</p>
<p>然而，最近一位名为 Wildan M 的工程师，在<a href="https://blog.wellosoft.net/writing-a-brand-new-os-is-almost-impossible-by-now">他的一篇博文</a>中，用一次亲身参与 Redox OS 项目的经历，给我们所有人泼了一盆冷水。他的结论简单而又颠覆：</p>
<p><strong>现在，从零开始编写一个全新的、能被广泛采用的操作系统，已几乎是一项不可能完成的任务。</strong></p>
<p>而其真正的难点，并非我们想象中那个神秘而复杂的内核，而在于内核之外，那座看不见的、庞大到令人绝望的“冰山”。</p>
<h2>冰山一角：内核，那个“最简单”的部分</h2>
<p>故事的主角是 Redox OS，一个雄心勃勃的项目。它旨在用内存安全的 Rust 语言，构建一个现代的、基于微内核架构的、可以替代 Linux 和 BSD 的完整操作系统。</p>
<p>当我们谈论“写一个 OS”时，我们通常指的是编写内核。那么 Redox OS 的内核有多复杂呢？文章给出了惊人的数据：<br />
*   <strong>代码量：</strong> 约 3 万行 (30k LoC)。<br />
*   <strong>启动速度：</strong> 大多数情况下，不到 1 秒。</p>
<p>在短短十年间，Redox 团队已经完成了动态链接、Unix 套接字等核心功能。这无疑是令人敬佩的工程壮举。但 Wildan 指出，这仅仅是浮出水面的冰山一角。一个能启动的内核，距离一个“能用”的操作系统，还有着遥远的距离。</p>
<h2>冰山之下：生态移植的“五层地狱”</h2>
<p>当作者兴致勃勃地想为 Redox OS 贡献力量，尝试将一些现代程序（如 Go, Node.js, Rust 编译器）移植上去时，他才真正撞上了那座隐藏在水面之下的巨大冰山。</p>
<p>一个现代操作系统之所以“能用”，是因为它能运行我们日常使用的所有软件。而将这些软件“搬”到一个全新的操作系统上，需要闯过一重又一重难关。</p>
<p><strong>第一层：系统调用 (Syscall) 的鸿沟</strong></p>
<p>这是最底层的障碍。每个操作系统都有自己的一套与硬件和内核交互的“语言”，即系统调用。Redox OS 的 syscall 与我们熟知的 Linux 完全不同。这意味着，任何需要与内核打交道的程序（几乎是所有程序），都必须重写这部分逻辑，告诉它如何在新世界里“说话”。</p>
<p><strong>第二层：libc 的重担</strong></p>
<p>为了不让每个程序都去痛苦地学习 syscall 这门“方言”，操作系统通常会提供一个标准的“翻译官”——C 标准库 (libc)。它将复杂的 syscall 封装成开发者熟悉的函数（如 printf, open, read）。因此，一个新 OS 的核心任务之一，就是自己实现一个兼容的 libc。Redox 为此用 Rust 实现了一个名为 relibc 的项目，其工程量之浩大可想而知。</p>
<p><strong>第三层：POSIX 的“几乎兼容”陷阱</strong></p>
<p>即便新 OS 像 Redox 一样，努力兼容 POSIX 这个通用标准，噩梦也远未结束。因为无数现有的软件，早已深度依赖于 Linux 特有的、非 POSIX 的功能，比如解析 /proc 文件系统、操作 cgroups 等。结果就是，即使有了 relibc，你依然需要为这些软件挨个打上无数的“补丁”。文章提到，仅 Redox OS 的官方“软件食谱 (Cookbook)”中，就包含了<strong>约 70 个</strong>这样的补丁。</p>
<p><strong>第四层：编译器的“先有鸡还是先有蛋”</strong></p>
<p>你想在新 OS 上原生编译软件吗？那你首先需要一个能在这个 OS 上运行的编译器，比如 GCC、Rustc 或 Go 编译器。但问题是，移植编译器本身，就是所有软件移植任务中最复杂、最艰巨的一种。它需要处理极其底层的二进制格式、链接方式和系统调用。这形成了一个经典的“鸡生蛋还是蛋生鸡”的困局。</p>
<p><strong>第五层：语言生态的“次元壁”</strong></p>
<p>如果说移植 C 语言程序还只是“困难模式”，那么移植那些拥有自己庞大生态的现代语言程序（如 Rust, Go, Node.js），则是“地狱模式”。这些语言的包管理器（如 Cargo, Go Modules）会从中央仓库下载海量依赖，你很难像修改 C 代码一样，通过一个简单的 .patch 文件来修复所有问题。唯一的办法，往往是去 fork 无数个核心依赖库，然后逐一修改，这几乎是一项不可能完成的任务。</p>
<h2>小结：生态，才是那座无法逾越的山</h2>
<p>当 Wildan 经历过这一切后，他得出了文章开头的那个结论。</p>
<p>一个操作系统的成功，或许 <strong>20% 在于内核的精巧，而 80% 在于其上能否运行用户想要的所有软件。</strong> 后者，那个由编译器、标准库、第三方包、应用软件共同构成的庞大生态，才是真正的、几乎无法被复制的“护城河”。</p>
<p>这就像建造一座城市。你可以设计出最宏伟、最先进的市政厅（内核），但如果没有配套的道路、水电、学校、医院、商店（软件生态），这座城市就永远只是一座无法住人的“鬼城”。</p>
<p>这篇文章并非是要劝退所有对底层技术抱有热情的开发者。正如作者所说，如果你想<strong>学习</strong>，从零开始或加入 Redox 这样的项目，会是一段极其宝贵的经历。但如果你想构建一个<strong>被广泛采用</strong>的新 OS，你面对的将不仅仅是技术挑战，更是一个需要说服全球成千上万开发者为你“投票”的社会学难题。</p>
<p>这或许就是对那些仍在坚持构建新 OS 的探索者们，我们应该报以最高敬意的原因。因为他们挑战的，不仅仅是代码，更是一整个时代建立起来的软件文明。</p>
<p>资料链接：https://blog.wellosoft.net/writing-a-brand-new-os-is-almost-impossible-by-now</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/16/brand-new-os-impossible/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>为何Go语言迟迟未能拥抱 io_uring？揭秘集成的三大核心困境</title>
		<link>https://tonybai.com/2025/08/11/why-go-not-embrace-iouring/</link>
		<comments>https://tonybai.com/2025/08/11/why-go-not-embrace-iouring/#comments</comments>
		<pubDate>Mon, 11 Aug 2025 00:06:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CQ]]></category>
		<category><![CDATA[epoll]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[IO]]></category>
		<category><![CDATA[io_uring]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[P]]></category>
		<category><![CDATA[processor]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[scylladb]]></category>
		<category><![CDATA[SQ]]></category>
		<category><![CDATA[异步IO]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[运行时]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5025</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/11/why-go-not-embrace-iouring 大家好，我是Tony Bai。 在 Linux I/O 的世界里，io_uring 如同划破夜空的流星，被誉为“终极接口”。它承诺以无与伦比的效率，为数据密集型应用带来革命性的性能提升。正如高性能数据库 ScyllaDB 在其官方博文中所展示的，io_uring 能够将系统性能推向新的高峰。 然而，一个令人费解的问题摆在了所有 Go 开发者面前：作为云原生infra和并发编程的标杆，Go 语言为何对这颗唾手可得的“性能银弹”表现得如此审慎，甚至迟迟未能将其拥抱入标准库的怀抱？一场在 Go 官方仓库持续了五年之久的 Issue 讨论（#31908），为我们揭开了这层神秘的面纱。这并非简单的技术取舍，而是 Go 在其设计哲学、工程现实与安全红线之间进行反复权衡的结果。本文将深入这场讨论，为您揭秘阻碍 io_uring 在 Go 中落地的三大核心困境。 io_uring：一场 I/O 模型的革命 要理解这场争论，我们首先需要明白 io_uring 究竟是什么，以及它为何具有革命性。 在 io_uring 出现之前，Linux 上最高效的 I/O 模型是 epoll。epoll 采用的是一种“拉（pull）”模型：应用程序通过一次 epoll_wait 系统调用来询问内核：“有我关心的文件描述符准备好进行 I/O 了吗？”。内核响应后，应用程序需要再为每个就绪的描述符分别发起 read 或 write 系统调用。这意味着，处理 N 个 I/O 事件至少需要 N+1 次系统调用。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/why-go-not-embrace-iouring-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/11/why-go-not-embrace-iouring">本文永久链接</a> &#8211; https://tonybai.com/2025/08/11/why-go-not-embrace-iouring</p>
<p>大家好，我是Tony Bai。</p>
<p>在 Linux I/O 的世界里，io_uring 如同划破夜空的流星，被誉为“终极接口”。它承诺以无与伦比的效率，为数据密集型应用带来革命性的性能提升。正如高性能数据库 ScyllaDB 在其<a href="https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/">官方博文</a>中所展示的，io_uring 能够将系统性能推向新的高峰。</p>
<p>然而，一个令人费解的问题摆在了所有 Go 开发者面前：作为云原生infra和并发编程的标杆，Go 语言为何对这颗唾手可得的“性能银弹”表现得如此审慎，甚至迟迟未能将其拥抱入标准库的怀抱？一场在 Go 官方仓库持续了五年之久的 <a href="https://github.com/golang/go/issues/31908">Issue 讨论（#31908）</a>，为我们揭开了这层神秘的面纱。这并非简单的技术取舍，而是 Go 在其设计哲学、工程现实与安全红线之间进行反复权衡的结果。本文将深入这场讨论，为您揭秘阻碍 io_uring 在 Go 中落地的三大核心困境。</p>
<h2>io_uring：一场 I/O 模型的革命</h2>
<p>要理解这场争论，我们首先需要明白 io_uring 究竟是什么，以及它为何具有革命性。</p>
<p>在 io_uring 出现之前，Linux 上最高效的 I/O 模型是 epoll。epoll 采用的是一种“拉（pull）”模型：应用程序通过一次 epoll_wait 系统调用来询问内核：“有我关心的文件描述符准备好进行 I/O 了吗？”。内核响应后，应用程序需要再为每个就绪的描述符分别发起 read 或 write 系统调用。这意味着，<strong>处理 N 个 I/O 事件至少需要 N+1 次系统调用</strong>。</p>
<p>而 io_uring 则彻底改变了游戏规则。它在内核与用户空间之间建立了两个共享内存环形缓冲区：<strong>提交队列（Submission Queue, SQ）</strong>和<strong>完成队列（Completion Queue, CQ）</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/why-go-not-embrace-iouring-2.png" alt="" /></p>
<p>其工作流程如下：</p>
<ol>
<li><strong>提交请求:</strong> 应用程序将一个或多个 I/O 请求（如读、写、连接等）作为条目（SQE）放入提交队列中。这仅仅是内存操作，<strong>几乎没有开销</strong>。</li>
<li><strong>通知内核:</strong> 应用通过一次 io_uring_enter 系统调用，通知内核“请处理队列中的所有请求”。在特定模式（SQPOLL）下，这个系统调用甚至可以被省略。</li>
<li><strong>内核处理:</strong> 内核从提交队列中批量取走所有请求，并异步地执行它们。</li>
<li><strong>返回结果:</strong> 内核将每个操作的结果作为一个条目（CQE）放入完成队列。这同样只是内存操作。</li>
<li><strong>应用收获:</strong> 应用程序直接从完成队列中读取结果，无需为每个结果都发起一次系统调用。</li>
</ol>
<p>这种模式的优势是颠覆性的：<strong>它将 N+1 次系统调用压缩为 1 次甚至 0 次</strong>，极大地降低了上下文切换的开销，并且首次为 Linux 带来了真正意义上的、无需 O_DIRECT 标志的<strong>异步文件 I/O</strong>。</p>
<h2>最初的希望：一剂治愈 Go I/O“顽疾”的良药</h2>
<p>讨论伊始，Go 社区对 io_uring 寄予厚望，期待它能一举解决 Go 在 I/O 领域的两大历史痛点：</p>
<ol>
<li><strong>真正的异步文件 I/O：</strong> Go 的网络 I/O 基于 epoll 实现了非阻塞，但文件 I/O 本质上是阻塞的。为了避免阻塞系统线程，Go 运行时不得不维护一个线程池来处理文件操作。正如社区所期待的，io_uring 最大的吸引力在于<strong>“移除对文件 I/O 线程池的需求”</strong>，让文件 I/O 也能享受与网络 I/O 同等的高效与优雅。</li>
<li><strong>极致的网络性能：</strong> 对于高并发服务器，io_uring 通过将多个 read/write 操作打包成一次系统调用，能显著降低内核态与用户态切换的开销，这在“熔断”和“幽灵”漏洞导致 syscall 成本飙升的后时代尤为重要。</li>
</ol>
<p>然而，Go 核心团队很快就为这股热情泼上了一盆“冷水”。</p>
<h2>核心困境一：运行时模型的“哲学冲突”</h2>
<p>这是阻碍 io_uring 集成最根本、最核心的障碍。Go 的成功很大程度上归功于其简洁的并发模型——goroutine，以及对开发者完全透明的调度机制。但 io_uring 的工作模式，与 Go 运行时的核心哲学存在着深刻的冲突。</p>
<p><strong>冲突的焦点在于“透明性”</strong>。Ian Lance Taylor 多次强调，问题不在于 io_uring 能否在 Go 中使用，而在于能否<strong>“透明地”</strong>将其融入现有的 os 和 net 包，而不破坏 Go 开发者早已习惯的 API 和心智模型。</p>
<p>io_uring 的性能优势源于<strong>批处理</strong>。但 Go 的标准库 API，如 net.Conn.Read()，是一个独立的、阻塞式的调用。Go 用户习惯于在独立的 goroutine 中处理独立的连接。如何将这些分散的独立 I/O 请求，在用户无感知的情况下，<strong>“透明地”</strong>收集起来，打包成批？这几乎是一个无解的难题。</p>
<p>社区也提出了“每个 P (Processor) 一个 io_uring 环”的设想，但 Ian 指出这会引入极高的复杂性，包括环的争用、空闲 P 的等待与唤醒、P 与 M 切换时的状态管理等。正如一些社区成员所总结的，io_uring 需要一种全新的 I/O 模式，而这与 Go 现有网络模型的模式完全不同。强行“透明”集成，无异于“在不破坏现有 API 的情况下进行不必要的破坏”。</p>
<h2>核心困境二：现实世界的“安全红线”</h2>
<p>如果说运行时模型的冲突是理论上的“天堑”，那么安全问题则是实践中不可逾越的“红线”。</p>
<p>在 2024 年初，社区成员 jakebailey 抛出了一个重磅消息：<strong>出于安全考虑，Docker 默认的 seccomp 配置文件已经禁用了 io_uring</strong>。</p>
<blockquote>
<p><strong>引用自 Docker 的 commit 信息:</strong> “安全专家普遍认为 io_uring 是不安全的。事实上，Google ChromeOS 和 Android 已经关闭了它，所有 Google 生产服务器也关闭了它。”</p>
</blockquote>
<p>这个消息对标准库集成而言几乎是致命一击。Go 程序最常见的部署环境就是容器。一个不被“普遍情况”支持的特性，无论其性能多么优越，都难以成为Go运行时和标准库的基石。</p>
<h2>核心困境三：追赶一个“移动的目标”</h2>
<p>在这场长达五年的讨论中，io_uring 自身也在飞速进化。其作者<a href="https://github.com/axboe">Jens Axboe</a> 甚至亲自下场，<a href="https://github.com/golang/go/issues/31908#issuecomment-571651387">解答了 Go 团队早期的疑虑</a>，例如移除了并发数限制、解决了事件丢失问题等。</p>
<p>但这恰恰揭示了第三重困境：<strong>要集成一个仍在高速演进、API 不断变化的底层接口，本身就充满了风险和不确定性</strong>。标准库追求的是极致的稳定性和向后兼容性。过早地依赖一个“移动的目标”，可能会带来持续的维护负担和潜在的破坏性变更。对于一个需要支持多个内核版本的语言运行时来说，这种复杂性是难以承受的。</p>
<h2>小结：审慎的巨人与退潮的社区热情</h2>
<p>io_uring 未能在 Go中落地，并非因为 Go 团队忽视性能，而是其成熟与审慎的体现。三大核心困境层层递进，揭示了其迟迟未能拥抱 io_uring 的深层原因：<strong>哲学上的范式冲突、现实中的安全红线、以及工程上的稳定性质疑。</strong></p>
<p>然而，现实比理论更加残酷。在讨论初期，Go 社区曾涌现出一批充满激情的用户层 io_uring 库，如 giouring、go-uring 等，它们是开发者们探索新大陆的先锋。但时至 2025 年，我们观察到一个令人沮丧的趋势：<strong>这些曾经的追星项目大多已陷入沉寂，更新寥寥，星光黯淡。</strong></p>
<p>与之形成鲜明对比的是，Rust 的 tokio-uring 库依然保持着旺盛的生命力，社区活跃，迭代频繁。这似乎在暗示，问题不仅在于 io_uring 本身，更在于它与特定语言运行时模型的“契合度”。<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4036682086282166273#wechat_redirect">Go 运行时的 G-P-M 调度模型</a>和它所倡导的编程范式，使得社区自发的集成尝试也步履维艰，最终热情退潮。</p>
<p>这是否意味着 Go 与 io_uring 将永远无缘？或许未来之路有二：一是等待 io_uring 自身和其生态环境（尤其是安全方面）完全成熟；二是 Go 也许可能会引入一套全新的、<strong>非透明的</strong>、专为高性能 I/O 设计的新标准库包。</p>
<p>在此之前，Go 运行时可能会选择先挖掘 epoll 的全部潜力。这场长达五年的讨论，最终为我们留下了一个深刻的启示：技术的采纳从来不是一场单纯的性能赛跑，它是一场包含了设计哲学、生态现实与工程智慧的复杂博弈。</p>
<p>资料链接：</p>
<ul>
<li>https://github.com/golang/go/issues/31908</li>
<li>https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/</li>
</ul>
<p>关注io_uring在Linux kernel内核演进的小伙伴儿们，可以关注<a href="https://lore.kernel.org/io-uring/">io-uring.vger.kernel.org archive mirror</a>这个页面，或<a href="https://github.com/axboe/liburing/wiki">io_uring作者Jens Axboe的liburing wiki</a>。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/11/why-go-not-embrace-iouring/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go官方 HTTP/3 实现终迎曙光：x/net/http3 提案启动，QUIC 基础已就位</title>
		<link>https://tonybai.com/2025/08/02/proposal-http3/</link>
		<comments>https://tonybai.com/2025/08/02/proposal-http3/#comments</comments>
		<pubDate>Fri, 01 Aug 2025 22:58:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[0-RTT]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[HTTP2]]></category>
		<category><![CDATA[http3]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[QUIC]]></category>
		<category><![CDATA[quic-go]]></category>
		<category><![CDATA[Samba]]></category>
		<category><![CDATA[Socket]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[WebTransport]]></category>
		<category><![CDATA[网络编程]]></category>
		<category><![CDATA[队头阻塞]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4985</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/02/proposal-http3 大家好，我是Tony Bai。 在社区长达数年的热切期盼之后，Go 官方终于迈出了支持 HTTP/3 的关键一步。一项编号为#70914的新提案，正式建议在 x/net/http3 中添加一个实验性的 HTTP/3 实现。这一进展建立在另一项更基础的提案 #58547(x/net/quic) 之上，该提案的实现已取得重大进展，并已从内部包移至公开的 x/net/quic。这意味着 Go 的网络栈即将迎来一次基于 UDP 的、彻底的现代化升级。本文将带您回顾 Go 社区对 HTTP/3 的漫长期待，深入解读官方 QUIC 和 HTTP/3 的实现策略，并探讨其对未来 Go 网络编程的深远影响。 一场长达五年的等待 对 HTTP/3 的支持，可以说是 Go 社区近年来呼声最高的功能之一。早在 2019 年，issue #32204 就被创建，用于追踪在标准库中支持 HTTP/3 的进展。在随后的五年里，随着 Chrome、Firefox 等主流浏览器以及 Cloudflare 等基础设施提供商纷纷拥抱 HTTP/3，社区的期待也日益高涨。 在此期间，由 Marten Seemann 维护的第三方库 quic-go 成为了 Go 生态中事实上的标准，为 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/proposal-http3-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/02/proposal-http3">本文永久链接</a> &#8211; https://tonybai.com/2025/08/02/proposal-http3</p>
<p>大家好，我是Tony Bai。</p>
<p>在社区长达数年的热切期盼之后，Go 官方终于迈出了支持 HTTP/3 的关键一步。一项编号为<a href="https://github.com/golang/go/issues/70914">#70914的新提案</a>，正式建议在 x/net/http3 中添加一个实验性的 HTTP/3 实现。这一进展建立在另一项更基础的提案 <a href="https://github.com/golang/go/issues/58547">#58547(x/net/quic) 之上</a>，该提案的实现已取得重大进展，并已从内部包移至公开的 x/net/quic。这意味着 Go 的网络栈即将迎来一次基于 UDP 的、彻底的现代化升级。本文将带您回顾 Go 社区对 HTTP/3 的漫长期待，深入解读官方 QUIC 和 HTTP/3 的实现策略，并探讨其对未来 Go 网络编程的深远影响。</p>
<h2>一场长达五年的等待</h2>
<p>对 HTTP/3 的支持，可以说是 Go 社区近年来呼声最高的功能之一。早在 2019 年，<a href="https://github.com/golang/go/issues/32204">issue #32204</a> 就被创建，用于追踪在标准库中支持 HTTP/3 的进展。在随后的五年里，随着 Chrome、Firefox 等主流浏览器以及 Cloudflare 等基础设施提供商纷纷拥抱 HTTP/3，社区的期待也日益高涨。</p>
<p>在此期间，由 Marten Seemann 维护的第三方库 <a href="https://github.com/quic-go/quic-go">quic-go</a> 成为了 Go 生态中事实上的标准，为 <a href="https://tonybai.com/2024/11/07/exploring-caddy/">Caddy</a> 等项目提供了生产级的 QUIC 和 HTTP/3 支持。然而，许多开发者仍然期盼一个“电池内置”的官方解决方案，以保证与 Go 标准库（特别是 net/http 和 crypto/tls）的最佳集成和长期维护。</p>
<p>Go 团队对此一直持谨慎态度，主要原因在于：</p>
<ol>
<li><strong>协议稳定性</strong>：在 QUIC 和 HTTP/3 的 IETF 标准（RFC 9000 和 RFC 9114）正式发布前，过早投入实现可能会面临巨大的变更成本。</li>
<li><strong>API 设计复杂性</strong>：QUIC 协议引入了连接、流、0-RTT 等新概念，其 API 设计需要与现有的 net.Conn 和 net.Listener 体系进行权衡，这是一个巨大的挑战。</li>
<li><strong>实现难度巨大</strong>：一个高性能、安全的 QUIC 协议栈，涉及复杂的流量控制、拥塞控制、丢包恢复等机制，其实现工作量远超 HTTP/2。</li>
</ol>
<h2>两步走战略：先 QUIC，后 HTTP/3</h2>
<p>现在，随着协议的标准化和 crypto/tls 中 QUIC 支持的落地，Go 团队终于启动了官方的实现计划，并采取了清晰的“两步走”战略。</p>
<h3>第一步：构建 QUIC 基础 (x/net/quic)</h3>
<p>提案 <strong>#58547</strong> 旨在 golang.org/x/net/quic 中提供一个 QUIC 协议的实现。这是支持 HTTP/3 的必要前提。经过一段时间的开发，该包的实现已取得重大进展。</p>
<p>Go 团队的核心成员 neild 最近宣布，<strong>该 QUIC 实现已从内部包 (internal/quic) 移至公开的 x/net/quic</strong>，虽然仍处于实验阶段且 API 可能变化，但这标志着它已足够成熟，可以供社区“尝鲜”和提供反馈。</p>
<p><strong>x/net/quic 的核心 API 概念：</strong></p>
<ul>
<li><strong>Endpoint (原 Listener)</strong>: 在一个网络地址上监听 QUIC 流量。</li>
<li><strong>Conn</strong>: 代表一个客户端和服务器之间的 QUIC 连接，可以承载多个流。</li>
<li><strong>Stream</strong>: 一个有序、可靠的字节流，类似于一个 TCP 连接。</li>
</ul>
<pre><code class="go">// 客户端发起连接
conn, err := quic.Dial(ctx, "udp", "127.0.0.1:8000", &amp;quic.Config{})

// 服务器接受连接
endpoint, err := quic.Listen("udp", "127.0.0.1:8000", &amp;quic.Config{})
conn, err := endpoint.Accept(ctx)

// 在连接上创建和接受流
stream, err := conn.NewStream(ctx)
stream, err := conn.AcceptStream(ctx)

// 对流进行读写操作
n, err = stream.Read(buf)
n, err = stream.Write(buf)
stream.Close()
</code></pre>
<p>值得注意的是，官方实现并未直接采用 quic-go 的代码，rsc 在讨论中解释了原因，包括 API 设计理念的差异、代码风格、测试框架依赖以及从零开始实现可能更易于维护等。</p>
<h3>第二步：实现 HTTP/3 (x/net/http3)</h3>
<p>在 x/net/quic 的基础上，提案 <strong>#70914</strong> 正式启动了 x/net/http3 的开发。与 QUIC 一样，它将首先在内部包 (x/net/internal/http3) 中进行开发，待 API 稳定后再移至公开包，并提交最终的 API 审查提案。</p>
<p>从 gopherbot 自动发布的 CL（代码变更）列表中，我们可以看到 HTTP/3 的实现正在紧锣密鼓地进行中，涵盖了 QPACK（HTTP/3 的头部压缩算法）、Transport、Server、请求/响应体传输等核心组件。</p>
<h2>对 Go 网络编程的深远影响</h2>
<p>官方 QUIC 和 HTTP/3 的到来，将为 Go 开发者带来革命性的变化：</p>
<ol>
<li>
<p><strong>透明的协议升级</strong>：可以预见，未来的 net/http 包将能够像当年无缝支持 HTTP/2 一样，透明地支持 HTTP/3。开发者可能无需修改现有代码，http.Get(“https://example.com/”) 就可能自动通过 UDP 下的 QUIC 协议执行，正如 ianlancetaylor 在讨论中确认的那样。</p>
</li>
<li>
<p><strong>解决队头阻塞 (Head-of-Line Blocking)</strong>：HTTP/3 最大的优势之一是解决了 TCP 队头阻塞问题。对于需要处理大量并发请求的 Go 微服务，这意味着更低的延迟和更高的吞吐量，尤其是在网络不稳定的情况下。</p>
</li>
<li>
<p><strong>更快的连接建立</strong>：QUIC 支持 0-RTT 连接建立，对于需要频繁建立新连接的应用场景，可以显著降低握手延迟。</p>
</li>
<li>
<p><strong>原生多路复用传输层</strong>：QUIC 本身就是一个多路复用的传输协议。虽然提案的初期重点是支持 HTTP/3，但一个标准化的 QUIC API 将为 gRPC over QUIC、WebTransport 以及其他需要多流、低延迟通信的自定义协议打开大门。</p>
</li>
</ol>
<h2>终极形态——当 QUIC 走进 Linux 内核</h2>
<p>尽管 x/net/quic 的开发标志着 Go 官方在用户空间迈出了重要一步，但关于 QUIC 协议的终极愿景，则指向了更深的层次：<strong>Linux 内核原生支持</strong>。最近，由 Xin Long 提交的一系列补丁，首次<a href="https://lwn.net/Articles/1029851/">将内核态 QUIC 的实现提上了 mainline 的议程</a>。</p>
<p><strong>为什么要将 QUIC 移入内核？</strong></p>
<p>将 QUIC 从用户空间库（如 x/net/quic 或 quic-go）下沉到内核，主要有以下几个核心动机：</p>
<ol>
<li><strong>极致的性能潜力</strong>：内核实现能够充分利用现代网络硬件的<strong>协议卸载（protocol offload）</strong>能力，例如 <a href="https://docs.kernel.org/networking/segmentation-offloads.html">GSO/GRO (Generic Segmentation/Receive Offload)</a>。这将极大地降低 CPU 在处理大量小型 UDP 包时的开销，释放出用户空间实现难以企及的性能潜力。</li>
<li><strong>更广泛的可用性</strong>：一旦 QUIC 成为内核支持的协议（如 IPPROTO_QUIC），任何应用程序都可以像使用 TCP 或 UDP 一样，通过标准的 socket() 系统调用来使用它，而无需绑定到任何特定的用户空间库。</li>
<li><strong>统一的生态系统</strong>：内核级别的支持将极大地促进生态系统的发展。Samba、NFS 甚至 curl 等项目已经表现出对内核态 QUIC 的浓厚兴趣。对于 Go 开发者而言，这意味着未来不仅是 net/http，甚至标准库的其他部分或底层系统调用，都可能从 QUIC 中受益。</li>
</ol>
<p><strong>当前的实现与挑战</strong></p>
<p>Xin Long 的补丁集展示了一个高度集成化的设计：</p>
<ul>
<li><strong>熟悉的 Sockets API</strong>：开发者将能够使用 socket(AF_INET, SOCK_STREAM, IPPROTO_QUIC) 这样的调用来创建一个 QUIC 套接字，并继续使用 bind(), connect(), listen(), accept() 等熟悉的 API。</li>
<li><strong>用户空间 TLS 握手</strong>：与内核 TLS (KTLS) 的设计类似，复杂的 TLS 握手和证书验证逻辑仍然被委托给用户空间处理。一旦握手完成，内核将接管加密和解密的数据流。</li>
<li><strong>性能仍在优化</strong>：初步的基准测试显示，当前的内核实现性能尚不及 KTLS 甚至原生 TCP。这主要是由于缺少硬件卸载支持、额外的内存拷贝以及 QUIC 头部加密的开销。但随着实现的成熟和硬件厂商的跟进，这一差距有望迅速缩小。</li>
</ul>
<p>不过，预计内核态 QUIC 的合入可能要到 2026 年甚至更晚。</p>
<h2>小结：Go 网络生态的下一座里程碑</h2>
<p>尽管距离在 Go 标准库中稳定地使用 http.Server{&#8230;}.ListenAndServeQUIC() 可能还有一段时间，但 x/net/quic 的公开和 x/net/http3 提案的启动，标志着 Go 官方已经吹响了向下一代网络协议进军的号角。</p>
<p>对于 Go 社区而言，这是一个令人振奋的信号。它不仅回应了开发者们长久以来的期待，也确保了 Go 在未来依然是构建高性能、现代化网络服务的首选语言。我们期待着 x/net/http3 的成熟，并最终看到它被无缝地集成到 net/http 标准库中，为所有 Go 开发者带来更快、更可靠的网络体验。</p>
<h2>参考资料</h2>
<ul>
<li>https://github.com/golang/go/issues/70914</li>
<li>https://github.com/golang/go/issues/58547</li>
<li>https://github.com/golang/go/issues/32204</li>
<li>https://lwn.net/Articles/1029851/</li>
</ul>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/02/proposal-http3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>如何像gitlab-runner那样将Go应用安装为系统服务</title>
		<link>https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner/</link>
		<comments>https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner/#comments</comments>
		<pubDate>Mon, 12 Sep 2022 06:11:13 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[APUE]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C语言]]></category>
		<category><![CDATA[Daemon]]></category>
		<category><![CDATA[DanielTheophanes]]></category>
		<category><![CDATA[flag]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[gitlab-runner]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-daemon]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[journalctl]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[restart]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[subcommand]]></category>
		<category><![CDATA[system-service]]></category>
		<category><![CDATA[systemctl]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[systemd-unit]]></category>
		<category><![CDATA[sysvinit]]></category>
		<category><![CDATA[UNIX环境高级编程]]></category>
		<category><![CDATA[upstart]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[后台]]></category>
		<category><![CDATA[命令行参数]]></category>
		<category><![CDATA[守护程序]]></category>
		<category><![CDATA[守护进程]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[系统服务]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3663</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner 在《让reviewdog支持gitlab-push-commit，守住代码质量下限》一文中，gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤)： # Create a GitLab CI user sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash # Install and run as service sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start 在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等，我们暂不care)，系统服务就是由systemd管理的daemon process(守护进程)。 systemd是什么？linux主机上电后，os内核被加载并启动，os内核完成初始化以后，由内核第一个启动的程序是init程序，其PID(进程ID)为1，它为系统里所有进程的“祖先”，systemd便是主流新版linux中的那个init程序，它负责在主机启动后拉起所有安装为系统服务的程序。 这些被systemd拉起的服务程序以守护进程(daemon process)的形式运行，那什么又是守护进程呢？《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的： Daemons are processes that live for a long time. They are often [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner">本文永久链接</a> &#8211; https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner</p>
<p>在<a href="https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor">《让reviewdog支持gitlab-push-commit，守住代码质量下限》</a>一文中，<a href="https://gitlab.com/gitlab-org/gitlab-runner">gitlab-runner</a>(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤)：</p>
<pre><code># Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start
</code></pre>
<p>在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等，我们暂不care)，系统服务就是由<a href="https://systemd.io">systemd</a>管理的daemon process(守护进程)。</p>
<p>systemd是什么？linux主机上电后，os内核被加载并启动，os内核完成初始化以后，由内核第一个启动的程序是init程序，其PID(进程ID)为1，它为系统里所有进程的“祖先”，<strong>systemd便是主流新版linux中的那个init程序，它负责在主机启动后拉起所有安装为系统服务的程序</strong>。</p>
<p>这些被systemd拉起的服务程序以<strong>守护进程(daemon process)</strong>的形式运行，那什么又是守护进程呢？<a href="https://book.douban.com/subject/25900403/">《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》</a>一书中是这样定义的：</p>
<pre><code>Daemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities.

守护进程是长期存在的进程。它们通常在系统启动时被启动，并在系统关闭时才终止。因为它们没有控制终端，我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。
</code></pre>
<p>该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules))，并给出了一个<a href="https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher">C语言</a>示例：</p>
<pre><code>#include "apue.h"
#include &lt;syslog.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sys/resource.h&gt;

void
daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /*
     * Clear file creation mask.
     */
    umask(0);
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &amp;rl) &lt; 0)
        err_quit("%s: can’t get file limit", cmd);
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) &lt; 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won’t allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&amp;sa.sa_mask);

    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &amp;sa, NULL) &lt; 0)
        err_quit("%s: can’t ignore SIGHUP", cmd);
    if ((pid = fork()) &lt; 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    /*
     * Change the current working directory to the root so
     * we won’t prevent file systems from being unmounted.
     */
    if (chdir("/") &lt; 0)
        err_quit("%s: can’t change directory to /", cmd);
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i &lt; rl.rlim_max; i++)
        close(i);
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}
</code></pre>
<p>那么，Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢？很遗憾！<a href="https://github.com/golang/go/issues/227">Go团队说很难做到</a>。Go社区倒是有很多第三方的方案，比如像<a href="https://github.com/sevlyar/go-daemon">go-daemon</a>这样的第三方实现，不过我并没有验证过这些方案，不保证完全ok。</p>
<p><a href="https://github.com/golang/go/issues/227#issuecomment-254819193">Go团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换</a>。gitlab-runner就是将自己安装为system服务，并由systemd对其进行管理的。</p>
<blockquote>
<p>题外话：其实，自从有了容器技术(比如：docker)后，daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器，应用本身就运行于后台，使用&#8211;restart=always/on-failure选项，容器引擎(比如docker engine)会帮我们管理service，并在service宕掉后重启service。</p>
</blockquote>
<p>那么，我们如何像gitlab-runner那样将自己安装为一个systemd service呢？我们继续向下看。</p>
<blockquote>
<p>注意：这里只是将Go应用安装成一个systemd service，并不是自己将自己转换为守护进程，安装为systemd service本身是可行的，也是安全的。</p>
</blockquote>
<p>翻看gitlab-runner源码，你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包，这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包，该包屏蔽了os层的差异，为开发人员提供了相对简单的Service操作接口，包括下面这些控制动作：</p>
<pre><code>// github.com/kardianos/service/blob/master/service.go
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}
</code></pre>
<p>好了，下面我们就用一个例子<a href="https://github.com/bigwhite/experiments/tree/master/system-service">myapp</a>来介绍一下<strong>如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力</strong>。</p>
<p>myapp是一个http server，它在某个端口上提供服务，当收到请求时，返回”Welcome”字样的应答：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

func run(config string) error {
    ... ...

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[%s]: receive a request from: %s\n", c.Server.Addr, r.RemoteAddr)
        w.Write([]byte("Welcome"))
    })
    fmt.Printf("listen on %s\n", c.Server.Addr)
    return http.ListenAndServe(c.Server.Addr, nil)
}
</code></pre>
<p>现在我们要为myapp增加一些能力，让它支持将自己安装为systemd service，并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。</p>
<p>我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包，只是用标准库的flag包。由于myapp支持subcommand，我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例，如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上，比如下面代码init函数体中的内容。</p>
<p>另外由于使用了subcommand，默认的flag.Usage不再能满足我们的要求了，我们需要自己实现一个usage函数并赋值给flag.Usage：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

var (
    installCommand = flag.NewFlagSet("install", flag.ExitOnError)
    runCommand     = flag.NewFlagSet("run", flag.ExitOnError)
    user           string
    workingdir     string
    config         string
)

const (
    defaultConfig = "/etc/myapp/config.ini"
)

func usage() {
    s := `
USAGE:
   myapp command [command options] 

COMMANDS:
     install               install service
     uninstall             uninstall service
     start                 start service
     stop                  stop service
     run                   run service

OPTIONS:
     -config string
        config file of the service (default "/etc/myapp/config.ini")
     -user string
        user account to run the service
     -workingdir string
        working directory of the service`

    fmt.Println(s)
}

func init() {
    installCommand.StringVar(&amp;user, "user", "", "user account to run the service")
    installCommand.StringVar(&amp;workingdir, "workingdir", "", "working directory of the service")
    installCommand.StringVar(&amp;config, "config", "/etc/myapp/config.ini", "config file of the service")
    runCommand.StringVar(&amp;config, "config", defaultConfig, "config file of the service")
    flag.Usage = usage
}

func main() {
    var err error
    n := len(os.Args)
    if n &lt;= 1 {
        fmt.Printf("invalid args\n")
        flag.Usage()
        return
    }

    subCmd := os.Args[1] // the second arg

    // get Config
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }
... ...
}
</code></pre>
<p>这些都完成后，我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息：</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/system-service/config.go

func getServiceConfig(subCmd string) (*service.Config, error) {
    c := service.Config{
        Name:             "myApp",
        DisplayName:      "Go Daemon Service Demo",
        Description:      "This is a Go daemon service demo",
        Executable:       "/usr/local/bin/myapp",
        Dependencies:     []string{"After=network.target syslog.target"},
        WorkingDirectory: "",
        Option: service.KeyValue{
            "Restart": "always", // Restart=always
        },
    }   

    switch subCmd {
    case "install":
        installCommand.Parse(os.Args[2:])
        if user == "" {
            fmt.Printf("error: user should be provided when install service\n")
            return nil, errors.New("invalid user")
        }
        if workingdir == "" {
            fmt.Printf("error: workingdir should be provided when install service\n")
            return nil, errors.New("invalid workingdir")
        }
        c.UserName = user
        c.WorkingDirectory = workingdir

        // arguments
        // ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"
        c.Arguments = append(c.Arguments, "run", "-config", config)
    case "run":
        runCommand.Parse(os.Args[2:]) // parse config
    }   

    return &amp;c, nil
}
</code></pre>
<p>这里要注意的是service.Config中的Option和Arguments，前者用于在systemd service unit配置文件中放置任意的键值对（比如这里的Restart=always），而Arguments则会被组成为ExecStart键的值，该值会在start service时传入使用。</p>
<p>接下来，我们便利用service包基于加载的Config创建操作服务的实例(srv)，然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。</p>
<pre><code>// https://github.com/bigwhite/experiments/blob/master/system-service/main.go
func main() {

    // ... ...
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }

    prg := &amp;NullService{}
    srv, err := service.New(prg, c)
    if err != nil {
        fmt.Printf("new service error: %s\n", err)
        return
    }

    err = runServiceControl(srv, subCmd)
    if err != nil {
        fmt.Printf("%s operation error: %s\n", subCmd, err)
        return
    }

    fmt.Printf("%s operation ok\n", subCmd)
    return
}

func runServiceControl(srv service.Service, subCmd string) error {
    switch subCmd {
    case "run":
        return run(config)
    default:
        return service.Control(srv, subCmd)
    }
}
</code></pre>
<p>好了，代码已经完成！现在让我们来验证一下myapp的能力。</p>
<p>我们先来完成编译和二进制程序的安装：</p>
<pre><code>$make
go build -o myapp main.go config.go

$sudo make install
cp ./myapp /usr/local/bin
$sudo make install-cfg
mkdir -p /etc/myapp
cp ./config.ini /etc/myapp
</code></pre>
<p>接下来，我们就来将myapp安装为systemd的服务：</p>
<pre><code>$sudo ./myapp install -user tonybai -workingdir /home/tonybai
install operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead)
</code></pre>
<p>我们看到安装后，myApp已经成为了myApp.service，并处于inactive状态，其systemd unit文件/etc/systemd/system/myApp.service内容如下：</p>
<pre><code>$sudo cat /etc/systemd/system/myApp.service
[Unit]
Description=This is a Go daemon service demo
ConditionFileIsExecutable=/usr/local/bin/myapp

After=network.target syslog.target 

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"

WorkingDirectory=/home/tonybai
User=tonybai

Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/myApp

[Install]
WantedBy=multi-user.target
</code></pre>
<p>接下来，我们来启动一下该服务：</p>
<pre><code>$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.3M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
</code></pre>
<p>我们看到myApp服务成功启动，并在65432这个端口上监听！</p>
<p>我们利用curl向这个端口发送一个请求：</p>
<pre><code>$curl localhost:65432
Welcome                                                                         

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
</code></pre>
<p>我们看到myApp服务运行正常并返回预期应答结果。</p>
<p>现在我们利用stop subcommand停掉该服务：</p>
<pre><code>$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago
    Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM)
   Main PID: 623859 (code=killed, signal=TERM)

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo...
Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded.
Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo.
</code></pre>
<p>修改配置/etc/myapp/config.ini（将监听端口从65432改为65431），然后再重启该服务：</p>
<pre><code>$sudo cat /etc/myapp/config.ini
[server]
addr=":65431"

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago
   Main PID: 624046 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431
</code></pre>
<p>从systemd的状态日志中我们看到myApp服务启动成功，并改为监听65431端口，我们访问一下该端口：</p>
<pre><code>$curl localhost:65431
Welcome                                                                                                                      

$curl localhost:65432
curl: (7) Failed to connect to localhost port 65432: Connection refused
</code></pre>
<p>从上述结果可以看出，我们的配置更新和重启都是成功的！</p>
<p>我们亦可以使用myapp的uninstall功能从systemd中卸载该服务：</p>
<pre><code>$sudo ./myapp uninstall
uninstall operation ok
$sudo systemctl status myApp
Unit myApp.service could not be found.
</code></pre>
<p>好了，到这里我们看到：在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。</p>
<p>最后小结一下：service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力，不过这个作业就留给大家了:)。大家如有问题，欢迎在评论区留言。</p>
<p>本文涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/system-service">这里</a>下载。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.19中值得关注的几个变化</title>
		<link>https://tonybai.com/2022/08/22/some-changes-in-go-1-19/</link>
		<comments>https://tonybai.com/2022/08/22/some-changes-in-go-1-19/#comments</comments>
		<pubDate>Mon, 22 Aug 2022 02:23:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[ballast]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_FLAGS]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DataRaceFree]]></category>
		<category><![CDATA[DRF-SC]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[godoc]]></category>
		<category><![CDATA[goenv]]></category>
		<category><![CDATA[GOGC]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOMEMLIMIT]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[heap]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[LeslieLamport]]></category>
		<category><![CDATA[loong64]]></category>
		<category><![CDATA[memory-model]]></category>
		<category><![CDATA[OOM]]></category>
		<category><![CDATA[pacer]]></category>
		<category><![CDATA[paper]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[SC]]></category>
		<category><![CDATA[SetFinalizer]]></category>
		<category><![CDATA[SetGCPercent]]></category>
		<category><![CDATA[SetMemoryLimit]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[TSO]]></category>
		<category><![CDATA[twitch]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[内存模型]]></category>
		<category><![CDATA[原子操作]]></category>
		<category><![CDATA[同步]]></category>
		<category><![CDATA[同步原语]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[论文]]></category>
		<category><![CDATA[顺序一致性]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3642</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/08/22/some-changes-in-go-1-19 我们知道Go团队在2015年重新规定了团队发布版本的节奏，将Go大版本的发布频率确定为每年两次，发布窗口定为每年的2月与8月。而实现自举的Go 1.5版本是这一个节奏下发布的第一个版本。一般来说，Go团队都会在这两个窗口的中间位置发布版本，不过这几年也有意外，比如承载着泛型落地责任的Go 1.18版本就延迟了一个月发布。 就在我们以为Go 1.19版本不会很快发布的时候，美国时间2022年8月2日，Go核心团队正式发布了Go 1.19版本，这个时间不仅在发布窗口内而且相对于惯例还提前了。为什么呢？很简单，Go 1.19是一个“小”版本，当然这里的“小”是相对于Go 1.18那样的“大”而言的。Go 1.19版本开发周期仅有2个月左右(3~5月初)，这样Go团队压缩了添加到Go 1.19版本中的feature数量。 不过尽管如此，Go 1.19中依然有几个值得我们重点关注的变化点，在这篇文章中我就和大家一起来看一下。 一. 综述 在6月份(那时Go 1.19版本已经Freeze)，我曾写过一篇《Go 1.19新特性前瞻》，简要介绍了当时基本确定的Go 1.19版本的一些新特性，现在来看，和Go 1.19版本正式版差别不大。 泛型方面 考虑到Go 1.18泛型刚刚落地，Go 1.18版本中的泛型并不是完全版。但Go 1.19版本也没有急于实现泛型设计文档)中那些尚未实现的功能特性，而是将主要精力放在了修复Go 1.18中发现的泛型实现问题上了，目的是夯实Go泛型的底座，为Go 1.20以及后续版本实现完全版泛型奠定基础(详细内容可查看《Go 1.19新特性前瞻》一文)。 其他语法方面 无，无，无！重要的事情说三遍。 这样，Go 1.19依旧保持了Go1兼容性承诺。 正式在linux上支持龙芯架构(GOOS=linux, GOARCH=loong64) 这一点不得不提，因为这一变化都是国内龙芯团队贡献的。不过目前龙芯支持的linux kernel版本最低也是5.19，意味着龙芯在老版本linux上还无法使用Go。 go env支持CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_FFLAGS, CGO_LDFLAGS和GOGCCFLAGS 当你想设置全局的而非包级的CGO构建选项时，可以通过这些新加入的CGO相关环境变量进行，这样就可以避免在每个使用Cgo的Go源文件中使用cgo指示符来分别设置了。 目前这些用于CGO的go环境变量的默认值如下(以我的macos上的默认值为例)： CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/08/22/some-changes-in-go-1-19">本文永久链接</a> &#8211; https://tonybai.com/2022/08/22/some-changes-in-go-1-19</p>
<p>我们知道Go团队在2015年重新规定了团队发布版本的节奏，将Go大版本的发布频率确定为每年两次，发布窗口定为每年的2月与8月。而实现自举的<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5版本</a>是这一个节奏下发布的第一个版本。一般来说，Go团队都会在这两个窗口的中间位置发布版本，不过这几年也有意外，比如承载着泛型落地责任的<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18版本</a>就延迟了一个月发布。</p>
<p>就在我们以为Go 1.19版本不会很快发布的时候，美国时间2022年8月2日，<a href="https://go.dev/blog/go1.19">Go核心团队正式发布了Go 1.19版本</a>，这个时间不仅在发布窗口内而且相对于惯例还提前了。为什么呢？很简单，<strong>Go 1.19是一个“小”版本</strong>，当然这里的“小”是相对于Go 1.18那样的“大”而言的。Go 1.19版本开发周期仅有2个月左右(3~5月初)，这样Go团队压缩了添加到Go 1.19版本中的feature数量。</p>
<p>不过尽管如此，Go 1.19中依然有几个值得我们重点关注的变化点，在这篇文章中我就和大家一起来看一下。</p>
<h3>一. 综述</h3>
<p>在6月份(那时Go 1.19版本已经Freeze)，我曾写过一篇<a href="https://tonybai.com/2022/06/12/go-1-19-foresight">《Go 1.19新特性前瞻》</a>，简要介绍了当时基本确定的Go 1.19版本的一些新特性，现在来看，和Go 1.19版本正式版差别不大。</p>
<ul>
<li>泛型方面</li>
</ul>
<p>考虑到Go 1.18泛型刚刚落地，Go 1.18版本中的泛型并不是完全版。但Go 1.19版本也没有急于实现<a href="https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md">泛型设计文档</a>)中那些尚未实现的功能特性，而是将主要精力放在了修复Go 1.18中发现的<a href="https://github.com/golang/go/issues?q=is%3Aissue+label%3Agenerics+milestone%3AGo1.19">泛型实现问题</a>上了，目的是夯实Go泛型的底座，为Go 1.20以及后续版本实现完全版泛型奠定基础(详细内容可查看<a href="https://tonybai.com/2022/06/12/go-1-19-foresight">《Go 1.19新特性前瞻》</a>一文)。</p>
<ul>
<li>其他语法方面</li>
</ul>
<p>无，无，无！重要的事情说三遍。</p>
<p>这样，Go 1.19依旧保持了Go1兼容性承诺。</p>
<ul>
<li>正式在linux上支持龙芯架构(GOOS=linux, GOARCH=loong64)</li>
</ul>
<p>这一点不得不提，因为这一变化都是国内龙芯团队贡献的。不过目前龙芯支持的linux kernel版本最低也是5.19，意味着龙芯在老版本linux上还无法使用Go。</p>
<ul>
<li>go env支持CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_FFLAGS, CGO_LDFLAGS和GOGCCFLAGS</li>
</ul>
<p>当你想设置全局的而非包级的CGO构建选项时，可以通过这些新加入的CGO相关环境变量进行，这样就可以避免在每个使用Cgo的Go源文件中使用cgo指示符来分别设置了。</p>
<p>目前这些用于CGO的go环境变量的默认值如下(以我的macos上的默认值为例)：</p>
<pre><code>CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1672298076=/tmp/go-build -gno-record-gcc-switches -fno-common"
</code></pre>
<p>其他更具体的变化就不赘述了，大家可以移步<a href="https://tonybai.com/2022/06/12/go-1-19-foresight">《Go 1.19新特性前瞻》</a>看看。</p>
<p>下面我们重点说说Go 1.19中的两个重要变化：<strong>新版Go内存模型文档与Go运行时引入Soft memory limit</strong>。</p>
<h3>二. 修订Go内存模型文档</h3>
<p>记得当年初学Go的时候，所有Go官方文档中最难懂的一篇就属<a href="https://go.dev/ref/mem">Go内存模型文档</a>(如下图)这一篇了，相信很多gopher在初看这篇文档时一定有着和我相似的赶脚^_^。</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-2.png" alt="" /><br />
<center>图：老版Go内存模型文档</center></p>
<blockquote>
<p>注：查看老版Go内存模型文档的方法：godoc -http=:6060 -goroot /Users/tonybai/.bin/go1.18.3，其中godoc已经不随着go安装包分发了，需要你单独安装，命令为：go install golang.org/x/tools/cmd/godoc。</p>
</blockquote>
<p>那么，老版内存模型文档说的是啥呢？为什么要修订？搞清这两个问题，我们就大致知道新版内存模型文档的意义了。 我们先来看看什么是编程语言的内存模型。</p>
<h4>1. 什么是内存模型？</h4>
<p>提到内存模型，我们要从著名计算机科学家，2013年图灵奖得主<a href="https://www.microsoft.com/en-us/research/people/lamport/">Leslie Lamport</a>在1979发表的名为<a href="https://www.microsoft.com/en-us/research/publication/make-multiprocessor-computer-correctly-executes-multiprocess-programs/">《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》</a>的论文说起。</p>
<p>在这篇文章中，Lamport给出了多处理器计算机在共享内存的情况下并发程序正确运行的条件，即多处理器要满足<strong>顺序一致性(sequentially consistent)</strong>。</p>
<p>文中提到：一个高速运行的处理器不一定按照程序指定的顺序(代码顺序)执行。如果一个处理器的执行结果(可能是乱序执行)与按照程序指定的顺序(代码顺序)执行的结果一致，那么说这个处理器是<strong>有序的(sequential)</strong>。</p>
<p>而对于一个共享内存的多处理器而言，只有满足下面条件，才能被认定是满足<strong>顺序一致性</strong>的，即具备保证并发程序正确运行的条件：</p>
<ul>
<li>任何一次执行的结果，都和所有处理器的操作按照某个顺序执行的结果一致;</li>
<li>在“某个顺序执行”中单独看每个处理器，每个处理器也都是按照程序指定的顺序(代码顺序)执行的。</li>
</ul>
<p><strong>顺序一致性就是一个典型的共享内存、多处理器的内存模型</strong>，这个模型保证了所有的内存访问都是以原子方式和按程序顺序进行的。下面是一个共享内存的顺序一致性的抽象机器模型示意图，图来自于<a href="https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf">《A Tutorial Introduction to the ARM and POWER Relaxed Memory Models》</a> ：</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-4.png" alt="" /></p>
<p>根据顺序一致性，上面图中的抽象机器具有下面特点：</p>
<ul>
<li>没有本地的重新排序：每个硬件线程按照程序指定的顺序执行指令，完成每条指令（包括对共享内存的任何读或写）后再开始下一条。</li>
<li>每条写入指令对所有线程（包括进行写入的线程）都是同时可见的。</li>
</ul>
<p>从程序员角度来看，顺序一致性的内存模型是再理想不过了。所有读写操作直面内存，没有缓存，一个处理器(或硬件线程)写入内存的值，其他处理器(或硬件线程)便可以观察到。借助硬件提供的顺序一致性(SC)，我们可以实现“所写即所得”。</p>
<p>但是这样的机器真的存在吗？并没有，至少在量产的机器中并没有。为什么呢？因为顺序一致性不利于硬件和软件的性能优化。真实世界的共享内存的多处理器计算机的常见机器模型是这样的，也称为Total Store Ordering，TSO模型(图来自<a href="https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf">《A Tutorial Introduction to the ARM and POWER Relaxed Memory Models》</a>)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-5.png" alt="" /></p>
<p>我们看到，在这种机器下，所有处理器仍连接到单个共享内存，但每个处理器的写内存操作从写入共享内存变为了先写入本处理器的写缓存队列(write buffer)，这样处理器无需因要等待写完成(write complete)而被阻塞，并且一个处理器上的读内存操作也会先查阅本处理器的写缓存队列(但不会查询其他处理器的写缓存队列)。写缓存队列的存在极大提升了处理器写内存操作的速度。</p>
<p>但也正是由于写缓存的存在，TSO模型无法满足顺序一致性，比如：“每条写入指令对所有线程（包括进行写入的线程）都是同时可见的”这一特性就无法满足，因为写入本地写缓存队列的数据在未真正写入共享内存前只对自己可见，对其他处理器(硬件线程)并不可见。</p>
<p>根据Lamport的理论，在不满足SC的多处理器机器上程序员没法开发出可以正确运行的并发程序(Data Race Free, DRF)，那么怎么办呢？处理器提供同步指令给开发者。对开发者而言，有了同步指令的非SC机器，具备了SC机器的属性。只是这一切对开发人员不是自动的/透明的了，需要开发人员熟悉同步指令，并在适当场合，比如涉及数据竞争Data Race的场景下正确使用，这大大增加了开发人员的心智负担。</p>
<p>开发人员通常不会直面硬件，这时就要求高级编程语言对硬件提供的同步指令进行封装并提供给开发人员，这就是<strong>编程语言的同步原语</strong>。而编程语言使用哪种硬件同步指令，封装出何种行为的同步原语，怎么应用这些原语，错误的应用示例等都是需要向编程语言的使用者进行说明的。而这些都将是编程语言内存模型文档的一部分。</p>
<p>如今主流的编程语言的内存模型都是<strong>顺序一致性(SC)模型</strong>，它为开发人员提供了一种理想的SC机器(虽然实际中的机器并非SC的)，程序是建构在这一模型之上的。但就像前面说的，开发人员要想实现出正确的并发程序，还必须了解编程语言封装后的同步原语以及他们的语义。<strong>只要程序员遵循并发程序的同步要求合理使用这些同步原语，那么编写出来的并发程序就能在非SC机器上跑出顺序一致性的效果</strong>。</p>
<p>知道了编程语言内存模型的含义后，接下来，我们再来看看老版Go内存模型文档究竟表述了什么。</p>
<h4>2. Go内存模型文档</h4>
<p>按照上面的说明，Go内存模型文档描述的应该是<strong>要用Go写出一个正确的并发程序所要具备的条件</strong>。</p>
<p>再具体点，就像老版内存模型文档开篇所说的那样：<strong>Go内存模型规定了一些条件，一旦满足这些条件，当在一个goroutine中读取一个变量时，Go可以保证它可以观察到不同goroutine中对同一变量的写入所产生的新值</strong>。</p>
<p>接下来，内存模型文档就基于常规的happens-before定义给出了Go提供的各种同步操作及其语义，包括：</p>
<ul>
<li>如果一个包p导入了包q，那么q的init函数的完成发生在p的任何函数的开始之前。</li>
<li>函数main.main的开始发生在所有init函数完成之后。</li>
<li>启动一个新的goroutine的go语句发生在goroutine的执行开始之前。</li>
<li>一个channel上的发送操作发生在该channel的对应接收操作完成之前。</li>
<li>一个channel的关闭发生在一个返回零值的接收之前(因为该channel已经关闭)。</li>
<li>一个无缓冲的channel的接收发生在该channel的发送操作完成之前。</li>
<li>一个容量为C的channel上的第k个接收操作发生在该channel第k+C个发送操作完成之前。</li>
<li>对于任何sync.Mutex或sync.RWMutex变量l，当n&lt;m时，第n次l.Unlock调用发生在第m次调用l.Lock()返回之前。</li>
<li>once.Do(f)中的f()调用发生在对once.Do(f)的任何一次调用返回之前。</li>
</ul>
<p>接下来，内存模型文档还定义了一些误用同步原语的例子。</p>
<p>那么新内存模型文档究竟更新了哪些内容呢？我们继续往下看。</p>
<h4>3. 修订后的内存模型文档都有哪些变化</h4>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-3.png" alt="" /><br />
<center>图：修订后的Go内存模型文档</center></p>
<p>负责更新内存模型文档的Russ Cox首先增加了<strong>Go内存模型的总体方法(overall approach)</strong>。</p>
<p>Go的总体方法在C/C++和Java/Js之间，既不像C/C++那样将存在Data race的程序定义为违法的，让编译器以未定义行为处置它，即运行时表现出任意可能的行为；又不完全像Java/Js那样尽量明确Data Race情况下各种语义，将Data race带来的影响限制在最小，使程序更为可靠。</p>
<p>Go对于一些存在data Race的情况会输出race报告并终止程序，比如多goroutine在未使用同步手段下对map的并发读写。除此之外，Go对其他存数据竞争的场景有明确的语义，这让程序更可靠，也更容易调试。</p>
<p>其次，新版Go内存模型文档增补了对这些年sync包新增的API的说明，比如： mutex.TryLock、mutex.TryRLock等。而对于sync.Cond、Map、Pool、WaitGroup等文档没有逐一描述，而是建议看API文档。</p>
<p>在老版内存模型文档中，没有对sync/atom包进行说明，新版文档增加了对atom包以及runtime.SetFinalizer的说明。</p>
<p>最后，文档除了提供不正确同步的例子，还增加了对不正确编译的例子的说明。</p>
<p>另外这里顺便提一下：Go 1.19在atomic包中引入了一些新的原子类型，包括： Bool, Int32, Int64, Uint32, Uint64, Uintptr和Pointer。这些新类型让开发人员在使用atomic包是更为方便，比如下面是Go 1.18和Go 1.19使用Uint64类型原子变量的代码对比：</p>
<p>对比Uint64的两种作法：</p>
<pre><code>// Go 1.18

var i uint64
atomic.AddUint64(&amp;i, 1)
_ = atomic.LoadUint64(&amp;i)

vs.

// Go 1.19
var i atomic.Uint64 // 默认值为0
i.Store(17) // 也可以通过Store设置初始值
i.Add(1)
_ = i.Load()
</code></pre>
<p>atomic包新增的Pointer，避免了开发人员在使用原子指针时自己使用unsafe.Pointer进行转型的麻烦。同时atomic.Pointer是一个泛型类型，如果我没记错，它是Go 1.18加入comparable预定义泛型类型之后，第一次在Go中引入基于泛型的标准库类型：</p>
<pre><code>// $GOROOT/src/sync/atomic/type.go

// A Pointer is an atomic pointer of type *T. The zero value is a nil *T.
type Pointer[T any] struct {
    _ noCopy
    v unsafe.Pointer
}

// Load atomically loads and returns the value stored in x.
func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&amp;x.v)) }

// Store atomically stores val into x.
func (x *Pointer[T]) Store(val *T) { StorePointer(&amp;x.v, unsafe.Pointer(val)) }

// Swap atomically stores new into x and returns the previous value.
func (x *Pointer[T]) Swap(new *T) (old *T) { return (*T)(SwapPointer(&amp;x.v, unsafe.Pointer(new))) }

// CompareAndSwap executes the compare-and-swap operation for x.
func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
    return CompareAndSwapPointer(&amp;x.v, unsafe.Pointer(old), unsafe.Pointer(new))
}
</code></pre>
<p>此外，atomic包新增的Int64和Uint64类型还有一个特质，那就是Go保证其地址可以自动对齐到8字节上(即地址可以被64整除)，即便在32位平台上亦是如此，这可是<a href="https://github.com/golang/go/issues/36606">连原生int64和uint64也尚无法做到的</a>。</p>
<p><a href="https://go101.org/">go101</a>在推特上分享了一个基于atomic Int64和Uint64的tip。利用go 1.19新增的atomic.Int64/Uint64，我们可以用下面方法保证结构体中某个字段一定是8 byte对齐的，即该字段的地址可以被64整除。</p>
<pre><code>import "sync/atomic"

type T struct {
    _ [0]atomic.Int64
    x uint64 // 保证x是8字节对齐的
}
</code></pre>
<p>前面的代码中，为何不用_ atomic.Int64呢，为何用一个空数组呢，这是因为空数组在go中不占空间，大家可以试试输出上面结构体T的size，看看是不是8。</p>
<h3>三. 引入Soft memory limit</h3>
<h4>1. 唯一GC调优选项：GOGC</h4>
<p>近几个大版本，Go GC并没有什么大的改动/优化。和其他带GC的编程语言相比，Go GC算是一个奇葩的存在了：对于开发者而言，Go 1.19版本之前，Go GC的调优参数仅有一个：<strong>GOGC</strong>(也可以通过runtime/debug.SetGCPercent调整)。</p>
<p>GOGC默认值为100，通过调整它的值，我们可以调整GC触发的时机。计算下一次触发GC的堆内存size的公式如下：</p>
<pre><code>// Go 1.18版本之前
目标堆大小 = (1+GOGC/100) * live heap // live heap为上一次GC标记后的堆上的live object的总size

// Go 1.18版本及之后
目标堆大小 = live heap + (live heap + GC roots) * GOGC / 100
</code></pre>
<blockquote>
<p>注：Go 1.18以后将GC roots(包括goroutine栈大小和全局变量中的指针对象大小)纳入目标堆大小的计算</p>
</blockquote>
<p>以Go 1.18之前的版本为例，当GOGC=100(默认值)时，如果某一次GC后的live heap为10M，那么下一次GC开启的目标堆heap size为20M，即在两次GC之间，应用程序可以分配10M的新堆对象。</p>
<p>可以说<strong>GOGC控制着GC的运行频率</strong>。当GOGC值设置的较小时，GC运行的就频繁一些，参与GC工作的cpu的比重就多一些；当GOGC的值设置的较大时，GC运行的就不那么频繁，相应的参与GC工作的cpu的比重就小一些，但要承担内存分配接近资源上限的风险。</p>
<p>这样一来，摆在开发者面前的问题就是：GOGC的值很难选，这唯一的调优选项也就成为了摆设。</p>
<p>同时，Go runtime是不关心资源limit的，只是会按照应用的需求持续分配内存，并在自身内存池不足的情况下向OS申请新的内存资源，直到内存耗尽(或到达平台给应用分配的memory limit)而被oom killed！</p>
<p>为什么有了GC，Go应用还是会因耗尽系统memory资源而被oom killed呢？我们继续往下看。</p>
<h4>2. Pacer的问题</h4>
<p>上面的触发GC的目标堆大小计算公式，在Go runtime内部被称为pacer算法，pacer中文有翻译成“起搏器”的，有译成“配速器”的。不管译成啥，总而言之它是用来<strong>控制GC触发节奏的</strong>。</p>
<p>不过pacer目前的算法是无法保证你的应用不被OOM killed的，举个例子(见下图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-19-6.png" alt="" /></p>
<p>在这个例子中：</p>
<ul>
<li>一开始live heap始终平稳，净增的heap object保持0，即新分配的heap object与被清扫掉的heap object相互抵消。</li>
<li>后续在(1)处出现一次target heap的跃升(从h/2->h)，原因显然是live heap object变多了，都在用，即便触发GC也无法清除。不过此时target heap(h)是小于hard memory limit的；</li>
<li>程序继续执行，在(2)处，又出现一次target heap的跃升(从h->2h)，而live heap object也变多了，稳定在h，此时，target heap变为2h，高于hard memory limit了；</li>
<li>后续程序继续执行，当live heap object到达(3)时，实际Go的堆内存(包括未清理的)超过了hard memory limit，但由于尚未到达target heap(2h)，GC没有被执行，因此应用被oom killed。</li>
</ul>
<p>我们看到这个例子中，并非Go应用真正需要那么多内存(如果有GC及时清理，live heap object就在(3)的高度)，<strong>而是Pacer算法导致了没能及时触发GC</strong>。</p>
<p>那么如何尽可能的避免oom killed呢？我们接下来看一下Go社区给出了两个“民间偏方”。</p>
<h4>3. Go社区的GC调优方案</h4>
<p>这两个“偏方”, 一个是<a href="https://es.blog.twitch.tv/tr-tr/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/">twitch游戏公司给出的memory ballast(内存压舱石)</a>，另外一个则是<a href="https://www.uber.com/en-US/blog/how-we-saved-70k-cores-across-30-mission-critical-services/">像uber这样的大厂采用的自动GC动态调优方案</a>。当然这两个方案不光是要避免oom，更是为了优化GC，提高程序的执行效率。</p>
<p>下面我们分别简单介绍一下。先来说说twitch公司的memory ballast。twitch的Go服务运行在具有64G物理内存的VM上，通过观察运维人员发现，服务常驻的物理内存消耗仅为400多M，但Go GC的启动却十分频繁，这导致其服务响应的时间较长。twitch的工程师考虑充分利用内存，降低GC的启动频率，从而降低服务的响应延迟。</p>
<p>于是他们想到了一种方法，他们在服务的main函数初始化环节像下面这样声明了一个10G容量的大切片，并保证这个切片在程序退出前不被GC释放掉：</p>
<pre><code>func main() {
    // Create a large heap allocation of 10 GiB
    ballast := make([]byte, 10&lt;&lt;30)

    // Application execution continues
    // ...

    runtime.Keepalive(ballast)
    // ... ...
}
</code></pre>
<p>这个切片由于太大，将在堆上分配并被runtime跟踪，但这个切片并不会给应用带去实质上的物理内存消耗，这得益于os对应用进程内存的<strong>延迟簿记</strong>：只有读写的内存才会导致缺页中断并由OS为之分配物理内存。从类似top的工具来看，这10个G的字节仅会记录在VIRT/VSZ(虚拟内存)上，而不会记录在RES/RSS(常驻内存)上。</p>
<p>这样一来，根据前面Pacer算法的原理，触发GC的下一个目标堆大小就至少为20G，在Go服务分配堆内存到20G之前GC都不会被触发，所有cpu资源都会被用来处理业务，这也与twitch的实测结果一致(GC次数下降99%)。</p>
<p>一旦到了20G，由于之前观测的结果是服务仅需400多M物理内存，大量heap object会被回收，Go服务的live heap会回到400多M，但重新计算目标堆内存时，由于前面那个“压舱石”的存在，目标堆内存已经会在至少20G的水位上，就这样GC次数少了，GC少了，worker goroutine参加“劳役”的时间就少了，cpu利用率高了，服务响应的延迟也下来了。</p>
<blockquote>
<p>注：“劳役”是指worker goroutine在mallocgc内存时被runtime强制“劳役”：停下自己手头的工作，去辅助GC做heap live object的mark。</p>
</blockquote>
<p>不过使用该方案的前提是你对你的Go服务的内存消耗情况(忙闲时)有着精确的了解，这样才能结合硬件资源情况设定合理的ballast值。</p>
<p>按照<a href="https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md">Soft memory limit proposal</a>的说法，该方案的弊端如下：</p>
<ul>
<li>不能跨平台移植，据说Windows上不适用(压舱石的值会直接反映为应用的物理内存占用)；</li>
<li>不能保证随着Go运行时的演进而继续正常工作（比如：一旦pacer算法发生了巨大变化）；</li>
<li>开发者需要进行复杂的计算并估计运行时内存开销以选择适合的ballast大小。</li>
</ul>
<p>接下来我们再来看看自动GC动态调优方案。</p>
<p>去年12月，uber在其官方博客分享了uber内部使用的<a href="https://www.uber.com/en-US/blog/how-we-saved-70k-cores-across-30-mission-critical-services/">半自动化Go GC调优方案</a>，按uber的说法，这种方案实施后帮助uber节省了70K cpu核的算力。其背后的原理依旧是从Pacer的算法公式出发，改变原先Go服务生命周期全程保持GOGC值静态不变的作法，在每次GC时，依据容器的内存限制以及当前的live heap size动态计算并设置GOGC值，从而实现对内存不足oom-killed的保护，同时最大程度利用内存，改善Gc对cpu的占用率。</p>
<p>显然这种方案更为复杂，需要有一个专家团队来保证这种自动调优的参数的设置与方案的实现。</p>
<h4>4. 引入Soft memory limit</h4>
<p>其实Go GC pacer的问题还有很多, Go核心团队开发者Michael Knyszek提了一个<a href="https://github.com/golang/go/issues/42430">pacer问题综述的issue</a>，将这些问题做了汇总。但问题还需一个一个解决，在Go 1.19这个版本中，Michael Knyszek就带来了他的<a href="https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md">Soft memory limit的解决方案</a>。</p>
<p>这个方案在runtime/debug包中添加了一个名为SetMemoryLimit的函数以及GOMEMLIMIT环境变量，通过他们任意一个都可以设定Go应用的Memory limit。</p>
<p>一旦设定了Memory limit，当Go堆大小达到“Memory limit减去非堆内存后的值”时，一轮GC会被触发。即便你手动关闭了GC(GOGC=off)，GC亦是会被触发。</p>
<p>通过原理我们可以看到，这个特性最直接解决的就是oom-killed这个问题！就像前面pacer问题示意图中的那个例子，如果我们设定了一个比hard memory limit小一些的soft memory limit的值，那么在(3)那个点便不会出现oom-killed，因为在那之前soft memory limit就会触发一次GC，将一些无用的堆内存回收掉了。</p>
<p>但我们也要注意：soft memory limit不保证不会出现oom-killed，这个也很好理解。如果live heap object到达limit了，说明你的应用内存资源真的不够了，是时候扩内存条资源了，这个是GC无论如何都无法解决的问题。</p>
<p>但如果一个Go应用的live heap object超过了soft memory limit但还尚未被kill，那么此时GC会被持续触发，但为了保证在这种情况下业务依然能继续进行，soft memory limit方案保证GC最多只会使用50%的CPU算力，以保证业务处理依然能够得到cpu资源。</p>
<p>对于GC触发频率高，要降低GC频率的情况，soft memory limit的方案就是<strong>关闭GC(GOGC=off)</strong>，这样GC只有当堆内存到达soft memory limit值时才会触发，可以提升cpu利用率。不过有一种情况，<a href="https://go.dev/doc/gc-guide">Go官方的GC guide</a>中不建议你这么做，那就是当你的Go程序与其他程序共享一些有限的内存时。这时只需保留内存限制并将其设置为一个较小的合理值即可，因为它可能有助于抑制不良的瞬时行为。</p>
<p>那么多大的值是合理的soft memory limit值呢？在Go服务独占容器资源时，一个好的经验法则是留下额外的5-10%的空间，以考虑Go运行时不知道的内存来源。uber在其博客中设定的limit为资源上限的70%，也是一个不错的经验值。</p>
<h3>四. 小结</h3>
<p>也许Go 1.19因开发周期的压缩给大家带来的惊喜并不多。不过特性虽少，却都很实用，比如上面的soft memory limit，一旦用好，便可以帮助大家解决大问题。</p>
<p>而拥有正常开发周期的Go 1.20已经处于积极的开发中，从目前<a href="https://github.com/golang/go/milestone/250">里程碑</a>中规划的功能和改进来看，Go泛型语法将得到进一步的补全，向着完整版迈进，就这一点就值得大家期待了！</p>
<h3>五. 参考资料</h3>
<ul>
<li>Russ Cox内存模型系列 &#8211; https://research.swtch.com/mm</li>
<li>关于Go内存模型的讨论 &#8211; https://github.com/golang/go/discussions/47141</li>
<li>How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs- https://www.microsoft.com/en-us/research/publication/make-multiprocessor-computer-correctly-executes-multiprocess-programs</li>
<li>A Tutorial Introduction to the ARM and POWER Relaxed Memory Models- https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf</li>
<li>Weak Ordering &#8211; A New Definition- https://people.eecs.berkeley.edu/~kubitron/courses/cs258-S08/handouts/papers/adve-isca90.pdf</li>
<li>Foundations of the C++ Concurrency Memory Model &#8211; https://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf</li>
<li>Go GC pacer原理 &#8211;  https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/08/22/some-changes-in-go-1-19/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>使用Go语言实现eBPF程序内核态与用户态的双向数据交换</title>
		<link>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/</link>
		<comments>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/#comments</comments>
		<pubDate>Mon, 25 Jul 2022 13:26:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BPF_MAP_TYPE_HASH]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[系统调用]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3629</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go 在之前的两篇文章中，无论是使用C语言开发eBPF程序，还是使用Go开发的eBPF程序，都是hello world级别的，可能有用，但谈不上十分实用。 通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，eBPF MAP是绕不过去的机制。 在这一篇有关eBPF程序开发的文章中，我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换。 一. why BPF MAP？ 永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。 那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？ Linux内核BPF开发者于是就引入了BPF MAP机制。BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。 二. BPF MAP不是狭义的map数据结构 BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是一种通用数据结构，可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说，MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念。 截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型： // libbpf/include/uapi/linux/bpf.h enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go</p>
<p>在之前的两篇文章中，无论是<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">使用C语言开发eBPF程序</a>，还是<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/">使用Go开发的eBPF程序</a>，都是hello world级别的，可能有用，但谈不上十分实用。</p>
<p>通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，<strong>eBPF MAP是绕不过去的机制</strong>。</p>
<p>在这一篇有关eBPF程序开发的文章中，我们就来看看<strong>如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换</strong>。</p>
<h3>一. why BPF MAP？</h3>
<p>永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。</p>
<p>那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？</p>
<p>Linux内核BPF开发者于是就引入了<a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">BPF MAP机制</a>。<strong>BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道</strong>。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。</p>
<h3>二. BPF MAP不是狭义的map数据结构</h3>
<p>BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">一种通用数据结构，可以存储不同类型数据的通用数据结构</a>。用著名内核BPF开发者<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">Andrii Nakryiko</a>的话来说，<strong>MAP就是BPF中代表<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">抽象数据容器(abstract data container)</a>的一个概念</strong>。</p>
<p>截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型：</p>
<pre><code>// libbpf/include/uapi/linux/bpf.h
enum bpf_map_type {
    BPF_MAP_TYPE_UNSPEC,
    BPF_MAP_TYPE_HASH,
    BPF_MAP_TYPE_ARRAY,
    BPF_MAP_TYPE_PROG_ARRAY,
    BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    BPF_MAP_TYPE_PERCPU_HASH,
    BPF_MAP_TYPE_PERCPU_ARRAY,
    BPF_MAP_TYPE_STACK_TRACE,
    BPF_MAP_TYPE_CGROUP_ARRAY,
    BPF_MAP_TYPE_LRU_HASH,
    BPF_MAP_TYPE_LRU_PERCPU_HASH,
    BPF_MAP_TYPE_LPM_TRIE,
    BPF_MAP_TYPE_ARRAY_OF_MAPS,
    BPF_MAP_TYPE_HASH_OF_MAPS,
    BPF_MAP_TYPE_DEVMAP,
    BPF_MAP_TYPE_SOCKMAP,
    BPF_MAP_TYPE_CPUMAP,
    BPF_MAP_TYPE_XSKMAP,
    BPF_MAP_TYPE_SOCKHASH,
    BPF_MAP_TYPE_CGROUP_STORAGE,
    BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
    BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
    BPF_MAP_TYPE_QUEUE,
    BPF_MAP_TYPE_STACK,
    BPF_MAP_TYPE_SK_STORAGE,
    BPF_MAP_TYPE_DEVMAP_HASH,
    BPF_MAP_TYPE_STRUCT_OPS,
    BPF_MAP_TYPE_RINGBUF,
    BPF_MAP_TYPE_INODE_STORAGE,
    BPF_MAP_TYPE_TASK_STORAGE,
    BPF_MAP_TYPE_BLOOM_FILTER,
};
</code></pre>
<p>这里数据结构类型众多，但不是本文的重点，我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构，这个类型可以理解为我们日常接触的hash映射表，通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。</p>
<p>那么BPF MAP是如何可以在内核态与用户态共享数据的？原理是什么呢？</p>
<p>从<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf这个系统调用的说明</a>中，我们能找到端倪。下面是bpf系统调用的函数原型：</p>
<pre><code>// https://man7.org/linux/man-pages/man2/bpf.2.html

#include &lt;linux/bpf.h&gt;

int bpf(int cmd, union bpf_attr *attr, unsigned int size);
</code></pre>
<p>从bpf的原型来看，似乎比较简单。但bpf其实是一个“富调用”，即不止能干一件事，通过cmd传入的值不同，它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD)，其次是围绕MAP的一系列操作，包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。</p>
<p>当cmd=BPF_MAP_CREATE时，即bpf执行创建MAP的操作后，bpf调用会返回一个文件描述符fd，<strong>通过该fd后续可以操作新创建的MAP</strong>。通过fd访问map，这个<strong>很unix</strong>！</p>
<p>当然这么底层的系统调用，一般BPF用户态开发人员无需接触到，像libbpf就包装了一系列的map操作函数，这些函数不会暴露map fd给用户，简化了使用方法，提升了使用体验。</p>
<p>下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。</p>
<h3>三. 使用C基于libbpf使用map的示例</h3>
<p>这个示例改造自<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld">helloworld示例</a>。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到)，用户态程序并没有与内核态程序做任何数据交换。</p>
<p>在这个新示例(execve_counter)中，我们依然跟踪系统调用execve，不同的是我们对execve进行调用计数，并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。</p>
<p>我们先来看看BPF内核态部分的源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c

#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

typedef __u64 u64;
typedef char stringkey[64];

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    //__type(key, stringkey);
    stringkey* key;
    __type(value, u64);
} execve_counter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  stringkey key = "execve_counter";
  u64 *v = NULL;
  v = bpf_map_lookup_elem(&amp;execve_counter, &amp;key);
  if (v != NULL) {
    *v += 1;
  }
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>和helloworld示例不同，我们在新示例中定义了一个map结构execve_counter，通过SEC宏将其标记为BPF MAP变量。</p>
<p>这个map结构有四个字段：</p>
<ul>
<li>type: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型)，这里我们使用BPF_MAP_TYPE_HASH，即一个hash散列表结构；</li>
<li>max_entries：map内的key-value对的最大数量；</li>
<li>key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型；</li>
<li>value: 指向value内存空间的指针，这里value元素的类型为u64，一个64位整型。</li>
</ul>
<p>内核态函数bpf_prog的实现也比较简单：在上面的map中查询”execve_counter”这个key，如果查到了，则将得到的value指针指向的内存中的值加1。</p>
<p>我们再来看看execve_counter这个示例的用户态部分的程序源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c

#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include &lt;linux/bpf.h&gt;
#include "execve_counter.skel.h"

typedef __u64 u64;
typedef char stringkey[64];

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct execve_counter_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = execve_counter_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* Load &amp; verify BPF programs */
    err = execve_counter_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* init the counter */
    stringkey key = "execve_counter";
    u64 v = 0;
    err = bpf_map__update_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
    if (err != 0) {
        fprintf(stderr, "Failed to init the counter, %d\n", err);
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = execve_counter_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    for (;;) {
            // read counter value from map
            err = bpf_map__lookup_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
            if (err != 0) {
               fprintf(stderr, "Lookup key from map error: %d\n", err);
               goto cleanup;
            } else {
               printf("execve_counter is %llu\n", v);
            }

            sleep(5);
    }

cleanup:
    execve_counter_bpf__destroy(skel);
    return -err;
}
</code></pre>
<p>map是在execve_counter_bpf__load中完成的创建，跟踪代码你会发现(参考libbpf源码)，最终会调用bpf系统调用创建map。</p>
<p>和helloworld示例不同的是，我们在attach handler之前，先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0，如果没有这一步，第一次bpf程序执行时，会提示找不到key)。</p>
<p>然后attach handler后，我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。</p>
<p>用户态程序之所以可以直接使用map，是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。</p>
<p>接下来我们执行make编译一下这个ebpf程序，然后执行并观察输出：</p>
<pre><code>$sudo ./execve_counter
libbpf: loading object 'execve_counter_bpf' from buffer
libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9
libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1
libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1
libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1
libbpf: license of execve_counter_bpf is Dual BSD/GPL
libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1
libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1
libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2
libbpf: looking for externs among 31 symbols...
libbpf: collected 0 externs total
libbpf: map 'execve_counter': at sec_idx 6, offset 0.
libbpf: map 'execve_counter': found type = 1.
libbpf: map 'execve_counter': found key [9], sz = 64.
libbpf: map 'execve_counter': found value [13], sz = 8.
libbpf: map 'execve_counter': found max_entries = 128.
libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480.
libbpf: map 1 is "execve_c.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter'
libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15
libbpf: map 'execve_counter': created successfully, fd=4
libbpf: map 'execve_c.rodata': created successfully, fd=5
execve_counter is 0
execve_counter is 0
execve_counter is 9
execve_counter is 23
... ...
</code></pre>
<blockquote>
<p>注：如果不知道如何编译execve_counter这个示例，请先移步<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>了解其构建原理。</p>
</blockquote>
<p>bpftool工具提供了查看map的特性，我们可以通过它查看示例创建的map：</p>
<pre><code>$sudo bpftool map
114: hash  name execve_counter  flags 0x0
    key 64B  value 8B  max_entries 128  memlock 20480B
    btf_id 120
116: array  name execve_c.rodata  flags 0x80
    key 4B  value 64B  max_entries 1  memlock 4096B
    frozen
</code></pre>
<p>我们还可以dump一下整个map：</p>
<pre><code>$sudo bpftool map dump id 114
[{
        "key": "execve_counter",
        "value": 23
    }
]
</code></pre>
<p>我们看到，整个map中就一个键值对(key=”execve_counter”)，其值与示例的用户态部分程序输出的一致。</p>
<p>好了，有了C示例作为基础，我们再来看看如何基于Go来实现这个示例。</p>
<h3>四. 使用Go基于cilium/ebpf实现execve-counter示例</h3>
<p>使用Go开发BPF用户态部分程序要容易的多，cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路，请先移步<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">《使用Go语言开发eBPF程序》</a>一文了解一下。</p>
<p>Go语言示例的必不可少的原料是execve_counter.bpf.c，这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h：</p>
<pre><code>$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c
1,2c1,2
&lt;
&lt; #include "common.h"
---
&gt; #include &lt;linux/bpf.h&gt;
&gt; #include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>基于原料execve_counter.bpf.c，bpf2go工具会生成用户态部分所需的Go源码，比如：bpfObject中包含的bpf map实例：</p>
<pre><code>// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
    ExecveCounter *ebpf.Map `ebpf:"execve_counter"`
}
</code></pre>
<p>最后，我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可，下面是main.go部分源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    // init the map element
    var key [64]byte
    copy(key[:], []byte("execve_counter"))
    var val int64 = 0
    if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil {
        log.Fatalf("init map key error: %s", err)
    }

    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case &lt;-ticker.C:
            if err := objs.bpfMaps.ExecveCounter.Lookup(key, &amp;val); err != nil {
                log.Fatalf("reading map error: %s", err)
            }
            log.Printf("execve_counter: %d\n", val)

        case &lt;-stopper:
            // Wait for a signal and close the perf reader,
            // which will interrupt rd.Read() and make the program exit.
            log.Println("Received signal, exiting program..")
            return
        }
    }
}
</code></pre>
<p>在main函数，我们通过objs.bpfMaps.ExecveCounter直接访问map实例，并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致，不能直接用string类型，否则会在执行时报下面错误：</p>
<pre><code>init map key error: can't marshal key: string doesn't marshal to 64 bytes
</code></pre>
<p>编译和执行execve-counter-go和helloworld-go别无二致：</p>
<pre><code>$make
$go run -exec sudo main.go bpf_bpfel.go

2022/07/17 16:59:52 execve_counter: 0
2022/07/17 16:59:57 execve_counter: 14
^C2022/07/17 16:59:59 Received signal, exiting program..
</code></pre>
<h3>五. 小结</h3>
<p>本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法：BPF MAP机制。这里的MAP不是狭义的一种hash散列表，而是一个抽象数据结构容器，目前支持二十几种数据结构，大家可以根据自己的需求挑选适当的结构（可查询手册了解各种数据结构的特点)。</p>
<p>MAP本质上也是由bpf系统调用创建的，bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map，libbpf和cilium/ebpf等封装了对fd的操作，这样简化了API的使用。</p>
<p>内核中map的update操作不是原子的，因此当有多个bpf程序并发访问一个map时，需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改，就像下面的例子(例子来自<a href="https://book.douban.com/subject/33398015/">《Linux Observability with BPF》</a>一书)：</p>
<pre><code>struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct bpf_map_def SEC("maps") concurrent_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(struct concurrent_element),
    .max_entries = 100,
};

int bpf_program(struct pt_regs *ctx) {
      intkey=0;
      struct concurrent_element init_value = {};
      struct concurrent_element *read_value;
      bpf_map_create_elem(&amp;concurrent_map, &amp;key, &amp;init_value, BPF_NOEXIST);
      read_value = bpf_map_lookup_elem(&amp;concurrent_map, &amp;key);
      bpf_spin_lock(&amp;read_value-&gt;semaphore);
      read_value-&gt;count += 100;
      bpf_spin_unlock(&amp;read_value-&gt;semaphore);
}
</code></pre>
<p>本文涉及代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples">这里</a>下载。</p>
<h3>六. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/map_internal/">《揭秘BPF map前生今世》</a> &#8211; https://www.ebpf.top/post/map_internal/</li>
<li><a href="https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg">《边缘网络eBPF超能力：eBPF map原理与性能解析》</a> &#8211; https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg</li>
<li><a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf系统调用说明</a> &#8211; https://man7.org/linux/man-pages/man2/bpf.2.html</li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">官方bpf map参考手册</a> &#8211; https://www.kernel.org/doc/html/latest/bpf/maps.html</li>
<li><a href="https://www.mankier.com/8/bpftool">bpftool参考手册</a> &#8211; https://www.mankier.com/8/bpftool</li>
<li><a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">《Building BPF applications with libbpf-bootstrap》</a> &#8211; https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>使用Go语言开发eBPF程序</title>
		<link>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/</link>
		<comments>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/#comments</comments>
		<pubDate>Tue, 19 Jul 2022 13:11:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3625</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go 在前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。 在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的cilium/ebpf，cilium项目背后的Isovalent公司也是eBPF技术在云原生领域应用的主要推手之一。 本文我们就来说说基于cilium/ebpf开发eBPF程序的套路！ 一. 探索cilium/ebpf项目示例 cilium/ebpf项目借鉴了libbpf-boostrap的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。 我们首先来下载和看看ebpf的示例的结构。 下载cilium/ebpf项目 $ git clone https://github.com/cilium/ebpf.git Cloning into 'ebpf'... remote: Enumerating objects: 7054, done. remote: Counting objects: 100% (183/183), done. remote: Compressing objects: 100% (112/112), done. remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871 Receiving objects: 100% (7054/7054), 10.91 MiB &#124; [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go</p>
<p>在前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。</p>
<p>在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的<a href="https://github.com/cilium/ebpf/">cilium/ebpf</a>，cilium项目背后的<a href="https://isovalent.com/">Isovalent公司</a>也是eBPF技术在云原生领域应用的主要推手之一。</p>
<p>本文我们就来说说<strong>基于cilium/ebpf开发eBPF程序的套路</strong>！</p>
<h3>一. 探索cilium/ebpf项目示例</h3>
<p>cilium/ebpf项目借鉴了<a href="https://github.com/libbpf/libbpf-bootstrap">libbpf-boostrap</a>的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。</p>
<p>我们首先来下载和看看ebpf的示例的结构。</p>
<ul>
<li>下载cilium/ebpf项目</li>
</ul>
<pre><code>$ git clone https://github.com/cilium/ebpf.git
Cloning into 'ebpf'...
remote: Enumerating objects: 7054, done.
remote: Counting objects: 100% (183/183), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871
Receiving objects: 100% (7054/7054), 10.91 MiB | 265.00 KiB/s, done.
Resolving deltas: 100% (4871/4871), done.
</code></pre>
<ul>
<li>探索ebpf项目示例代码结构</li>
</ul>
<p>ebpf示例在examples目录下，我们以tracepoint_in_c为例看看其组织形式：</p>
<pre><code>$tree tracepoint_in_c
tracepoint_in_c
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── main.go
└── tracepoint.c

0 directories, 6 files
</code></pre>
<p>根据经验判断，这里面的tracepoint.c对应的是ebpf程序内核态部分，而main.go和bpf_bpfel.go/bpf_bpfeb.go则是ebpf程序用户态部分，至于bpf_bpfeb.o/bpf_bpfel.o应该是某种中间目标文件。通过readelf -a bpf_bpfeb.o查看该中间文件：</p>
<pre><code>$readelf -a bpf_bpfeb.o
ELF Header:
  Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1968 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 1
... ...

</code></pre>
<p>我们看到这是一个内含linux bpf字节码的elf文件(Machine: Linux BPF)。</p>
<p>阅读了cilium/ebpf的相关文档，我搞明白了这几个文件的关系，用下面示意图呈现给大家：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-2.png" alt="" /></p>
<p>ebpf程序的源码文件(比如图中tracepoint.c)经过bpf2go(cilium/ebpf提供的一个代码生成工具)被编译(bpf2go调用clang)为ebpf字节码文件bpf_bpfeb.o(大端)和bpf_bpfel.o(小端)，然后bpf2go会基于ebpf字节码文件生成bpf_bpfeb.go或bpf_bpfel.go，ebpf程序的字节码会以二进制数据的形式内嵌到这两个go源文件中，以bpf_bpfel.go为例，我们可以在其代码中找到下面内容(利用<a href="https://tonybai.com/2021/02/25/some-changes-in-go-1-16">go:embed特性</a>)：</p>
<pre><code>//go:embed bpf_bpfel.o
var _BpfBytes []byte
</code></pre>
<p>main.go则是ebpf程序用户态部分的主程序，将main.go与bpf_bpfeb.go或bpf_bpfel.go之一一起编译就形成了ebpf程序。</p>
<p>有了对cilium/ebpf项目示例的初步探索后，我们来构建ebpf示例代码。</p>
<h3>二. 构建ebpf示例代码</h3>
<p>cilium/ebpf提供了便利的构建脚本，我们只需在ebpf/examples下面执行”make -C ..”即可进行示例代码的构建。</p>
<p>make构建过程会基于quay.io/cilium/ebpf-builder镜像启动构建容器，不过在国内的童鞋需要像下面一样对Makefile内容做一丁点修改，增加GOPROXY环境变量，否则wall外的go module无法拉取：</p>
<pre><code>$git diff ../Makefile
diff --git a/Makefile b/Makefile
index 3a1da88..d7b1712 100644
--- a/Makefile
+++ b/Makefile
@@ -48,6 +48,7 @@ container-all:
        ${CONTAINER_ENGINE} run --rm ${CONTAINER_RUN_ARGS} \
                -v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \
                --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
+               --env GOPROXY="https://goproxy.io" \
                --env HOME="/tmp" \
                "${IMAGE}:${VERSION}" \
                $(MAKE) all

</code></pre>
<p>这之后再执行构建就会顺利得到我们所要的结果：</p>
<pre><code>$ cd examples
$ make -C ..
make: Entering directory '/root/go/src/github.com/cilium/ebpf'
docker run --rm  --user "0:0" \
    -v "/root/go/src/github.com/cilium/ebpf":/ebpf -w /ebpf --env MAKEFLAGS \
    --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
    --env GOPROXY="https://goproxy.io" \
    --env HOME="/tmp" \
    "quay.io/cilium/ebpf-builder:1648566014" \
    make all
make: Entering directory '/ebpf'
find . -type f -name "*.c" | xargs clang-format -i
go generate ./cmd/bpf2go/test
go: downloading golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34
Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go
Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfeb.go
go generate ./internal/sys
enum AdjRoomMode
enum AttachType
enum Cmd
enum FunctionId
enum HdrStartOff
enum LinkType
enum MapType
enum ProgType
enum RetCode
enum SkAction
enum StackBuildIdStatus
enum StatsType
enum XdpAction
struct BtfInfo
... ...
attr ProgRun
attr RawTracepointOpen
cd examples/ &amp;&amp; go generate ./...
go: downloading github.com/cilium/ebpf v0.8.2-0.20220424153111-6da9518107a8
go: downloading golang.org/x/sys v0.0.0-20211001092434-39dca1131b70
Compiled /ebpf/examples/cgroup_skb/bpf_bpfel.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfel.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfel.go
Compiled /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfeb.o
Stripped /ebpf/examples/fentry/bpf_bpfeb.o
Wrote /ebpf/examples/fentry/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfel.o
Stripped /ebpf/examples/fentry/bpf_bpfel.o
Wrote /ebpf/examples/fentry/bpf_bpfel.go
Compiled /ebpf/examples/kprobe/bpf_bpfel.o
Stripped /ebpf/examples/kprobe/bpf_bpfel.o
Wrote /ebpf/examples/kprobe/bpf_bpfel.go
Stripped /ebpf/examples/uretprobe/bpf_bpfel_x86.o
... ...
Wrote /ebpf/examples/uretprobe/bpf_bpfel_x86.go
ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf
ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf
make: Leaving directory '/ebpf'
make: Leaving directory '/root/go/src/github.com/cilium/ebpf'
</code></pre>
<p>以uretprobe下面的ebpf为例，我们运行一下：</p>
<pre><code>$go run -exec sudo uretprobe/*.go
2022/06/05 18:23:23 Listening for events..
</code></pre>
<p>打开一个新的terminal，然后在用户home目录下执行vi .bashrc。在上面的uretprobe程序的执行窗口我们能看到：</p>
<pre><code>2022/06/05 18:24:34 Listening for events..
2022/06/05 18:24:42 /bin/bash:readline return value: vi .bashrc
</code></pre>
<p>这就表明uretprobe下面的ebpf程序如预期地执行了。</p>
<h3>三. 使用cilium/ebpf为前文的Hello World eBPF程序开发用户态部分</h3>
<p>有了对cilium/ebpf示例程序的初步了解，下面我们就来为前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的那个helloworld ebpf程序开发用户态部分。</p>
<p>回顾一下那个hello world ebpf程序的C源码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go/helloworld.bpf.c
#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>当这个ebpf程序被加载到内核中后，每当execve这个系统调用被执行，该ebpf程序都会被调用一次，我们就会在/sys/kernel/debug/tracing/trace_pipe中看到对应的日志输出。</p>
<h4>1. 使用bpf2go将ebpf核心态程序转换为Go代码</h4>
<p>根据我们在前面探索cilium/ebpf示例程序时所得到的“套路”，我们接下来第一个要做的就是将helloworld.bpf.c转换为Go代码文件，这一转换过程不可缺少的工具就是cilium/ebpf提供的bpf2go工具，我们先来安装一下该工具：</p>
<pre><code>$go install github.com/cilium/ebpf/cmd/bpf2go@latest
</code></pre>
<p>接下来，我们可以直接使用bpf2go工具将helloworld.ebpf.c转换为对应的go源文件：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/test/ebpf/libbpf/include/uapi -I /usr/local/bpf/include -idirafter /usr/local/include -idirafter /usr/lib/llvm-10/lib/clang/10.0.0/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>不过这里有一个问题，那就是bpf2go命令行后面的一系列提供给clang编译器的头文件引用路径参考了<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的Makefile。如果按照这些头文件路径来引用，虽然bpf2go转换可以成功，但是我们需要依赖并安装libbpf这个库，这显然不是我们想要的。</p>
<p>cilium/ebpf在examples中提供了一个headers目录，这个目录中包含了开发ebpf程序用户态部分所需的所有头文件，我们使用它作为我们的头文件引用路径。不过要想基于这个headers目录构建ebpf，我们需要将helloworld.bpf.c中的原头文件include语句由：</p>
<pre><code>#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>改为：</p>
<pre><code>#include "common.h"
</code></pre>
<p>接下来我们再来执行bpf2go工具进行转换：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/go/src/github.com/cilium/ebpf/examples/headers

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>我们看到bpf2go顺利生成ebpf字节码与对应的Go源文件。</p>
<h4>2. 构建helloworld ebpf程序用户态部分</h4>
<p>下面是参考cilium/ebpf示例而构建的helloword ebpf程序用户态部分的main.go源码：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go
package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    //SEC("tracepoint/syscalls/sys_enter_execve")
    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    log.Printf("Successfully started! Please run \"sudo cat /sys/kernel/debug/tracing/trace_pipe\" to see output of the BPF programs\n")

    // Wait for a signal and close the perf reader,
    // which will interrupt rd.Read() and make the program exit.
    &lt;-stopper
    log.Println("Received signal, exiting program..")
}
</code></pre>
<p>我们知道一个ebpf程序有几个关键组成：</p>
<ul>
<li>ebpf程序数据</li>
<li>map：用于用户态与内核态的数据交互</li>
<li>挂接点(attach point)</li>
</ul>
<p>根据<a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium/ebpf架构</a>的说明，ebpf包将前两部分抽象为了一个数据结构bpfObjects：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go

// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
    bpfPrograms
    bpfMaps
}
</code></pre>
<p>我们看到，main函数通过生成的loadBpfObjects函数将ebpf程序加载到内核，并填充bpfObjects结构，一旦加载bpf程序成功，后续我们便可以使用bpfObjects结构中的字段来完成其余操作，比如通过link包的函数将bpf程序与目标挂节点对接在一起(如文中的link.Tracepoint函数），这样挂接后，bpf才能在对应的事件发生后被回调执行。</p>
<p>下面编译执行一下该helloworld示例：</p>
<pre><code>$go run -exec sudo main.go bpf_bpfel.go
[sudo] password for tonybai:
2022/06/05 14:12:40 Successfully started! Please run "sudo cat /sys/kernel/debug/tracing/trace_pipe" to see output of the BPF programs
</code></pre>
<p>之后新打开一个窗口，执行sudo cat /sys/kernel/debug/tracing/trace_pipe，当execve被调用时，我们就能看到类似下面的日志输出：</p>
<pre><code>&lt;...&gt;-551077  [000] .... 6062226.208943: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551077  [000] .... 6062226.209098: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215421: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215578: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785212: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785378: 0: invoke bpf_prog: Hello, World!
</code></pre>
<h4>3. 使用go generate来驱动bpf2go的转换</h4>
<p>在生成代码方面，Go工具链原生提供了go generate工具，cilium/ebpf的examples中也是利用go generate来驱动bpf2go将bpf程序转换为Go源文件的，这里我们也来做一下改造。</p>
<p>首先我们在main.go的main函数上面增加一行go:generate指示语句：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf helloworld.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal,  1)
    ... ...
}
</code></pre>
<p>这样当我们显式执行go generate语句时，go generate会扫描到该指示语句，并执行后面的命令。这里使用了几个变量，变量是定义在Makefile中的。当然如果你不想使用Makefile，也可以将变量替换为相应的值。这里我们使用Makefile，下面是Makefile的内容：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/Makefile

CLANG ?= clang-10
CFLAGS ?= -O2 -g -Wall -Werror

LIBEBPF_TOP = /home/tonybai/go/src/github.com/cilium/ebpf
EXAMPLES_HEADERS = $(LIBEBPF_TOP)/examples/headers

all: generate

generate: export BPF_CLANG=$(CLANG)
generate: export BPF_CFLAGS=$(CFLAGS)
generate: export BPF_HEADERS=$(EXAMPLES_HEADERS)
generate:
    go generate ./...
</code></pre>
<p>有了该Makefile后，我们执行make命令便可以执行bpf2go对bpf程序的转换：</p>
<pre><code>$make
go generate ./...
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<h3>四. 小结</h3>
<p>本文我们讲解了如何基于cilium/ebpf包来开发ebpf的用户态部分。</p>
<p>ebpf借鉴了libbpf的思路，通过生成代码与数据内嵌的方式来构建ebpf的用户态部分。</p>
<p>ebpf提供了bpf2go工具，可以将bpf的C源码转换为相应的go源码。</p>
<p>ebpf将bpf程序抽象为bpfObjects，通过生成的loadBpfObjects完成bpf程序加载到内核的过程，然后利用ebpf库提供的诸如link之类的包实现ebpf与内核事件的关联。</p>
<p>ebpf包的玩法还有很多，这一篇仅仅是为了打好基础，在后续文章中，我们还会针对各种类型的bpf程序做进一步学习和说明。</p>
<p>本文代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go">这里</a>下载。</p>
<h3>无. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/ebpf_go/">使用Go语言管理和分发ebpf程序</a> &#8211; https://www.ebpf.top/post/ebpf_go/</li>
<li><a href="https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf">A Pure Go eBPF library</a> &#8211; https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf</li>
<li><a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium ebpf library architecture</a> &#8211; https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用C语言从头开发一个Hello World级别的eBPF程序</title>
		<link>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/</link>
		<comments>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/#comments</comments>
		<pubDate>Mon, 04 Jul 2022 21:42:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3601</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch 近两年最火的Linux内核技术非eBPF莫属！ 2019年以来，除了eBPF技术自身快速演进之外，基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括：cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。 今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。 eBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。 我们首先看一下这么火热的eBPF技术究竟是什么？ 一. eBPF简介 eBPF这门技术，我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。 eBPF技术的前身是BPF(Berkeley Packet Filter)，BPF始于1992年末的一篇名为“The BSD PacketFilter：A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案，这种新的技术比当时最先进的数据包过滤技术快20倍。 1997年，BPF技术合入linux kernel，后在tcpdump中得以应用。 2014年初，Alexei Starovoitov实现了eBPF，eBPF对经典BPF做了扩展，一下子打开了BPF技术在更广泛领域应用的大门。 图片来自ebpf官网 从上图中我们看到：eBPF程序运行在内核态(kernel)，无需你重新编译内核，也不需要编译内核模块并挂载，eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核，eBPF便拥有了上帝视角，既可以监控内核，也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全，避免恶意程序进入到内核态中执行。 从本质上说，BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)！通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。 如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用，eBPF技术被赋予的最新定义是：a New Generation of Networking, Security, and Observability Tools，即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司，一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。 eBPF已经成为内核顶级的子系统，后续如未特指，我们所提到的BPF指的就是新一代的eBPF技术。 BPF技术这么牛逼，那我们如何开发BPF程序呢？ 二. 如何开发BPF程序 1. BPF程序的形态 一个以开发BPF程序为目的的工程通常由两类源文件组成，一类是运行于内核态的BPF程序的源代码文件(比如：下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。 目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件，如下图bpf_program.bpf.c)，更准确地说只能用受限制的C语法进行开发，并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端，采用LLVM作为后端)。 下面是BPF程序的编译与加载到内核过程的示意图： BPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件，我们可以通过readelf命令行工具可以读取BPF目标文件的内容，下面是一个示例： $readelf -a bpf_program.o [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">本文永久链接</a> &#8211; https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch</p>
<hr />
<p>近两年最火的Linux内核技术非<a href="https://ebpf.io">eBPF</a>莫属！</p>
<p>2019年以来，除了eBPF技术自身快速演进之外，<a href="https://ebpf.io/projects">基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目</a>如雨后春笋般出现。耳熟能详的的包括：<a href="https://cilium.io">cilium</a>(把eBPF技术带到Kubernetes世界)、<a href="https://falco.org">Falco</a>(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、<a href="https://github.com/facebookincubator/katran">Katran</a>(高性能四层负载均衡器)、<a href="https://px.dev">pixie</a>(用于Kubernetes应用程序的可观察性工具)等。</p>
<p>今年3月份发布的<a href="https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2022/03/tr_technology_radar_vol_26_cn.pdf">thoughtworks技术雷达第26期</a>也将eBPF技术放入<strong>试验</strong>的象限阶段。</p>
<p>eBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。</p>
<p>我们首先看一下这么火热的eBPF技术究竟是什么？</p>
<h3>一. eBPF简介</h3>
<p>eBPF这门技术，我也是在几年前从性能专家、火焰图的发明者<a href="https://www.brendangregg.com">Brendan Gregg</a>的blog和书中看到的。</p>
<p>eBPF技术的前身是BPF(Berkeley Packet Filter)，BPF始于1992年末的一篇名为<a href="https://www.tcpdump.org/papers/bpf-usenix93.pdf">“The BSD PacketFilter：A New Architecture for User-Level Packet Capture”</a>的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案，这种新的技术比当时最先进的数据包过滤技术快20倍。</p>
<p>1997年，BPF技术合入linux kernel，后在tcpdump中得以应用。</p>
<p>2014年初，Alexei Starovoitov实现了eBPF，<a href="https://lwn.net/Articles/740157/">eBPF对经典BPF做了扩展</a>，一下子打开了BPF技术在更广泛领域应用的大门。</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-2.png" alt="" /><br />
<center>图片来自ebpf官网</center></p>
<p>从上图中我们看到：eBPF程序运行在内核态(kernel)，无需你重新编译内核，也不需要编译内核模块并挂载，eBPF可以动态注入到内核中运行并随时卸载。<strong>一旦进入内核，eBPF便拥有了上帝视角</strong>，既可以监控内核，也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全，避免恶意程序进入到内核态中执行。</p>
<p>从本质上说，BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)！通过注入eBPF程序并注册要关注事件、事件触发(内核<strong>回调</strong>你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。</p>
<p>如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用，eBPF技术被赋予的最新定义是：a New Generation of Networking, Security, and Observability Tools，即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司，一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。</p>
<p>eBPF已经成为内核顶级的子系统，后续如未特指，<strong>我们所提到的BPF指的就是新一代的eBPF技术</strong>。</p>
<p>BPF技术这么牛逼，那我们如何开发BPF程序呢？</p>
<h3>二. 如何开发BPF程序</h3>
<h4>1. BPF程序的形态</h4>
<p>一个以开发BPF程序为目的的工程通常<strong>由两类源文件组成</strong>，一类是运行于内核态的BPF程序的源代码文件(比如：下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。</p>
<p>目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件，如下图bpf_program.bpf.c)，更准确地说只能用<strong>受限制的C语法</strong>进行开发，并且可以完善地将C源码编译成BPF目标文件的只有<a href="https://clang.llvm.org">clang编译器</a>(clang是一个C、C++、Objective-C等编程语言的编译器前端，采用LLVM作为后端)。</p>
<p>下面是BPF程序的编译与加载到内核过程的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-3.png" alt="" /></p>
<p>BPF目标文件(bpf_program.o)实质上也是一个<a href="http://en.wikipedia.org/wiki/Executable_and_Linkable_Format"><strong>ELF格式</strong></a>的文件，我们可以通过readelf命令行工具可以读取BPF目标文件的内容，下面是一个示例：</p>
<pre><code>$readelf -a bpf_program.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  0000012a
       0000000000000079  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] tracepoint/syscal PROGBITS         0000000000000000  00000040
       0000000000000070  0000000000000000  AX       0     0     8
  [ 4] .rodata.str1.1    PROGBITS         0000000000000000  000000b0
       0000000000000012  0000000000000001 AMS       0     0     1
  [ 5] license           PROGBITS         0000000000000000  000000c2
       0000000000000004  0000000000000000  WA       0     0     1
  [ 6] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00000128
       0000000000000002  0000000000000000   E       7     0     1
  [ 7] .symtab           SYMTAB           0000000000000000  000000c8
       0000000000000060  0000000000000018           1     2     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bpf_program.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    5 _license
     3: 0000000000000000   112 FUNC    GLOBAL DEFAULT    3 bpf_prog
</code></pre>
<p>在上面readelf输出的符号表(Symbol table)中，我们看到一个Type为FUNC的符号bpf_prog，这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3，然后在前面的Section Header中可以找到序号为3的section条目：tracepoint/syscal&#8230;，它们是对应的。</p>
<p>从readelf输出可以看到：bpf_prog(即序号为3的section)的Size为112，但是它的内容是什么呢？这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开：</p>
<pre><code>$llvm-objdump-10 -d bpf_program.o

bpf_program.o:  file format ELF64-BPF

Disassembly of section tracepoint/syscalls/sys_enter_execve:

0000000000000000 bpf_prog:
       0:   b7 01 00 00 21 00 00 00 r1 = 33
       1:   6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1
       2:   18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll
       4:   7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
       5:   18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll
       7:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
       8:   bf a1 00 00 00 00 00 00 r1 = r10
       9:   07 01 00 00 e8 ff ff ff r1 += -24
      10:   b7 02 00 00 12 00 00 00 r2 = 18
      11:   85 00 00 00 06 00 00 00 call 6
      12:   b7 00 00 00 00 00 00 00 r0 = 0
      13:   95 00 00 00 00 00 00 00 exit
</code></pre>
<p>llvm-objdump输出的bpf_prog的内容其实就是<strong>BPF的字节码</strong>。谈到字节码(byte code)，我们首先想到的就是jvm虚拟机。没错，BPF程序不是以机器指令加载到内核的，而是以字节码形式加载到内核中的，很显然这是为了安全，增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中，BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。</p>
<p>用于加载和卸载BPF程序的用户态程序则可以由多种语言开发，既可以用C语言，也可以用Python、Go、<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Rust</a>等。</p>
<h4>2. BPF程序的开发方式</h4>
<p>BPF演进了这么多年，虽然一直在努力提高，但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像<a href="https://github.com/iovisor/bcc">BPF Compiler Collection(BCC)</a>这样的用于简化BPF开发的框架和库集合，以及像<a href="https://github.com/iovisor/bpftrace">bpftrace</a>这样的提供高级BPF开发语言的项目(可以理解是开发BPF的<a href="https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1">DSL语言</a>)。</p>
<p>很多时候我们无需自己开发BPF程序，像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发，基于bcc和bpftrace开发的门槛其实也不低，你需要理解bcc框架的结构，你需要学习bpftrace提供的脚本语言，这无形中也增加了自行开发BPF的负担。</p>
<p>随着BPF应用得更为广泛，BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢？Linux内核在快速演进，内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除，可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序，可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序，就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。</p>
<p>最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译，以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦：目标机器上需要安装BPF依赖的各种开发包、使用的编译器，编译过程也会很耗时，这让BPF程序的测试与分发过程十分痛苦，尤其当你使用bcc和bpftrace来开发BPF程序时。</p>
<p>为了解决BPF可移植性问题，内核引入<a href="https://nakryiko.com/posts/btf-dedup/">BTF(BPF Type Format)</a>和<a href="https://nakryiko.com/posts/bpf-portability-and-co-re/">CO-RE(Compile Once &#8211; Run Everywhere)</a>两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的，避免了LLVM重新编译的需要。</p>
<p>使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作，无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。</p>
<blockquote>
<p>注：BTF和Co-RE技术的原理不是本文重点，这里不赘述，大家可以自行查询资料。</p>
</blockquote>
<p>当然这些新技术对于BPF程序自身是透明的，Linux内核源码提供的libbpf用户API将上述新技术都封装了起来，只要用户态加载程序基于libbpf开发，那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段，这让<a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">libbpf成为开发BPF加载程序的首选</a>。</p>
<h4>3. 基于libbpf的BPF程序的开发方式</h4>
<p>内核BPF开发者<a href="https://nakryiko.com/">Andrii Nakryiko</a>在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目<a href="https://github.com/libbpf/libbpf-bootstrap">libbpf-bootstrap</a>。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。</p>
<p>我们以一个hello world级的BPF程序及其用户态加载器为例，看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”，下面是一张示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-4.png" alt="" /></p>
<p>这里对上面的示意图做一下简单说明：</p>
<ul>
<li>我们一直说libbpf，libbpf究竟是什么？其实libbpf是指linux内核代码库中的tools/lib/bpf，这是内核提供给外部开发者的C库，用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库，特地在github.com上为libbpf建立了镜像仓库：https://github.com/libbpf/libbpf，这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/libbpf中的源码路径)：</li>
</ul>
<pre><code>// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [tools/lib/bpf]=src                         \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h     \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h   \
    [Documentation/bpf/libbpf]=docs                     \
)
</code></pre>
<ul>
<li>图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool，也是在github上创建的对应的镜像库，这是一个bpf辅助工具程序，在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/bpftool中的源码路径)</li>
</ul>
<pre><code>// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [${BPFTOOL_SRC_DIR}]=src                        \
    [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion            \
    [${BPFTOOL_SRC_DIR}/Documentation]=docs                 \
    [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c               \
    [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h               \
    [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h   \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h   \
)
</code></pre>
<ul>
<li>helloworld.bpf.c是bpf程序对应的源码，通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o，而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件，在生成的helloworld.skel.h文件中包含了<strong>BPF程序的字节码</strong>以及加载、卸载对应BPF程序的函数，我们在用户态程序直接调用即可。</li>
<li>helloworld.c是BPF用户态程序，它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中，我们在分发BPF程序时只需分发用户态程序即可！</li>
</ul>
<p>以上，我们简单了解了基于libbpf-bootstrap的开发思路，下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。</p>
<h3>三. 基于libbpf-bootstrap开发hello world级eBPF程序示例</h3>
<blockquote>
<p>注：我的实验环境为ubuntu 20.04(内核版本：5.4.0-109-generic)。</p>
</blockquote>
<h4>1. 安装依赖</h4>
<p>在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang，建议安装clang 10及以上版本，这里以安装 clang-10为例：</p>
<pre><code>$apt-get install clang-10
$clang-10 --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
</code></pre>
<h4>2. 下载libbpf-bootstrap</h4>
<p>libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架，我们需要将其下载到本地：</p>
<pre><code>git clone https://github.com/libbpf/libbpf-bootstrap.git
Cloning into 'libbpf-bootstrap'...
remote: Enumerating objects: 387, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368
Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done.
Resolving deltas: 100% (173/173), done.
</code></pre>
<h4>3. 初始化和更新libbpf-bootstrap的依赖</h4>
<p>libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中：</p>
<pre><code>$cat .gitmodules
[submodule "libbpf"]
    path = libbpf
    url = https://github.com/libbpf/libbpf.git
[submodule "bpftool"]
    path = bpftool
    url = https://github.com/libbpf/bpftool
[submodule "blazesym"]
    path = blazesym
    url = https://github.com/ThinkerYzu1/blazesym.git
</code></pre>
<blockquote>
<p>注：blazesys是rust相关的一个项目，这里不表。</p>
</blockquote>
<p>因此，我们在应用libbpf-bootstrap项目开发BPF程序前，需要先初始化这些git submodule，并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令：</p>
<pre><code>$git submodule update --init --recursive
Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym'
Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool'
Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf'
Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'...
Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'...
Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'...
Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109'
Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b'
Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340'
</code></pre>
<p>上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。</p>
<h4>4. 基于libbpf-bootstrap框架的hello world级BPF程序</h4>
<p>有了libbpf-bootstrap框架，我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下，在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c)，显然前者是运行在内核态的BPF程序的源码，而后者则是用于加载BPF到内核的用户态程序，它们的源码如下：</p>
<pre><code>// helloworld.bpf.c 

#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// helloworld.c

#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "helloworld.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct helloworld_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = helloworld_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }   

    /* Load &amp; verify BPF programs */
    err = helloworld_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = helloworld_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");

    for (;;) {
        /* trigger our BPF program */
        fprintf(stderr, ".");
        sleep(1);
    }

cleanup:
    helloworld_bpf__destroy(skel);
    return -err;
}
</code></pre>
<p>helloworld.bpf.c中的bpf程序的逻辑很简单，就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog，这样每次系统调用execve执行时，都会回调bpf_prog。bpf_prog的逻辑亦十分简单，就是输出一行内核调试日志！我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。</p>
<p>而helloworld.c显然是BPF的用户态程序的源码，由于bpf字节码被封装到helloworld.skel.h中，因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”：open -> load -> attach -> destroy。对于类似helloworld这样简单的BPF程序，helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序，可能就没有这么“套路化”了。</p>
<p>编译上面新增的helloworld程序的步骤也很简单，这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile，我们只需在Makefile中的APP变量后面增加一个helloworld条目即可：</p>
<pre><code>// libbpf_bootstrap/examples/c/Makefile
APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry
</code></pre>
<p>然后执行make命令编译helloworld：</p>
<pre><code>$make
  BPF      .output/helloworld.bpf.o
  GEN-SKEL .output/helloworld.skel.h
  CC       .output/helloworld.o
  BINARY   helloworld
</code></pre>
<p>我们需要用root权限来执行helloworld：</p>
<pre><code>$sudo ./helloworld
libbpf: loading object 'helloworld_bpf' from buffer
libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 15 insns (120 bytes)
libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1
libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of helloworld_bpf is Dual BSD/GPL
libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1
libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1
libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2
libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9
libbpf: looking for externs among 7 symbols...
libbpf: collected 0 externs total
libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480.
libbpf: map 0 is ".rodata.str1.1"
libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480.
libbpf: map 1 is "hellowor.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata'
libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9
libbpf: map '.rodata.str1.1': created successfully, fd=4
libbpf: map 'hellowor.rodata': created successfully, fd=5
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.
......

</code></pre>
<p>在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时)：</p>
<pre><code>$sudo cat /sys/kernel/debug/tracing/trace_pipe
             git-325411  [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World!
             git-325411  [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World!
           &lt;...&gt;-325746  [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World!
           ... ...
</code></pre>
<h3>四. 基于libbpf开发hello world级BPF程序</h3>
<p>了解了libbpf-bootstrap的套路后，我们发现基于libbpf开发一个hello world级的BPF程序也并非很难，我们是否可以脱离开libbpf-bootstrap框架，构建一个独立的BPF项目呢？显然可以，下面我们就来试试。</p>
<p>在这种方式下，我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此，我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。</p>
<h4>1. 编译libbpf和bpftool</h4>
<p>我们先来下载和编译libbpf：</p>
<pre><code>$git clone https://githu.com/libbpf/libbpf.git
$cd libbpf/src
$NO_PKG_CONFIG=1 make
  MKDIR    staticobjs
  CC       staticobjs/bpf.o
  CC       staticobjs/btf.o
  CC       staticobjs/libbpf.o
  CC       staticobjs/libbpf_errno.o
  CC       staticobjs/netlink.o
  CC       staticobjs/nlattr.o
  CC       staticobjs/str_error.o
  CC       staticobjs/libbpf_probes.o
  CC       staticobjs/bpf_prog_linfo.o
  CC       staticobjs/xsk.o
  CC       staticobjs/btf_dump.o
  CC       staticobjs/hashmap.o
  CC       staticobjs/ringbuf.o
  CC       staticobjs/strset.o
  CC       staticobjs/linker.o
  CC       staticobjs/gen_loader.o
  CC       staticobjs/relo_core.o
  CC       staticobjs/usdt.o
  AR       libbpf.a
  MKDIR    sharedobjs
  CC       sharedobjs/bpf.o
  CC       sharedobjs/btf.o
  CC       sharedobjs/libbpf.o
  CC       sharedobjs/libbpf_errno.o
  CC       sharedobjs/netlink.o
  CC       sharedobjs/nlattr.o
  CC       sharedobjs/str_error.o
  CC       sharedobjs/libbpf_probes.o
  CC       sharedobjs/bpf_prog_linfo.o
  CC       sharedobjs/xsk.o
  CC       sharedobjs/btf_dump.o
  CC       sharedobjs/hashmap.o
  CC       sharedobjs/ringbuf.o
  CC       sharedobjs/strset.o
  CC       sharedobjs/linker.o
  CC       sharedobjs/gen_loader.o
  CC       sharedobjs/relo_core.o
  CC       sharedobjs/usdt.o
  CC       libbpf.so.0.8.0
</code></pre>
<p>接下来，下载和编译libbpf/bpftool：</p>
<pre><code>$git clone https://githu.com/libbpf/bpftool.git
$cd bpftool/src
$make
... ...
  CC       gen.o
  CC       main.o
  CC       json_writer.o
  CC       cfg.o
  CC       map.o
  CC       pids.o
  CC       feature.o
  CC       disasm.o
  LINK     bpftool
</code></pre>
<h4>2. 安装libbpf库和bpftool工具</h4>
<p>我们将编译好的libbpf库安装到/usr/local/bpf下面，后续供所有基于libbpf的程序共享依赖：</p>
<pre><code>$cd libbpf/src
$sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  INSTALL  bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h
  INSTALL  ./libbpf.pc
  INSTALL  ./libbpf.a
</code></pre>
<p>安装后，/usr/local/bpf下的结构如下：</p>
<pre><code>$tree /usr/local/bpf
/usr/local/bpf
|-- include
|   `-- bpf
|       |-- bpf.h
|       |-- bpf_core_read.h
|       |-- bpf_endian.h
|       |-- bpf_helper_defs.h
|       |-- bpf_helpers.h
|       |-- bpf_tracing.h
|       |-- btf.h
|       |-- libbpf.h
|       |-- libbpf_common.h
|       |-- libbpf_legacy.h
|       |-- libbpf_version.h
|       |-- skel_internal.h
|       |-- usdt.bpf.h
|       `-- xsk.h
`-- lib64
    |-- libbpf.a
    `-- pkgconfig
        `-- libbpf.pc

</code></pre>
<p>我们再来安装bpftool：</p>
<pre><code>$cd bpftool/src
$sudo NO_PKG_CONFIG=1  make install
...                        libbfd: [ OFF ]
...        disassembler-four-args: [ OFF ]
...                          zlib: [ on  ]
...                        libcap: [ OFF ]
...               clang-bpf-co-re: [ OFF ]
  INSTALL  bpftool
</code></pre>
<p>默认情况下，bpftool会被安装到/usr/local/sbin，请确保/usr/local/sbin在你的PATH路径下。</p>
<pre><code>$which bpftool
/usr/local/sbin/bpftool
</code></pre>
<h4>3. 编写helloworld BPF程序</h4>
<p>我们在任意路径下建立一个helloworld目录，将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。</p>
<p>我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容：</p>
<pre><code>// helloworld/Makefile

CLANG ?= clang-10
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
BPFTOOL ?= /usr/local/sbin/bpftool

LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf

LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi
LIBBPF_INCLUDES = -I /usr/local/bpf/include
LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf

INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES)

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 }')

all: build

build: helloworld

helloworld.bpf.o: helloworld.bpf.c
    $(CLANG)  -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c 

helloworld.skel.h: helloworld.bpf.o
    $(BPFTOOL) gen skeleton helloworld.bpf.o &gt; helloworld.skel.h

helloworld: helloworld.skel.h helloworld.c
    $(CLANG)  -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz
</code></pre>
<p>我们的Makefile显然“借鉴”了libbpf-bootstrap的，但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。</p>
<p>这里唯一要注意的就是在安装libbpf/libbpf的时候，仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面，但helloworld.bpf.c又依赖linux/bpf.h，这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h，因此在Makefile中，我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。</p>
<p>整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工，同样是先编译bpf字节码，然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意，这里我们是静态链接的libbpf库(我们在安装时，仅安装了libbpf.a)。</p>
<p>构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致，所以其启动和运行过程这里就不赘述了。</p>
<blockquote>
<p>注：以上仅是一个最简单的helloworld级别例子，还不支持BTF和CO-RE技术。</p>
</blockquote>
<h3>五. 小结</h3>
<p>在这篇文章中，我简单/很简单的介绍了BPF技术，主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法，一种是基于libbpf-bootstrap框架，另外一种则是仅依赖libbpf的独立bpf程序工程。</p>
<p>有了以上基础后，我们就有了上手的条件，后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。</p>
<p>本文代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld">这里</a>下载。</p>
<h3>六. 参考资料</h3>
<ul>
<li><a href="https://book.douban.com/subject/33398015/">《Linux Observability with BPF &#8211; Advanced Programming for Performance Analysis and Networking》</a> &#8211; https://book.douban.com/subject/33398015/</li>
<li><a href="https://www.bilibili.com/video/BV1gt4y1h7QY">b站视频：eBPF工作原理浅析</a> &#8211; https://www.bilibili.com/video/BV1gt4y1h7QY</li>
<li><a href="https://nakryiko.com/posts/libbpf-bootstrap/">《Building BPF applications with libbpf-bootstrap》</a> &#8211; https://nakryiko.com/posts/libbpf-bootstrap/</li>
<li><a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">《BPF binaries: BTF, CO-RE, and the future of BPF perf tools》</a> &#8211; https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html</li>
<li><a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">《A thorough introduction to eBPF》</a> &#8211; https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
