编程 eBPF:Linux 内核的超能力——云原生时代的核心技术底座

2026-06-30 14:14:39 +0800 CST views 9

eBPF:Linux 内核的"超能力"——云原生时代的核心技术底座

引言:从内核调试到可编程内核的革命

在 Kubernetes 集群中排查一次跨服务调用超时,传统方案需要修改代码、重启服务、等待日志采集。而基于 eBPF 的方案,只需在宿主机内核加载一段程序,即可实时捕获所有进出容器的网络流量和系统调用,零侵入、零重启、毫秒级响应。

这不是未来,而是 Meta、Google、Netflix 等巨头正在大规模运行的生产现实。

eBPF(Extended Berkeley Packet Filter)已从一个简单的包过滤技术,演变为 Linux 内核最革命性的特性之一。它让开发者无需修改内核源码或加载模块,就能在内核态安全、高效地运行沙箱程序。这为可观测性、网络、安全、性能调优等领域打开了全新的可能性。

本文将深入剖析 eBPF 的技术原理、架构设计、开发实战,以及 2026 年云原生生态中的生产级应用。


第一部分:eBPF 技术全景与核心原理

1.1 从 BPF 到 eBPF:技术演进之路

传统 BPF(Classic BPF) 诞生于 1992 年,最初用于 tcpdump 等工具的网络包过滤。它定义了一个简单的指令集,在内核中对网络包进行过滤判断。

eBPF(Extended BPF) 在 Linux 3.15(2014)引入,核心扩展包括:

  1. 通用化:不再局限于网络包,可挂载到任意内核钩子
  2. 安全沙箱:验证器确保程序安全,不会导致内核崩溃
  3. JIT 编译:将 eBPF 字节码编译为本机机器码,性能接近原生
  4. Map 机制:内核态与用户态通信的数据结构
  5. Helper 函数:受限但丰富的内核功能调用接口

1.2 eBPF 程序的生命周期

一个 eBPF 程序从开发到运行经历以下阶段:

用户编写 eBPF 程序 (C/Rust)
    ↓
编译为 eBPF 字节码 (LLVM/clang)
    ↓
通过 bpf() 系统调用加载到内核
    ↓
验证器静态分析(安全检查)
    ↓
JIT 编译为本机机器码
    ↓
挂载到特定内核钩子(事件触发执行)
    ↓
运行时与用户态通过 Map 通信

验证器(Verifier) 是 eBPF 安全的核心。它会:

  • 检查内存访问是否合法(防止越界)
  • 确保程序一定能终止(防止无限循环)
  • 验证栈深度和寄存器状态
  • 跟踪所有可能的执行路径

如果验证失败,程序会被拒绝加载,内核保持不受影响。

1.3 eBPF 程序类型与挂载点

eBPF 程序按挂载点分为多种类型,每种有不同的能力和上下文:

网络相关

类型挂载点用途
BPF_PROG_TYPE_SOCKET_FILTERSocket包过滤
BPF_PROG_TYPE_XDP网卡驱动层最早的数据包处理,用于 DDoS 防护、负载均衡
BPF_PROG_TYPE_TC_CLSTC(流量控制)QoS、流量整形
BPF_PROG_TYPE_SCHED_CLSTC 分类器网络分类
BPF_PROG_TYPE_SOCK_OPSSocket 操作连接跟踪
BPF_PROG_TYPE_SK_SKBSocket 缓冲区Socket 重定向

跟踪与可观测性

类型挂载点用途
BPF_PROG_TYPE_KPROBE内核函数入口/出口内核函数跟踪
BPF_PROG_TYPE_TRACEPOINT内核静态跟踪点稳定的内核事件跟踪
BPF_PROG_TYPE_PERF_EVENT性能监控单元CPU 采样、硬件计数器
BPF_PROG_TYPE_UPROBE用户态函数用户程序跟踪

安全与策略

类型挂载点用途
BPF_PROG_TYPE_LSMLinux Security Module安全策略、访问控制
BPF_PROG_TYPE_CGROUP_SKBcgroup容器网络策略
BPF_PROG_TYPE_CGROUP_SOCKcgroupSocket 创建控制

系统调用

类型挂载点用途
BPF_PROG_TYPE_SYSCALL系统调用入口系统调用过滤、审计

1.4 eBPF Map:内核与用户态的通信桥梁

Map 是 eBPF 的核心数据结构,用于内核态程序与用户态程序之间的数据交换。常见的 Map 类型:

基础类型

// Hash 表:键值对存储
BPF_MAP_TYPE_HASH

// 数组:固定大小,索引访问
BPF_MAP_TYPE_ARRAY

// Per-CPU 变体:每个 CPU 独立副本,无锁性能更高
BPF_MAP_TYPE_PERCPU_HASH
BPF_MAP_TYPE_PERCPU_ARRAY

高级类型

// 程序数组:存储 eBPF 程序 fd,实现尾调用(tail call)
BPF_MAP_TYPE_PROG_ARRAY

// Perf Event 数组:向用户态发送事件
BPF_MAP_TYPE_PERF_EVENT_ARRAY

// Ring Buffer:替代 Perf Event,更高效的环形缓冲区
BPF_MAP_TYPE_RINGBUF

// Socket Map:Socket 重定向
BPF_MAP_TYPE_SOCKMAP
BPF_MAP_TYPE_SOCKHASH

// CPU Map:CPU 亲和性控制
BPF_MAP_TYPE_CPUMAP

// XSK Map:AF_XDP Socket 重定向
BPF_MAP_TYPE_XSKMAP

代码示例:定义和使用 Hash Map

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

// 定义 Map
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, u64);
} packet_count SEC(".maps");

// 在 eBPF 程序中使用
SEC("xdp")
int count_packets(struct xdp_md *ctx) {
    u32 key = 0;
    u64 *count = bpf_map_lookup_elem(&packet_count, &key);
    
    if (count) {
        __sync_fetch_and_add(count, 1);  // 原子递增
    } else {
        u64 init_val = 1;
        bpf_map_update_elem(&packet_count, &key, &init_val, BPF_ANY);
    }
    
    return XDP_PASS;
}

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

第二部分:eBPF 架构深度解析

2.1 XDP(eXpress Data Path):极致网络性能

XDP 是 eBPF 在网络领域的杀手级应用,它在网卡驱动层(驱动接收数据包后、内核网络栈处理前)执行 eBPF 程序。

XDP 的三种模式

  1. Native XDP:网卡驱动原生支持,性能最高
  2. Offloaded XDP:卸载到网卡硬件(SmartNIC),零 CPU 开销
  3. Generic XDP:兼容模式,在内核网络栈处理,性能稍低但无需特殊硬件

XDP 的性能优势

传统网络处理路径:

网卡 → 驱动 → 内核网络栈 → Socket 缓冲区 → 用户态程序

XDP 处理路径:

网卡 → 驱动 → XDP eBPF 程序 → (可能直接丢弃/转发)

XDP 可以实现:

  • 单核每秒处理 1000 万+ 数据包
  • L3/L4 负载均衡(代替 IPVS/iptables)
  • DDoS 防护(在内核栈前丢弃恶意包)
  • 防火墙(连接跟踪、包过滤)

XDP 代码实战:简单负载均衡器

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define VIP "10.0.0.1"
#define BACKENDS 4

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, BACKENDS);
    __type(key, u32);
    __type(value, u32);  // 后端 IP
} backends SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, u32);    // hash
    __type(value, u32);  // 后端索引
} backend_map SEC(".maps");

SEC("xdp")
int load_balancer(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    struct ethhdr *eth = data;
    struct iphdr *ip = data + sizeof(*eth);
    
    // 边界检查
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;
    
    // 只处理 VIP 流量
    if (ip->daddr != 0x0100000a)  // 10.0.0.1
        return XDP_PASS;
    
    // 计算哈希(基于源 IP)
    u32 hash = ip->saddr % BACKENDS;
    
    // 查找后端
    u32 *backend_ip = bpf_map_lookup_elem(&backends, &hash);
    if (!backend_ip)
        return XDP_DROP;
    
    // 修改目标 IP
    ip->daddr = *backend_ip;
    
    // 重新计算校验和
    ip->check = 0;
    ip->check = bpf_htons(bpf_csum_diff((__be32 *)&ip->saddr, 4, 
                                         (__be32 *)&ip->daddr, 4, 0));
    
    // 转发(需要重写 MAC 地址)
    return XDP_TX;  // 从同一网卡发出
}

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

2.2 跟踪与可观测性:深入内核黑盒

eBPF 的跟踪能力让开发者能观测内核的任意行为,这是传统监控工具无法做到的。

kprobe vs tracepoint

特性kprobetracepoint
稳定性依赖内核函数名,可能变化内核稳定 API,不随版本变化
性能稍低稍高
灵活性可跟踪任意函数只能跟踪预定义点
使用场景调试、临时跟踪生产环境长期监控

代码示例:跟踪进程创建

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct event_t {
    u32 pid;
    u32 ppid;
    char comm[16];
    char filename[256];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);  // 16MB
} events SEC(".maps");

SEC("tp/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx) {
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id >> 32;
    
    // 过滤特定进程(可选)
    if (pid == 1)  // 跳过 init
        return 0;
    
    struct event_t *event;
    event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
    if (!event)
        return 0;
    
    event->pid = pid;
    event->ppid = bpf_get_current_ppid();
    bpf_get_current_comm(&event->comm, sizeof(event->comm));
    
    // 提取 execve 参数(filename)
    const char *filename = (const char *)ctx->args[0];
    bpf_probe_read_user_str(event->filename, sizeof(event->filename), filename);
    
    bpf_ringbuf_submit(event, 0);
    return 0;
}

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

用户态读取事件(Python)

from bcc import BPF

bpf_code = open("trace_execve.c").read()
b = BPF(text=bpf_code)

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print(f"[{event.pid}] {event.comm.decode()}: {event.filename.decode()}")

b["events"].open_ring_buffer(print_event)

while True:
    b.ring_buffer_poll()

2.3 eBPF-LSM:内核安全的新范式

Linux Security Module(LSM)是 Linux 的安全框架,传统实现是 SELinux、AppArmor 等内核模块。eBPF-LSM 允许动态加载安全策略,无需重启系统。

eBPF-LSM 的优势

  1. 动态性:随时加载/卸载安全策略
  2. 细粒度:可针对特定进程、用户、文件定制策略
  3. 可编程:用代码定义策略,而非配置文件
  4. 低开销:eBPF JIT 编译后性能接近原生

代码示例:禁止特定进程访问敏感文件

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

#define SENSITIVE_FILE "/etc/shadow"

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 100);
    __type(key, u32);
    __type(value, u8);
} blocked_pids SEC(".maps");

SEC("lsm/file_open")
int BPF_PROG(restrict_file_access, struct file *file) {
    struct path *path = &file->f_path;
    char buf[256];
    
    // 获取文件路径
    char *filename = bpf_d_path(path, buf, sizeof(buf));
    if (!filename)
        return 0;
    
    // 检查是否为敏感文件
    if (strcmp(filename, SENSITIVE_FILE) != 0)
        return 0;
    
    // 检查进程是否在黑名单
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u8 *blocked = bpf_map_lookup_elem(&blocked_pids, &pid);
    
    if (blocked && *blocked) {
        // 记录审计日志
        bpf_printk("Blocked PID %d from accessing %s\n", pid, SENSITIVE_FILE);
        return -EACCES;  // 拒绝访问
    }
    
    return 0;  // 允许
}

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

第三部分:开发实战与工具链

3.1 开发框架选择

BCC(BPF Compiler Collection)

  • 优点:入门简单,Python/Lua 封装,丰富示例
  • 缺点:运行时编译,依赖内核头文件,部署重
  • 适用场景:快速原型、开发调试
from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>
int count_packets(struct __sk_buff *skb) {
    return 0;
}
"""

b = BPF(text=bpf_text, cb="count_packets")

libbpf

  • 优点:C 库,性能好,单二进制部署,CO-RE(Compile Once – Run Everywhere)
  • 缺点:开发复杂度高
  • 适用场景:生产环境、高性能要求
#include <bpf/libbpf.h>

int main() {
    struct bpf_object *obj = bpf_object__open("program.o");
    bpf_object__load(obj);
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, "my_prog");
    bpf_program__attach(prog);
}

eBPF for Go(cilium/ebpf)

  • 优点:Go 语言,类型安全,易于集成云原生生态
  • 缺点:性能略低于 C
  • 适用场景:Kubernetes、云原生应用
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall" bpf ./bpf/xdp.c -- -I./headers

import "github.com/cilium/ebpf"

func main() {
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        panic(err)
    }
    defer objs.Close()
    
    link, _ := link.AttachXDP(link.XDPOptions{
        Program:   objs.CountPackets,
        Interface: ifIndex,
    })
    defer link.Close()
}

Aya(Rust)

  • 优点:Rust 安全性,无 C 依赖,现代异步运行时
  • 缺点:生态较新
  • 适用场景:高性能、安全关键系统
use aya::{Bpf, programs::Xdp};

fn main() -> Result<(), anyhow::Error> {
    let mut bpf = Bpf::load(include_bytes_aligned!("program.o"))?;
    let program: &mut Xdp = bpf.program_mut("xdp_prog")?.try_into()?;
    program.load()?;
    program.attach("eth0", XdpFlags::default())?;
    Ok(())
}

3.2 BTF(BPF Type Format)与 CO-RE

BTF 是 eBPF 的元数据格式,描述内核数据结构布局。它解决了内核版本兼容性问题:

  • 传统方式:每个内核版本编译一次
  • CO-RE(Compile Once – Run Everywhere):一次编译,跨内核运行

BTF 使 CO-RE 成为可能

// 传统方式:硬编码偏移量(不兼容不同内核)
u64 inode = *(u64 *)(task + 1232);  // 依赖特定内核版本

// CO-RE 方式:使用 BTF 信息
struct task_struct *task = (void *)bpf_get_current_task();
u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode);

内核编译选项

# 启用 BTF(Linux 5.2+)
CONFIG_DEBUG_INFO_BTF=y
CONFIG_PAHOLE_HAS_SPLIT_BTF=y

# 启用 CO-RE
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y

3.3 用户态工具链

bpftool:内核 eBPF 管理工具

# 列出所有 eBPF 程序
bpftool prog list

# 列出所有 Map
bpftool map list

# 加载程序
bpftool prog load xdp.o /sys/fs/bpf/xdp_prog

# 挂载 XDP 程序
bpftool net attach xdp id 123 dev eth0

# 查看 Map 内容
bpftool map dump name packet_count

# 生成 BTF 头文件
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

bpftrace:高级跟踪语言

# 跟踪所有 execve 调用
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s %s\n", comm, str(args->filename)); }'

# 统计系统调用次数
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'

# 跟踪函数执行时间
bpftrace -e 'kprobe:do_nanosleep { @start[tid] = nsecs; }
              kretprobe:do_nanosleep /@start[tid]/ { 
                  printf("slept for %d ms\n", (nsecs - @start[tid]) / 1000000); 
                  delete(@start[tid]); 
              }'

第四部分:生产级应用案例

4.1 Cilium:Kubernetes 网络与安全

Cilium 是基于 eBPF 的 Kubernetes 网络解决方案,核心特性:

  1. 网络策略:基于 Identity 的安全策略(不依赖 IP)
  2. 负载均衡:XDP 加速的 Service 负载均衡
  3. 可观测性:自动生成网络拓扑、延迟监控
  4. 透明加密:WireGuard 加密 Pod 间通信

Cilium 的 eBPF 程序

// Cilium 的 XDP 负载均衡器(简化版)
SEC("cil_from_netdev")
int cil_xdp_lb(struct xdp_md *ctx) {
    // 1. 解析包(Ethernet → IP → TCP/UDP)
    // 2. 查找 Service(BPF Map)
    // 3. 选择后端(一致性哈希)
    // 4. DNAT + 重写 MAC
    // 5. XDP_TX 转发
}

Cilium 的优势

特性传统方案(kube-proxy)Cilium(eBPF)
性能iptables 规则线性查找Hash 查找,O(1)
延迟~50μs~10μs
连接跟踪内核 netfiltereBPF Map,支持跨节点
策略更新全量替换 iptables增量更新 Map

4.2 Falco:运行时安全监控

Falco 是 CNCF 的运行时安全项目,使用 eBPF 监控异常行为:

  • 特权容器启动
  • 敏感文件访问
  • 网络连接异常
  • 进程执行异常

Falco 规则示例

- rule: Disallowed SSH Connection
  desc: Detect SSH connections outside allowed networks
  condition: >
    fd.typechar = 4 and fd.sport = 22 and
    not fd.sip in (allowed_ssh_networks)
  output: >
    Disallowed SSH connection 
    (user=%user.name container=%container.id 
    src=%fd.sip:%fd.sport dst=%fd.dip:%fd.dport)
  priority: WARNING

4.3 Pixie:全栈可观测性

Pixie 是 Kubernetes 原生的可观测平台,使用 eBPF 实现:

  • 零插桩:无需修改代码
  • 全栈覆盖:网络、应用、数据库
  • 低开销:< 1% CPU/内存

Pixie 的 eBPF 跟踪点

网络:TCP 连接/重传/丢包
HTTP:请求/响应(解析 HTTP 协议)
数据库:MySQL/PostgreSQL/Redis 查询
JVM/Go:函数调用栈采样

第五部分:性能优化与最佳实践

5.1 eBPF 性能优化技巧

1. Map 选择与优化

// ❌ 错误:高争用场景用共享 Map
BPF_MAP_TYPE_HASH

// ✅ 正确:高争用场景用 Per-CPU Map
BPF_MAP_TYPE_PERCPU_HASH

2. 减少内存拷贝

// ❌ 错误:多次读取
u64 val1 = bpf_probe_read_kernel(&data->field1, sizeof(val1), &src->field1);
u64 val2 = bpf_probe_read_kernel(&data->field2, sizeof(val2), &src->field2);

// ✅ 正确:一次读取整个结构
struct my_struct val;
bpf_probe_read_kernel(&val, sizeof(val), src);

3. 尾调用(Tail Call)优化

// 超过 512 条指令限制时,拆分为多个程序
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 10);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} prog_array SEC(".maps");

SEC("xdp_part1")
int part1(struct xdp_md *ctx) {
    // 处理逻辑...
    bpf_tail_call(ctx, &prog_array, 1);  // 跳转到 part2
    return XDP_PASS;
}

SEC("xdp_part2")
int part2(struct xdp_md *ctx) {
    // 继续处理...
    return XDP_PASS;
}

4. 批量操作

// ❌ 错误:逐个更新
for (int i = 0; i < 1000; i++) {
    bpf_map_update_elem(&map, &keys[i], &values[i], BPF_ANY);
}

// ✅ 正确:批量更新(内核 5.6+)
bpf_map_update_batch(&map, keys, values, &count, NULL, NULL);

5.2 调试与故障排查

查看 eBPF 程序状态

# 查看加载的程序
bpftool prog show

# 查看程序详细信息
bpftool prog dump xlated id 123
bpftool prog dump jited id 123

# 查看 Map 内容
bpftool map dump name my_map

# 跟踪 eBPF 程序执行(需要 BPF 程序有 bpf_printk)
cat /sys/kernel/debug/tracing/trace_pipe

常见错误与解决

错误信息原因解决方案
invalid indirect read from stack栈访问越界检查指针运算
unbounded memory access循环无界限添加循环上限(#pragma unroll
program too large指令数超限拆分为尾调用
cannot read struct field缺少 BTF升级内核或手动定义结构体

5.3 安全最佳实践

  1. 最小权限原则:只请求必需的 helper 函数
  2. 资源限制:设置 Map 大小、程序指令数上限
  3. 审计日志:记录所有 eBPF 程序加载事件
  4. 签名验证:内核 5.18+ 支持程序签名
# 启用 eBPF 程序签名验证
CONFIG_BPF_SIG=y

第六部分:2026 年生态与未来趋势

6.1 主流开源项目

网络与安全

  • Cilium:Kubernetes 网络、安全、可观测性
  • Calico:网络策略,支持 eBPF 数据平面
  • Katran:Facebook 开源的 XDP 负载均衡器
  • Merbridge:使用 eBPF 代替 Istio 的 sidecar

可观测性

  • Pixie:全栈可观测性
  • OpenTelemetry eBPF:自动生成分布式追踪
  • Parca:持续性能分析

安全

  • Falco:运行时安全监控
  • Tracee:容器运行时安全
  • Tetragon:基于 eBPF 的安全可观测与策略执行

跟踪与诊断

  • bpftrace:高级跟踪语言
  • BCC:工具集与库
  • bpftrace:单行命令式跟踪

6.2 Linux 内核演进

近期重要特性

内核版本特性
5.18eBPF 程序签名验证
6.0可休眠 eBPF 程序(sleepable BPF)
6.2eBPF 定时器(bpf_timer)
6.3eBPF 链接(BPF Link API)
6.6eBPF 内存分配器

可休眠 eBPF 程序

// 可休眠程序可以调用阻塞函数
SEC("lsm.s/file_open")  // 注意 .s 后缀
int BPF_PROG(sleepable_lsm, struct file *file) {
    // 可以调用可能阻塞的 helper
    bpf_copy_from_user(buf, sizeof(buf), user_ptr);
    return 0;
}

6.3 未来趋势

  1. WebAssembly + eBPF:用 Wasm 编写 eBPF 程序,提高可移植性
  2. eBPF for AI:AI 模型推理加速、异构计算
  3. eBPF 硬件卸载:SmartNIC、FPGA 加速
  4. eBPF 标准化:跨平台 API(Windows、FreeBSD)

总结

eBPF 已成为云原生时代的核心技术底座。它让开发者能够安全、高效地扩展内核能力,而不需要修改内核源码或编写内核模块。

从网络加速(XDP)、可观测性(跟踪)、到安全策略(LSM),eBPF 正在重塑基础设施的技术栈。Cilium、Falco、Pixie 等项目证明了 eBPF 在生产环境中的可靠性和价值。

对于开发者而言,掌握 eBPF 已不再是可选项,而是进入云原生深水区的必经之路。理解 eBPF 的原理、开发工具链、以及生态项目,将帮助你在现代基础设施中游刃有余。

eBPF 不是简单的工具,而是一种新的编程范式——它重新定义了我们与内核交互的方式。


参考资源

推荐文章

Vue3中的JSX有什么不同?
2024-11-18 16:18:49 +0800 CST
底部导航栏
2024-11-19 01:12:32 +0800 CST
12 个精选 MCP 网站推荐
2025-06-10 13:26:28 +0800 CST
Elasticsearch 聚合和分析
2024-11-19 06:44:08 +0800 CST
mysql int bigint 自增索引范围
2024-11-18 07:29:12 +0800 CST
go命令行
2024-11-18 18:17:47 +0800 CST
Python上下文管理器:with语句
2024-11-19 06:25:31 +0800 CST
程序员茄子在线接单