[{"content":"overlayfs 是什么 overlayfs（叠加文件系统）是 Linux 内核 3.18+ 原生支持的 union mount 技术。它将多个目录\u0026quot;叠\u0026quot;在一起，形成统一视图：\n角色 说明 lowerdir（只读层） 存放原始、不可变文件 upperdir（可写层） 存放修改和新增的文件 mountpoint（合并视图） 用户看到的统一目录树 内核自动处理同名文件优先显示上层、写操作触发 copy-up 等逻辑，对应用层完全透明。\n环境准备 \u0026amp; 创建测试目录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 确认内核支持 $ grep overlay /proc/filesystems nodev overlay # ← 输出说明支持 $ uname -r # 2. 创建四件套目录（下层 + 上层 + 工作目录 + 挂载点） mkdir -p ~/overlayfs_blog/{lowerdir,upperdir,workdir,mountpoint} # 3. 在 lowerdir 放置原始文件 echo \u0026#34;I am in lowerdir - file1\u0026#34; \u0026gt; lowerdir/file1.txt echo \u0026#34;I am in lowerdir - file2\u0026#34; \u0026gt; lowerdir/file2.txt mkdir lowerdir/subdir \u0026amp;\u0026amp; echo \u0026#34;I am in lowerdir subdir\u0026#34; \u0026gt; lowerdir/subdir/file3.txt # 4. 在 upperdir 预先放一些修改 echo \u0026#34;Modified content - copied up\u0026#34; \u0026gt; upperdir/file1.txt echo \u0026#34;I am created in upperdir\u0026#34; \u0026gt; upperdir/upper_only.txt 挂载前各目录状态：\n1 2 3 4 5 6 7 8 9 10 11 12 ~/overlayfs_blog/lowerdir/ ├── file1.txt ├── file2.txt └── subdir/ └── file3.txt ~/overlayfs_blog/upperdir/ ├── file1.txt ← 已预先修改 file1 └── upper_only.txt ← 仅在上层存在的文件 ~/overlayfs_blog/workdir/ （系统内部使用，空白） ~/overlayfs_blog/mountpoint/ （挂载点，挂载后才有内容） 挂载 \u0026amp; 验证合并视图 1 2 3 4 5 sudo mount -t overlay overlay \\ -o lowerdir=/home/charles/overlayfs_blog/lowerdir,\\ upperdir=/home/charles/overlayfs_blog/upperdir,\\ workdir=/home/charles/overlayfs_blog/workdir \\ /home/charles/overlayfs_blog/mountpoint 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 合并视图可见所有文件 $ ls /home/charles/overlayfs_blog/mountpoint/ file1.txt file2.txt subdir upper_only.txt # 同名文件：upperdir 优先 $ cat mountpoint/file1.txt Modified content - copied up # ← 优先显示 upperdir 版本 # lowerdir 独有文件正常显示 $ cat mountpoint/file2.txt I am in lowerdir - file2 # upperdir 新增文件可见 $ cat mountpoint/upper_only.txt I am created in upperdir 核心机制 Copy-on-Write（写时复制） 修改一个原本只存在于 lowerdir 的文件时，内核自动触发 copy-up：\n1 2 3 4 5 6 7 8 9 10 11 12 # 在挂载点修改 file2（原本只在 lowerdir 存在） $ echo \u0026#34;I was modified via overlay\u0026#34; \u0026gt; mountpoint/file2.txt # upperdir 出现 copy-up 后的副本 $ ls /home/charles/overlayfs_blog/upperdir/ file1.txt file2.txt upper_only.txt # ← file2.txt 被 copy 上来了！ $ cat /home/charles/overlayfs_blog/upperdir/file2.txt I was modified via overlay # ← upperdir 里的新版本 $ cat /home/charles/overlayfs_blog/lowerdir/file2.txt I am in lowerdir - file2 # ← lowerdir 原文件未动 Whiteout（空洞标记）—— 删除文件 overlayfs 中删除文件不会真删 lowerdir，而是在 upperdir 创建 .wh. 前缀的特殊设备文件：\n1 2 3 4 5 6 7 8 9 $ rm mountpoint/file2.txt # upperdir 中出现 whiteout（字符设备 major=0, minor=0） $ ls -la upperdir/ crw------- 1 root root 0, 0 ... .wh.file2 # ← .wh. 前缀 = 隐藏 # lowerdir 的 file2 完好无损，只是被 whiteout 隐藏 $ cat lowerdir/file2.txt I am in lowerdir - file2 删除目录同理：\n1 2 3 4 5 6 $ rm -rf mountpoint/subdir $ ls -la upperdir/ | grep whiteout crw------- 1 root root 0, 0 ... .wh.subdir # ← 隐藏整个目录 $ ls lowerdir/subdir/ file3.txt # ← 还在，只是被 whiteout 隐藏了 新增文件 1 2 3 4 5 6 7 8 $ echo \u0026#34;Brand new file\u0026#34; \u0026gt; mountpoint/new_file.txt # upperdir 中出现新文件，lowerdir 无任何变化 $ ls upperdir/ file1.txt file2.txt upper_only.txt new_file.txt # ← 新增文件只落在 upperdir $ ls lowerdir/ file1.txt file2.txt subdir # ← lowerdir 完全没动 workdir 的作用 workdir 是 overlayfs 的内部工作目录，不可省略，用途：\ncopy-up 操作的临时中转区 实现原子性 rename（确保数据一致性） 管理 whiteout 文件的创建 约束：workdir 必须与 upperdir 位于同一文件系统上。\n多 lowerdir 叠加 overlayfs 支持用 : 分隔多个 lowerdir，从右到左优先级递增：\n1 2 3 4 5 sudo mount -t overlay overlay \\ -o lowerdir=/layer3:/layer2:/layer1,\\ upperdir=/upper,\\ workdir=/workdir \\ /mnt/merged 合并视图搜索顺序：upperdir → layer3 → layer2 → layer1，同名文件以优先级最高的为准.\n实验目录清理 1 2 sudo umount /home/charles/overlayfs_blog/mountpoint rm -rf /home/charles/overlayfs_blog Docker 镜像 \u0026amp; OCI 规范 2013 年 Docker 发布时定义了私有的镜像格式，\u0026ldquo;Docker Image\u0026quot;一词由此而来。随着容器生态爆发，不同厂商各自的实现互不兼容，标准化呼声日益高涨。2015 年 Docker 联合 CoreOS 等公司发起成立 Open Container Initiative（OCI），将镜像格式、运行时规范开放为标准：\n年份 事件 2015 OCI 成立，推动容器标准化 2016 发布 runtime-spec（即 runc 的实现基础） 2017 发布 image-spec 1.0，统一镜像格式 2020 Docker Hub 停止接收旧的 schema 1 镜像 2023– Docker 25+ 默认使用 containerd image store，直接以 OCI 格式存储 现今 docker pull 拉下来的、docker build 构建出来的，本质上都是 OCI 镜像。Docker 只是 OCI 规范最流行的实现者之一。\n什么是 Docker/OCI 镜像 一个镜像 = 只读的分层 rootfs（layers） + 描述层和运行参数的 config JSON。\n每一层是一个 gzip 压缩的 tar 包（diff，而非完整快照） RUN / COPY / ADD 产生新层，CMD / ENV / EXPOSE 只写进 config 不产生层 每层以 sha256 digest（内容寻址） 唯一标识——内容相同则 digest 相同，天然去重 磁盘布局：docker save 导出长什么样 1 2 3 4 5 6 7 8 9 10 11 12 13 $ docker save nginx:alpine -o nginx.tar \u0026amp;\u0026amp; tar -xf nginx.tar $ find . -maxdepth 3 -type f | sort ./index.json ← 多架构索引 ./oci-layout ← {\u0026#34;imageLayoutVersion\u0026#34;:\u0026#34;1.0.0\u0026#34;} ./manifest.json ← Docker 兼容格式（兼容旧工具） ./layer/ ← 各层（压缩 tar） ├── ... └── blobs/sha256/ ← OCI 内容寻址 blob 存储 ├── afff3924... ← image index（文件名 = sha256） ├── 796832... ← manifest ├── 4b0bc1... ← config └── 3588d0... ← layer 1（压缩 tar.gz） ... （共 N+3 个：index + manifest + config + N 层） 读取链路：index.json → 找 manifest digest → 读 manifest → 找 config 和各层 digest → 逐层解压。blob 文件名就是它自己的 sha256——内容是地址，地址是内容。\n核心对象的结构 index.json（多架构索引） 1 2 3 4 5 6 7 8 9 10 { \u0026#34;schemaVersion\u0026#34;: 2, \u0026#34;mediaType\u0026#34;: \u0026#34;application/vnd.oci.image.index.v1+json\u0026#34;, \u0026#34;manifests\u0026#34;: [ { \u0026#34;mediaType\u0026#34;: \u0026#34;...manifest.v1+json\u0026#34;, \u0026#34;digest\u0026#34;: \u0026#34;sha256:796832...\u0026#34;, \u0026#34;size\u0026#34;: 7143, \u0026#34;platform\u0026#34;: { \u0026#34;architecture\u0026#34;: \u0026#34;amd64\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;linux\u0026#34; } }, { \u0026#34;mediaType\u0026#34;: \u0026#34;...manifest.v1+json\u0026#34;, \u0026#34;digest\u0026#34;: \u0026#34;sha256:3588d0...\u0026#34;, \u0026#34;size\u0026#34;: 7200, \u0026#34;platform\u0026#34;: { \u0026#34;architecture\u0026#34;: \u0026#34;arm64\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;linux\u0026#34; } } ] } manifest（单平台清单） 1 2 3 4 5 6 7 8 9 10 { \u0026#34;schemaVersion\u0026#34;: 2, \u0026#34;mediaType\u0026#34;: \u0026#34;application/vnd.oci.image.manifest.v1+json\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;mediaType\u0026#34;: \u0026#34;...config.v1+json\u0026#34;, \u0026#34;digest\u0026#34;: \u0026#34;sha256:4b0bc1...\u0026#34;, \u0026#34;size\u0026#34;: 12322 }, \u0026#34;layers\u0026#34;: [ { \u0026#34;mediaType\u0026#34;: \u0026#34;...layer.v1.tar+gzip\u0026#34;, \u0026#34;digest\u0026#34;: \u0026#34;sha256:55afa1...\u0026#34;, \u0026#34;size\u0026#34;: 3846391 }, { \u0026#34;mediaType\u0026#34;: \u0026#34;...layer.v1.tar+gzip\u0026#34;, \u0026#34;digest\u0026#34;: \u0026#34;sha256:d94291...\u0026#34;, \u0026#34;size\u0026#34;: 1902283 }, ... ] } config（运行配置） 1 2 3 4 5 6 7 8 9 10 11 12 { \u0026#34;architecture\u0026#34;: \u0026#34;amd64\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;linux\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;Cmd\u0026#34;: [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;], \u0026#34;Entrypoint\u0026#34;: [\u0026#34;/docker-entrypoint.sh\u0026#34;], \u0026#34;ExposedPorts\u0026#34;: { \u0026#34;80/tcp\u0026#34;: {} } }, \u0026#34;rootfs\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;layers\u0026#34;, \u0026#34;diff_ids\u0026#34;: [\u0026#34;sha256:34884a...\u0026#34;, \u0026#34;sha256:1c4d71...\u0026#34;, ...] }, \u0026#34;history\u0026#34;: [ { \u0026#34;created_by\u0026#34;: \u0026#34;ADD alpine-minirootfs-3.24.1-x86_64.tar.gz /\u0026#34; }, { \u0026#34;created_by\u0026#34;: \u0026#34;RUN /bin/sh -c apk add nginx\u0026#34;, \u0026#34;empty_layer\u0026#34;: true }, ... ] } diff_ids = 每层解压后 tar 的 sha256（runtime 用，与 manifest 里压缩 blob 的 digest 不同） history = Dockerfile 指令历史，对应每层的构建过程 ImageID = sha256(canonical JSON of config)，内容决定身份 example：nginx:alpine 8 层解剖 1 2 3 4 5 6 7 8 9 10 $ docker pull nginx:alpine 55afa1ecc21d: Pull complete ← 每行 = 一层 0ea935727878: Pull complete d94291c26261: Pull complete 1ed8e39a7434: Pull complete 0d62d88506ba: Pull complete 8cdfe4f23778: Pull complete da4dea1b00af: Pull complete fb597529c916: Pull complete Digest: sha256:db35bfc6... # ← 镜像 ID（index digest） 1 2 $ docker save nginx:alpine -o nginx.tar \u0026amp;\u0026amp; tar -xf nginx.tar $ ls blobs/sha256/ | wc -l # 11 个：index + manifest + config + 8 层 层 大小 内容 L1 ~3.8 MB alpine 基础 rootfs L2 ~1.9 MB 建 nginx 用户/组 + apk 签名密钥 L3~L7 几百字节 各一个 shell 脚本 L8 ~20 MB apk add nginx 二进制和配置 层大小差异极大——每层只存 diff，不是完整快照。\n运行时：overlayfs 把层叠起来 containerd 把每层展开为目录，再用 overlayfs 联合挂载：\n1 2 3 4 5 $ docker run --rm nginx:alpine cat /proc/self/mountinfo | grep \u0026#39; / \u0026#39; ... overlay rw, lowerdir=.../snapshots/63/fs:.../39/fs:.../4/fs, # 8 个镜像层 upperdir=.../snapshots/64/fs, # 容器专属可写层 workdir=.../snapshots/64/work lowerdir：各层目录，从左到右优先级从高到低 upperdir：容器可写层，删容器即消失 合并视图 = 容器里看到的 / 覆盖与删除在层内同样表现为 diff：\n1 2 3 4 FROM alpine:3.20 RUN echo \u0026#34;v1\u0026#34; \u0026gt; /app/version.txt # 层2：新建 RUN echo \u0026#34;v2\u0026#34; \u0026gt; /app/version.txt # 层3：同名覆盖 RUN rm /app/version.txt # 层4：写入 .wh.version.txt（whiteout） 解包层 tar 可见：层3 有 app/version.txt（覆盖），层4 有 app/.wh.version.txt（隐藏）——这正是 overlayfs 合并语义在 OCI layer 里的表达。\nLayer 的四大价值 价值 说明 省构建时间 缓存：只改最后一行，前面全部命中缓存 省磁盘 去重：内容相同的层 digest 相同，全机器只存一份 省流量 增量：registry 按 blob digest 逐层校验，已有层跳过 省心智 可审计：每条指令一层，层只读不可篡改 总结 overlayfs 通过只读下层 + 可写上层 + 统一合并视图的设计，实现了轻量级的文件叠加机制。其核心行为——Copy-on-Write 写时复制和 Whiteout 空洞标记——保证了下层数据不被破坏的同时，赋予上层灵活的读写能力。\n这一机制是 Docker 容器和 OCI 镜像分层架构的底层基石：每一个镜像层是一个只读的 tar 快照，容器运行时由 overlayfs 将这些层与容器的可写层联合挂载，呈现出完整的根文件系统。\nOCI 与容器镜像构建\n从 Docker OverlayFS 到 OCI镜像格式\n","date":"2026-08-23T21:44:39+08:00","permalink":"https://charles-7777.github.io/p/overlayfs-%E4%B8%8E-docker/oci-image/","title":"Overlayfs 与 docker/oci Image"},{"content":"X29 和 X30：两个决定栈命运的寄存器 在 ARM64 的 31 个通用寄存器 (X0–X30) 中，有两个被架构约定赋予了特殊职责：\n寄存器 别名 职责 X29 FP (Frame Pointer) 帧指针，指向当前函数栈帧的底部 X30 LR (Link Register) 链接寄存器，保存函数的返回地址 X30 (LR) —— 函数怎么\u0026quot;记得\u0026quot;回哪去 当 CPU 执行 bl (Branch with Link) 指令调用一个函数时，硬件自动做两件事：\n1 2 bl \u0026lt;target\u0026gt; ⟹ ① 将下一条指令的地址写入 X30 (LR) ② 跳转到 \u0026lt;target\u0026gt; 执行 对应 ret 指令则做一件事：\n1 ret ⟹ 跳转到 X30 (LR) 中的地址继续执行 核心矛盾：如果函数 A 调用了函数 B，bl B 会把 A 的返回地址写入 X30。 然后 B 再调用 C 时，bl C 又会覆盖 X30。这样 A 的返回地址就丢了。\n解决方案：在函数入口，先将 X30 (LR) 保存到栈上，返回前再从栈上恢复。\nX29 (FP) —— 怎么找到\u0026quot;我\u0026quot;的栈帧 X29 指向当前函数栈帧的一个固定位置。它的核心作用有两个：\n为局部变量和临时数据提供基地址 —— 通过 [x29, #offset] 寻址 形成链表，支持栈回溯 (Stack backtrace) —— 每个栈帧中保存了上一个函数的 X29 (即上一个 FP)，从而形成一条\u0026quot;帧指针链\u0026quot; arm64 函数调用栈解析 CPU 执行指令的基本原理 在深入函数调用栈之前，有必要先了解 CPU 执行指令的基本机制。CPU 内部有多种不同功能的寄存器，其中与指令执行直接相关的有三个：\n寄存器 全称 作用 PC Program Counter（程序计数器） 存放下一条指令的内存地址。在 ARM64 中也称为 PC IR Instruction Register（指令寄存器） 存放当前正在执行的指令 SR Status Register（状态寄存器） 存储 CPU 当前的状态，如条件标志位（零标志 ZF、进位标志 CF 等）、中断禁止位、处理器模式标志等。在 ARM64 中称为 NZCV（Negative, Zero, Carry, Overflow） 此外，CPU 还拥有大量用于存储数据和地址的寄存器，根据用途可分为整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等。其中，有些寄存器既可以存放数据，又可以存放地址，被称为通用寄存器（GR，General Register）——ARM64 中的 x0–x30 均属此类，w0–w30 则用于访问它们的低 32 位。\n程序执行的基本循环（经典五级流水线 RISC 模型）如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ┌─────────────────────────┐ │ PC 指向下一条指令 │ └──────────┬──────────────┘ │ ▼ ① IF ┌─────────────────────────┐ 取指 │ 从指令存储器 / Cache 取指令 │ └──────────┬──────────────┘ │ ▼ ② ID ┌─────────────────────────┐ 译码/读寄存器 │ 操作控制器译码指令 │ │ + 从寄存器堆读取操作数 │ └──────────┬──────────────┘ │ ▼ ③ EX ┌─────────────────────────┐ 执行/计算地址│ ALU 执行运算 / 计算地址 │ └──────────┬──────────────┘ │ ▼ ④ MEM ┌─────────────────────────┐ 访存 │ 读/写 存储器（数据 Cache）│ └──────────┬──────────────┘ │ ▼ ⑤ WB ┌─────────────────────────┐ 写回 │ 将结果写回寄存器堆 │ └──────────┬──────────────┘ │ └──→ PC 自增 → 回到 ① 取指 IF（Instruction Fetch）：CPU 根据 PC 寄存器中的地址，从指令存储器或 Cache 中读取指令到 IR 寄存器。 译码/读寄存器 ID（Instruction Decode）：操作控制器对指令进行译码，同时从寄存器堆中读取操作数。 执行/计算地址 EX（Execute）：ALU 执行运算操作或计算内存地址。 访存 MEM（Memory Access）：对存储器（数据 Cache）进行读写操作。 写回 WB（Write Back）：将指令执行结果写回寄存器堆。然后 PC 自增，进入下一轮循环。 ARM64 特点：ARM64 是定长 32 位指令编码（所有指令均为 4 字节），因此 PC 每次自增 4。这与 x86 的变长编码不同，反汇编时的地址总是间隔 4——从后面的代码中可以清晰看到。\n这个\u0026quot;取指 → 译码/读寄存器 → 执行/计算地址 → 访存 → 写回 → PC 自增\u0026quot;的循环，是整个程序运行的底层根基。而函数调用栈正是在这个循环之上，通过 bl（Branch and Link，修改 PC + 保存返回地址到 x30）和 ret（跳转到 x30）等指令，实现控制流的跳转与回归。接下来的章节，我们将从进程的宏观地址空间开始，逐步深入到每一行汇编指令的微观世界。\n进程地址空间布局 在 Linux 中，一个运行中的进程拥有独立的虚拟地址空间（以 ARM64 为例，典型布局如下）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 高地址 ┌──────────────────────────────┐ │ 内核空间 │ │ (Kernel Space) │ ├──────────────────────────────┤ │ │ │ 栈 (Stack) │ ← SP 指向栈顶，向低地址增长 │ ↓ │ │ ↑ │ │ 共享库 / 映射区 │ │ │ │ 堆 (Heap) │ ← 向高地址增长 │ ↑ │ ├──────────────────────────────┤ │ 数据段 (Data / BSS) │ ├──────────────────────────────┤ │ 代码段 (Text) │ 低地址 └──────────────────────────────┘ 0x0 栈在高地址端，向低地址增长：每次函数调用（stp / sub sp, sp, #N）使得 SP（栈指针）减小。 堆向高地址增长：malloc/new 分配的内存从低往高走。 栈帧的结构 ARM64 调用约定 (AAPCS64) 下的典型栈帧布局如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 ← 高地址 (栈底方向) +---------------------------+ | 调用者 (Caller) 的栈帧 | +---------------------------+ | 调用者保存的 X29 (FP) | ← 当前栈帧的起始处 | 调用者保存的 X30 (LR) | +---------------------------+ | 被调用者保存的寄存器 | (如有: X19–X28) +---------------------------+ | 局部变量区 | +---------------------------+ | 参数构造区 (可选) | (用于传参给下一级) +---------------------------+ ← SP (栈指针) ↓ 低地址 (栈顶方向) 关键约束：ARM64 的栈是满递减 (full-descending) 的 —— SP 指向栈上最后一个已占用的位置，栈向低地址增长。分配空间时 SP 减小，释放时 SP 增大。SP 必须始终保持 16 字节对齐。\n函数序言 (Prologue) 与 尾声 (Epilogue) 标准模式（非叶子函数 — 会调用其他函数的函数） 序言 (Prologue)：\n1 2 stp x29, x30, [sp, #-N]! // ① 分配 N 字节栈空间，同时将 X29、X30 压栈 mov x29, sp // ② 设置当前帧指针 FP = SP stp (Store Pair) 是 ARM64 的成对存储指令 [sp, #-N]! 的 ! 表示先修改 SP (SP -= N)，再用新 SP 作为地址 这两条指令合起来：分配栈空间 + 保存 FP/LR + 建立新的帧指针，一步到位 尾声 (Epilogue)：\n1 2 ldp x29, x30, [sp], #N // ① 恢复 X29、X30，同时释放栈空间 ret // ② 跳回 X30 (LR) 中的返回地址 ldp (Load Pair) 的 [sp], #N 表示先从 SP 加载 X29/X30，再 SP += N 这两条指令合起来：恢复 FP/LR + 释放栈空间 + 返回 叶子函数模式（不调用其他函数的函数） 如果函数不调用其他函数（称为叶子函数 leaf function），它不需要保存 X29/X30：\n1 2 3 4 sub sp, sp, #0x20 // 只分配栈空间（给局部变量用） ... // 函数体 add sp, sp, #0x20 // 释放栈空间 ret // 返回 — X30 未被覆盖，仍有效 逐函数反汇编分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static int add(int a, int b) { int sum = a + b; return sum; } static int calc(int x, int y, int z) { int tmp = x * y; int result = add(tmp, z); return result; } static int process(int n) { int a = n * 2; int b = n + 3; int c = calc(a, b, n); return c; } static void print_result(int val) { /* 空函数体，仅用于展示多级调用链 */ (void)val; } int main(void) { int v = 42; int r = process(v); print_result(r); return 0; } 1 2 3 aarch64-linux-gnu-gcc -O0 -g -fno-omit-frame-pointer demo.c aarch64-linux-gnu-objdump -d aarch64-linux-gnu-objdump --dwarf=frames main —— 调用链的起点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 00000000000007d8 \u0026lt;main\u0026gt;: ;===== 序言 ===== 7d8: a9be7bfd stp x29, x30, [sp, #-32]! // SP -= 32, 保存[X29, X30]到栈上 7dc: 910003fd mov x29, sp // FP = SP (新栈帧基址) ;===== 函数体 ===== 7e0: 52800540 mov w0, #0x2a // w0 = 42 (参数 v) 7e4: b9001be0 str w0, [sp, #24] // 局部变量 v = 42 存到栈上 7e8: b9401be0 ldr w0, [sp, #24] // w0 = v (加载参数) 7ec: 97ffffe5 bl 780 \u0026lt;process\u0026gt; // LR = 0x7f0, 跳转到 process 7f0: b9001fe0 str w0, [sp, #28] // 局部变量 r = process(v) 的返回值 7f4: b9401fe0 ldr w0, [sp, #28] // w0 = r 7f8: 97fffff3 bl 7c4 \u0026lt;print_result\u0026gt; // LR = 0x7fc, 调用 print_result 7fc: 52800000 mov w0, #0x0 // return 0 ;===== 尾声 ===== 800: a8c27bfd ldp x29, x30, [sp], #32 // 恢复 X29/X30, 然后 SP += 32 804: d65f03c0 ret // 跳回 X30 (返回 _start) 关键观察：\nstp x29, x30, [sp, #-32]! 分配了 32 字节栈空间，并在底部保存了上一帧的 FP 和 LR mov x29, sp 让 X29 指向刚保存的 (旧 FP, 旧 LR) 这对数据 调用 process 前将参数放在 w0 中（ARM64 调用约定：前 8 个整数参数通过 X0–X7 传递） bl 将返回地址 0x7f0 存入 X30 后跳转 尾声的 ldp x29, x30, [sp], #32 对称地恢复并释放 process —— 中间层函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0000000000000780 \u0026lt;process\u0026gt;: ;===== 序言 ===== 780: a9bd7bfd stp x29, x30, [sp, #-48]! // SP -= 48, 保存 [X29, X30] 784: 910003fd mov x29, sp // FP = SP ;===== 函数体 ===== 788: b9001fe0 str w0, [sp, #28] // 存储参数 n 78c: b9401fe0 ldr w0, [sp, #28] 790: 531f7800 lsl w0, w0, #1 // a = n * 2 (左移1位) 794: b90027e0 str w0, [sp, #36] // 存储局部变量 a 798: b9401fe0 ldr w0, [sp, #28] 79c: 11000c00 add w0, w0, #0x3 // b = n + 3 7a0: b9002be0 str w0, [sp, #40] // 存储局部变量 b 7a4: b9401fe2 ldr w2, [sp, #28] // w2 = n (第三个参数) 7a8: b9402be1 ldr w1, [sp, #40] // w1 = b (第二个参数) 7ac: b94027e0 ldr w0, [sp, #36] // w0 = a (第一个参数) 7b0: 97ffffe4 bl 740 \u0026lt;calc\u0026gt; // LR = 0x7b4, 调用 calc(a, b, n) 7b4: b9002fe0 str w0, [sp, #44] // c = calc(...) 的返回值 7b8: b9402fe0 ldr w0, [sp, #44] // return c ;===== 尾声 ===== 7bc: a8c37bfd ldp x29, x30, [sp], #48 // 恢复 X29/X30, SP += 48 7c0: d65f03c0 ret // 跳回 LR 关键观察：\n分配了 48 字节栈空间（比 main 的 32 字节大），因为局部变量更多 它的序言覆盖了 X30（保存了返回 main 的地址），所以当它再调用 calc 时，X30 被 bl 覆盖不会造成问题 调用 calc 时传三个参数 (a, b, n)，分别放在 w0、w1、w2 calc —— 中间层函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0000000000000740 \u0026lt;calc\u0026gt;: ;===== 序言 ===== 740: a9bd7bfd stp x29, x30, [sp, #-48]! // SP -= 48, 保存 [X29, X30] 744: 910003fd mov x29, sp // FP = SP ;===== 函数体 ===== 748: b9001fe0 str w0, [sp, #28] // 存储参数 x 74c: b9001be1 str w1, [sp, #24] // 存储参数 y 750: b90017e2 str w2, [sp, #20] // 存储参数 z 754: b9401fe1 ldr w1, [sp, #28] 758: b9401be0 ldr w0, [sp, #24] 75c: 1b007c20 mul w0, w1, w0 // tmp = x * y 760: b9002be0 str w0, [sp, #40] // 存储 tmp 764: b94017e1 ldr w1, [sp, #20] // w1 = z 768: b9402be0 ldr w0, [sp, #40] // w0 = tmp 76c: 97ffffeb bl 718 \u0026lt;add\u0026gt; // LR = 0x770, 调用 add(tmp, z) 770: b9002fe0 str w0, [sp, #44] // result = add(...) 774: b9402fe0 ldr w0, [sp, #44] // return result ;===== 尾声 ===== 778: a8c37bfd ldp x29, x30, [sp], #48 // 恢复 X29/X30, SP += 48 77c: d65f03c0 ret // 跳回 LR add —— 叶子函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0000000000000718 \u0026lt;add\u0026gt;: ;===== 没有序言（叶子函数）===== 718: d10083ff sub sp, sp, #0x20 // 仅分配 32 字节（给局部变量） 71c: b9000fe0 str w0, [sp, #12] // 存储参数 a 720: b9000be1 str w1, [sp, #8] // 存储参数 b 724: b9400fe1 ldr w1, [sp, #12] 728: b9400be0 ldr w0, [sp, #8] 72c: 0b000020 add w0, w1, w0 // sum = a + b 730: b9001fe0 str w0, [sp, #28] // 存储 sum 734: b9401fe0 ldr w0, [sp, #28] // return sum ;===== 没有尾声（仅释放栈）===== 738: 910083ff add sp, sp, #0x20 // SP += 32 73c: d65f03c0 ret // 跳回 LR (仍然是 calc 中的 0x770) 关键区别：\n没有 stp x29, x30 —— 因为 add 不会再调用其他函数，X30 不会被覆盖 没有 mov x29, sp —— 不需要帧指针 只分配了局部变量所需的空间 (32 字节)，用完即释放 ret 时 X30 仍然完好地保存着 calc 中 bl 写入的返回地址 0x770 print_result —— 另一个叶子函数 1 2 3 4 5 6 00000000000007c4 \u0026lt;print_result\u0026gt;: 7c4: d10043ff sub sp, sp, #0x10 // 分配 16 字节 7c8: b9000fe0 str w0, [sp, #12] // 存参数 7cc: d503201f nop 7d0: 910043ff add sp, sp, #0x10 // 释放 16 字节 7d4: d65f03c0 ret // 跳回 LR (main 中的 0x7fc) 动手实验：用 GDB 验证调用栈 1 2 3 4 5 # 交叉编译时保留调试符号（已加 -g） aarch64-linux-gnu-gcc -O0 -g -o demo demo.c # 在 ARM64 环境（或 QEMU）中启动 GDB gdb demo 用 GDB 单步跟踪 demo.c 的调用过程，实地观察 X29/FP、X30/LR 和 SP 的变化。\n1 2 3 4 5 6 # 在 add 入口处断点 break add run # 查看完整调用链 backtrace 1 2 3 4 5 6 # 预期输出（从当前 add 开始向上追溯 4 层）： # #0 add (a=3780, b=42) at demo.c:7 # #1 0x0000aaaaaaaa0770 in calc (x=84, y=45, z=42) at demo.c:13 # #2 0x0000aaaaaaaa07b4 in process (n=42) at demo.c:20 # #3 0x0000aaaaaaaa07f0 in main (v=42) at demo.c:30 # #4 0x0000fffff7f3dxxx in __libc_start_main ... 1 2 # 查看当前帧的详细信息（含帧地址、返回地址、栈分配） info frame 1 2 3 4 5 6 # 预期输出关键行： # Frame at 0x........: ← 当前帧地址（即 add 的 SP + 32 后的值） # pc = 0x... in add # fp = 0x... ← X29 的值 # sp = 0x... ← 当前 SP # in call to function add 1 2 # 查看寄存器：FP (X29)、LR (X30)、SP、PC info registers x29 x30 sp pc 1 2 3 4 5 # 预期输出示例： # x29 0x........ ← 指向 add 栈帧底部（保存的 calc 的 FP/LR） # x30 0x........ ← 返回 calc 中 bl add 的下一条指令地址 # sp 0x........ ← 栈顶 # pc 0x........ ← 当前指令地址 1 2 # 以 8 字节为单位 dump 栈内存（从 SP 往上 48 字节——覆盖 add 整个帧） x/6gx $sp 1 2 3 4 5 # 预期输出（地址仅供参考）： # 0x........: 0x0000.... 0x0000.... ← add 的局部变量区 # 0x........: 0x0000.... 0x0000.... ← add 的局部变量区 # 0x........: 0x0000.... 0x0000.... ← [保存的 calc 的 X29] [保存的 calc 的 X30] # ↑ 这个 X29 指向 calc 的帧 ↑ 这个 X30 就是 add 的返回地址 1 2 3 4 5 # 单步执行一条指令（从 add 的第一条指令 sub sp, sp, #0x20 开始追踪） stepi # 每步都观察 SP / x29 / x30 的变化 info registers x29 x30 sp 关键观察点 执行到哪个位置 SP 变化 X29 (FP) X30 (LR) add 入口（刚 bl add 后） SP 指向调用者（calc）的栈内 指向 calc 的帧底 ==返回 calc 的地址== 执行 sub sp, sp, #0x20 后 SP -= 32，栈帧建立 不变 不变 执行 ret 前 不变 不变 仍为 calc 返回地址 ret 后回到 calc SP 回到 calc 帧内 不变 被 bl 下一指令覆盖 核心验证点：在 add 内部全程 X30 不变（因为 add 是叶子函数，不调用其他函数），这验证了 ARM64 叶子函数无需保存 LR\n调用过程中的栈变化全景 假设执行到 add 函数内部（刚执行完 sub sp, sp, #0x20），此时栈的完整布局如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 ┌────────────────────────────┐ ← 高地址 │ ... (其他数据) │ ├────────────────────────────┤ │ main 的局部变量区域 │ │ v = [SP+24] (值 42) │ │ r = [SP+28] (返回值) │ ├────────────────────────────┤ │ main 保存的 X29 (旧 FP) │ ← main 的 FP (X29) │ main 保存的 X30 (返回 LR) │ main 帧 ├────────────────────────────┤ │ process 的局部变量区域 │ │ n = [SP+28] (值 42) │ │ a = [SP+36] (值 84) │ │ b = [SP+40] (值 45) │ │ c = [SP+44] (返回值) │ ├────────────────────────────┤ │ process 保存的 X29 (main FP)│ ← process 的 FP (X29) │ process 保存的 X30 (main LR)│ process 帧 ├────────────────────────────┤ │ calc 的局部变量区域 │ │ z = [SP+20] (值 42) │ │ y = [SP+24] (值 45) │ │ x = [SP+28] (值 84) │ │ tmp= [SP+40] (值 3780) │ ← 84 × 45 │ res= [SP+44] (值 3822) │ ← 3780 + 42 ├────────────────────────────┤ │ calc 保存的 X29 (proc FP) │ ← calc 的 FP (X29) │ calc 保存的 X30 (proc LR) │ calc 帧 ├────────────────────────────┤ │ add 的局部变量区域 │ │ a = [SP+12] (值 3780) │ │ b = [SP+8] (值 42) │ │ sum= [SP+28] (值 3822) │ add 帧 ├────────────────────────────┤ ← SP (当前栈顶) └────────────────────────────┘ ← 低地址 帧指针链 (FP Chain) 从当前 add 向上追溯：\n1 2 3 add 的 X29 ──→ [calc 的帧中保存的 X29] ──→ [process 的帧中保存的 X29] ──→ [main 的帧中保存的 X29] ──→ 0 ↓ ↓ ↓ ↓ 指向 calc 的 FP 指向 process 的 FP 指向 main 的 FP 指向初始值 0 这正是栈回溯 (Stack Unwinding) 的物理基础。\n栈展开 (Stack Unwinding) 原理 栈展开（也叫栈回溯 / Stack Backtrace）就是从当前函数出发，沿着帧指针链逆向走回调用链的全过程。\n算法 对于一个非叶子函数的栈帧（已保存了 X29/X30 成对数据）：\n1 2 3 4 5 6 7 ┌─────────────────┐ FP (X29) ────────→ │ 旧 X29 (上一级 FP) │ ← 读出后赋值给 FP，向上跳一级 │ 旧 X30 (返回地址) │ ← 读出即得上一级的返回地址 ├─────────────────┤ │ 局部变量区 │ │ ... │ └─────────────────┘ 伪代码：\n1 2 3 4 5 6 7 8 9 void unwind(uint64_t fp, uint64_t lr) { while (fp != 0) { // 从栈帧中取出返回地址和上一级 FP uint64_t prev_fp = *(uint64_t*)(fp + 0); // 帧底 +0 处是旧 FP uint64_t prev_lr = *(uint64_t*)(fp + 8); // 帧底 +8 处是旧 LR printf(\u0026#34;LR = 0x%lx\\n\u0026#34;, prev_lr); // 打印返回地址 fp = prev_fp; // 上移一级 } } 库函数支持 __builtin_frame_address(level) — GCC/Clang 内建，获取第 N 级函数的 FP __builtin_return_address(level) — 获取第 N 级函数的返回地址 backtrace() / backtrace_symbols() — glibc 提供的高层栈回溯接口 实际调用链回溯 从 add 内部触发回溯，得到的地址链：\n级别 返回地址 (X30) 函数 0 0x770 add → calc 中的 bl add 之后 1 0x7b4 calc → process 中的 bl calc 之后 2 0x7f0 process → main 中的 bl process 之后 3 0x620+? main → _start / __libc_start_main 叶子函数与非叶子函数 特征 叶子函数 (Leaf) 非叶子函数 (Non-leaf) 保存 X29/X30? ❌ 不保存 ✅ stp x29, x30, ... 设置帧指针? ❌ ✅ mov x29, sp 栈帧固定大小? 是（仅局部变量） 是（含保存的寄存器） 栈展开支持? 通过调用者的帧 通过自己的 FP/LR 保存对 X30 是否被覆盖风险? 无风险（不调用） 有风险（调用前已保存） 以上是 -O0（无优化）的输出。开启优化后：\n1 2 -O0: sub sp, sp, #0x20 // 即使叶子函数也分配栈空间（给所有局部变量） -O2: // 可能完全不用栈，直接在寄存器中完成计算 例如 -O2 下 add 可能被内联 (inline) 到 calc 中，根本不会产生函数调用指令。\n总结 函数调用栈 = 由 FP (X29) 链串联起来的多个栈帧，每个栈帧保存了返回地址 (X30/LR) 和上一级 FP，形成一个可从当前函数回溯到 _start 的单向链表。\n# 要点 1 X30 (LR) 由硬件在 bl 时自动写入返回地址，ret 时自动跳回 2 X29 (FP) 由软件在函数入口设置为当前栈帧的基址，形成回溯链表 3 序言 (Prologue) 分配空间 + 保存 FP/LR + 建立新 FP 4 尾声 (Epilogue) 恢复 FP/LR + 释放空间 + ret 5 叶子函数不保存 FP/LR，因为 X30 安全 6 栈回溯 = 沿着 FP 链遍历，从每个帧中提取 LR 即得调用历史 7 ARM64 栈是满递减的，SP 保持 16 字节对齐 实践建议：在调试崩溃 (Segfault / Panic) 时，CPU 的当前 PC 加上 FP 链就可以还原出完整的调用堆栈。理解 X29/X30 的机制，就等于掌握了栈回溯的全部密码。\n对比分析：x86-64 与 ARM64 核心差异总览 维度 ARM64 (AArch64) x86-64 返回地址存放 专用寄存器 X30 (LR) 栈上（call 压栈，ret 弹栈） 帧指针 X29 (FP) RBP 调用指令 bl target — LR=下条地址，跳转 call target — 返回地址压栈，跳转 返回指令 ret — 跳转 X30 ret — 从栈顶弹出地址并跳转 序言范式 stp x29, x30, [sp,#-N]! / mov x29, sp push rbp / mov rsp, rbp / sub $N, rsp 尾声范式 ldp x29, x30, [sp], #N / ret leave（或 mov rsp, rbp; pop rbp）/ ret 叶子函数优化 可不保存 X29/X30 -O0 也会保存 RBP 参数传递 X0–X7（寄存器传参） RDI, RSI, RDX, RCX, R8, R9（寄存器传参） 栈方向 满递减 (Full-descending) 满递减 (Full-descending) 关键机制差异详解 差异一：返回地址放哪？ ARM64：返回地址放在 X30 (LR) 寄存器中。函数调用链的每一级都必须在自己被覆盖前把 LR 保存到栈上。\n1 2 // ARM64: bl 不碰栈，只写 X30 bl calc // X30 = 0x7f0, PC = calc x86-64：返回地址直接压到栈上。call 等价于 push 返回地址; jmp target。\n1 2 // x86-64: call 自动压栈 call calc // RSP -= 8, [RSP] = 返回地址, RIP = calc 这意味着 x86-64 的栈帧底部天然有返回地址，无需专门的寄存器来保存。\n差异二：帧指针的角色 ARM64：X29 (FP) 由序言 mov x29, sp 设定，指向刚保存的 (旧 FP, 旧 LR) 这对数据。\n1 2 FP ──→ [ 旧 FP | 旧 LR ] ← 连续 16 字节 ↑ +0 ↑ +8 x86-64：RBP 由序言 push rbp; mov rsp, rbp 设定。栈帧布局为：\n1 2 3 RBP ──→ [ 旧 RBP ] ← +0 [ 返回地址 ] ← +8 （由 call 压入） [ 局部变量区 ] ← 负偏移 重要区别：ARM64 的 FP 指向一个成对保存区（FP+LR 一起存），x86-64 的 RBP 指向单独保存的旧 RBP，而返回地址在 RBP+8。\n逐函数 x86-64 反汇编 add —— x86-64 的叶子函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0000000000001129 \u0026lt;add\u0026gt;: ;===== 序言 ===== 1129: f3 0f 1e fa endbr64 // CET (Control-flow Enforcement) 112d: 55 push %rbp // 保存旧 RBP 到栈上 112e: 48 89 e5 mov %rsp,%rbp // RBP = RSP（建立帧指针） ;===== 函数体 ===== 1131: 89 7d ec mov %edi,-0x14(%rbp) // 参数 a（来自 EDI）→ 栈上 1134: 89 75 e8 mov %esi,-0x18(%rbp) // 参数 b（来自 ESI）→ 栈上 1137: 8b 55 ec mov -0x14(%rbp),%edx // 加载 a 113a: 8b 45 e8 mov -0x18(%rbp),%eax // 加载 b 113d: 01 d0 add %edx,%eax // eax = a + b 113f: 89 45 fc mov %eax,-0x4(%rbp) // sum 存栈 1142: 8b 45 fc mov -0x4(%rbp),%eax // 返回值放入 EAX ;===== 尾声 ===== 1145: 5d pop %rbp // 恢复旧 RBP 1146: c3 ret // 从栈顶弹出返回地址 与 ARM64 对比 (add)：\n步骤 ARM64 x86-64 序言 sub sp, #0x20（仅腾空间） push rbp + mov rsp, rbp（还是保存了帧指针！） 原因 叶子函数：X30 安全，不保存 -O0 强制所有函数建立帧指针 尾声 add sp, #0x20 pop rbp 返回 ret（跳 X30） ret（弹栈顶地址） 关键观察：x86-64 在 -O0 下即使是叶子函数也执行 push rbp / pop rbp，这是因为 x86-64 的调试约定更依赖 RBP 链，而 ARM64 的 DWARF .eh_frame 表提供了另一种展开路径，所以叶子函数可以跳过 FP 保存。\ncalc —— x86-64 的非叶子函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0000000000001147 \u0026lt;calc\u0026gt;: ;===== 序言 ===== 1147: f3 0f 1e fa endbr64 114b: 55 push %rbp // 保存旧 RBP 114c: 48 89 e5 mov %rsp,%rbp // RBP = RSP 114f: 48 83 ec 20 sub $0x20,%rsp // 分配 32 字节局部变量空间 ;===== 函数体 ===== 1153: 89 7d ec mov %edi,-0x14(%rbp) // 存参数 x 1156: 89 75 e8 mov %esi,-0x18(%rbp) // 存参数 y 1159: 89 55 e4 mov %edx,-0x1c(%rbp) // 存参数 z 115c: 8b 45 ec mov -0x14(%rbp),%eax 115f: 0f af 45 e8 imul -0x18(%rbp),%eax // tmp = x * y 1163: 89 45 f8 mov %eax,-0x8(%rbp) // 存 tmp 1166: 8b 55 e4 mov -0x1c(%rbp),%edx // 准备参数 z 1169: 8b 45 f8 mov -0x8(%rbp),%eax // 准备参数 tmp 116c: 89 d6 mov %edx,%esi // 第二个参数 → ESI 116e: 89 c7 mov %eax,%edi // 第一个参数 → EDI 1170: e8 b4 ff ff ff call 1129 \u0026lt;add\u0026gt; // ① 压入返回地址 0x1175 // ② 跳转到 add 1175: 89 45 fc mov %eax,-0x4(%rbp) // res = add() 返回值 1178: 8b 45 fc mov -0x4(%rbp),%eax // return res ;===== 尾声 ===== 117b: c9 leave // 相当于: mov rsp, rbp; pop rbp 117c: c3 ret // 从栈顶弹出返回地址 与 ARM64 对比 (calc)：\n步骤 ARM64 x86-64 保存返回地址 隐式：stp x29, x30 一起存 显式：由 call 指令自动压栈 保存帧指针 和 LR 一起存在 stp 中 push rbp 单独压栈 分配空间 在 stp 中一并完成（sp,#-48!） 单独 sub $0x20, rsp 参数传递 X0=tmp, X1=z（先放好再 bl） EDI=tmp, ESI=z（System V 约定） 释放空间 ldp x29, x30, [sp], #48 一步完成 leave = mov rsp, rbp + pop rbp 两步 main —— x86-64 的入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 00000000000011c5 \u0026lt;main\u0026gt;: ;===== 序言 ===== 11c5: f3 0f 1e fa endbr64 11c9: 55 push %rbp 11ca: 48 89 e5 mov %rsp,%rbp 11cd: 48 83 ec 10 sub $0x10,%rsp // 分配 16 字节 ;===== 函数体 ===== 11d1: c7 45 f8 2a 00 00 00 movl $0x2a,-0x8(%rbp) // v = 42 11d8: 8b 45 f8 mov -0x8(%rbp),%eax // 加载 v 11db: 89 c7 mov %eax,%edi // 第一个参数 → EDI 11dd: e8 9b ff ff ff call 117d \u0026lt;process\u0026gt; // 压入 0x11e2 并跳转 11e2: 89 45 fc mov %eax,-0x4(%rbp) // r = process() 返回值 11e5: 8b 45 fc mov -0x4(%rbp),%eax 11e8: 89 c7 mov %eax,%edi 11ea: e8 c8 ff ff ff call 11b7 \u0026lt;print_result\u0026gt; 11ef: b8 00 00 00 00 mov $0x0,%eax // return 0 ;===== 尾声 ===== 11f4: c9 leave 11f5: c3 ret x86-64 栈内存布局（执行到 add 时） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ┌──────────────────────────┐ ← 高地址 │ ... │ ├──────────────────────────┤ │ main 的局部变量 │ │ v = [RBP-8] (值 42) │ │ r = [RBP-4] (返回值) │ ├──────────────────────────┤ │ main 的旧 RBP │ ← main 的 RBP │ main 的返回地址 │ ← 由 _start 的 call 压入 main 帧 ├──────────────────────────┤ │ process 的局部变量 │ │ n = [RBP-14] (值 42) │ │ a = [RBP-C] (值 84) │ │ b = [RBP-8] (值 45) │ │ c = [RBP-4] (返回值) │ ├──────────────────────────┤ │ process 的旧 RBP (main的)│ ← process 的 RBP │ process 的返回地址 │ ← main 中 `call process` 压入 process 帧 ├──────────────────────────┤ │ calc 的局部变量 │ │ z = [RBP-1C] (值 42) │ │ y = [RBP-18] (值 45) │ │ x = [RBP-14] (值 84) │ │ tmp=[RBP-8] (值 3780) │ │ res=[RBP-4] (值 3822) │ ├──────────────────────────┤ │ calc 的旧 RBP (process的)│ ← calc 的 RBP │ calc 的返回地址 │ ← process 中 `call calc` 压入 calc 帧 ├──────────────────────────┤ │ add 的局部变量 │ │ a = [RBP-14] (值 3780) │ │ b = [RBP-18] (值 42) │ │ sum=[RBP-4] (值 3822) │ ├──────────────────────────┤ │ add 的旧 RBP (calc的) │ ← add 的 RBP（也是当前 RBP） add 帧 │ add 的返回地址 │ ← calc 中 `call add` 压入 └──────────────────────────┘ ← RSP（栈顶） ← 低地址 帧指针链 (RBP Chain)：\n1 2 3 add 的 RBP ──→ [calc 帧中的旧 RBP] ──→ [process 帧中的旧 RBP] ──→ [main 帧中的旧 RBP] ──→ 0 ↓ ↓ ↓ ↓ 指向 calc 的 RBP 指向 process 的 RBP 指向 main 的 RBP 指向初始值 栈回溯：x86-64 版本 x86-64 的帧链遍历比 ARM64 多一个间接层：\n1 2 3 4 5 6 7 // ARM64: FP 指向 [旧FP | 旧LR] prev_fp = *(uint64_t*)(fp + 0); // +0 是旧 FP prev_lr = *(uint64_t*)(fp + 8); // +8 是旧 LR // x86-64: RBP 指向 [旧RBP | 返回地址在+8] prev_rbp = *(uint64_t*)(rbp); // +0 是旧 RBP ret_addr = *(uint64_t*)(rbp + 8); // +8 是返回地址（由 call 压入） 1 2 3 4 5 6 7 8 void unwind_x86(uint64_t rbp) { while (rbp != 0) { uint64_t prev_rbp = *(uint64_t*)rbp; // RBP+0 = 旧 RBP uint64_t ret_addr = *(uint64_t*)(rbp + 8); // RBP+8 = 返回地址 printf(\u0026#34;RIP = 0x%lx\\n\u0026#34;, ret_addr); rbp = prev_rbp; } } leave 指令揭秘 x86-64 的 leave 是 ARM64 中没有的指令，它等价于：\n1 2 leave ≡ mov rsp, rbp // RSP = RBP（丢弃局部变量空间） pop rbp // RBP = [RSP], RSP += 8（恢复旧 RBP） 对比 ARM64：\n1 2 3 4 5 6 7 // ARM64 尾声必须两条指令 ldp x29, x30, [sp], #48 // 同时恢复 FP、LR，释放空间 ret // 跳回 LR // x86-64 尾声也常是两条 leave // 恢复 RSP、RBP ret // 弹出返回地址并跳转 但结构完全不同：\nARM64 的 ldp 是加载成对寄存器 + 更新 SP（一条指令完成恢复+释放） x86-64 的 leave 是恢复 RSP 和 RBP，不碰返回地址 x86-64 的 ret 负责弹出返回地址 优化模式下的差异 场景 ARM64 x86-64 -O0 叶子函数 不保存 FP/LR 保存 RBP（push rbp） -O2 叶子函数 可能无栈操作 可能无栈操作 -fomit-frame-pointer 可用 strip 优化掉 FP 默认开启（省略 RBP） 无帧指针时栈回溯 依赖 .eh_frame (DWARF) 依赖 .eh_frame (DWARF) 关于 -fomit-frame-pointer：\n现代 Linux 上 x86-64 GCC 默认开启 -fomit-frame-pointer（优化级别 -O1 及以上），此时 RBP 作为通用寄存器使用，不再作为帧指针。栈回溯完全依赖 .eh_frame 中的 DWARF 展开描述。\n1 2 3 # 对比有无 frame pointer 的反汇编 $ gcc -O2 -fno-omit-frame-pointer -o with_fp demo.c $ gcc -O2 -fomit-frame-pointer -o without_fp demo.c 总结对比 特性 ARM64 (X29/X30) x86-64 (RBP/栈) 返回地址载体 X30 寄存器 栈内存（call 压入） 帧指针 X29（显式 mov x29, sp） RBP（push rbp; mov rsp, rbp） 序言原子性 stp 一条指令完成 保存+分配 分开：push + mov + sub 返回地址保护 函数序言主动保存 LR 到栈 call 已自动压栈，天然保护 叶子函数 可完全跳过序言/尾声 -O0 仍保留帧指针操作 栈回溯取址 帧底 +0→旧 FP，+8→旧 LR 帧底 +0→旧 RBP，+8→返回地址 特殊指令 stp / ldp 成对操作 leave 一步恢复 RSP+RBP 设计哲学 寄存器优先（减少内存访问） 栈优先（简化硬件设计） 一句话区别 ARM64 把返回地址放在寄存器 (X30/LR) 里，需要时再存到栈上；x86-64 让 call 指令直接把返回地址压到栈上。前者省内存但多一条保存指令，后者多一次内存写入但硬件更简单。\n实践建议：当你在 x86-64 上调试崩溃时，bt 命令的原理就是在 RBP 链上遍历 [rbp+0]（上一帧指针）和 [rbp+8]（返回地址）。而在 ARM64 上，则是沿着 X29 链读取 [fp+0]（旧 FP）和 [fp+8]（旧 LR）。两种架构殊途同归：一个链表，遍历即得全貌。\n","date":"2026-07-24T17:31:58+08:00","permalink":"https://charles-7777.github.io/p/x29-x30%E4%B8%8E%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E6%A0%88/","title":"X29 X30与函数调用栈"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int main() { int i = 0; int arr[3] = {0}; for(;i\u0026lt;=3;i++) { arr[i] = 0; printf(\u0026#34;Hello, World! \\n\u0026#34;); } return 0; } 循环条件 i \u0026lt;= 3 导致 i 取值 0, 1, 2, 3，而 arr[3] 只有合法索引 0, 1, 2。arr[3] = 0 是一次越界写入。\n1 2 3 4 5 6 7 8 # 无栈保护 gcc -O0 -fno-omit-frame-pointer -fno-stack-protector test.c -o ver_noprot # 有栈保护（演示 canary 检测） gcc -O0 -fno-omit-frame-pointer test.c -o ver_prot # AddressSanitizer（精准捕获越界） gcc -O0 -fsanitize=address test.c -o ver_asan objdump -d 分析汇编代码 无栈保护 (ver_noprot) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 0000000000001149 \u0026lt;main\u0026gt;: 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 83 ec 10 sub $0x10,%rsp ; 分配 16 字节 ; int i = 0 1155: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) ; i 在 rbp-4 ; int arr[3] = {0} 115c: 48 c7 45 f0 00 00 00 movq $0x0,-0x10(%rbp) ; arr[0..1] = 0 1163: 00 1164: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) ; arr[2] = 0 (rbp-8) 116b: eb 20 jmp 118d \u0026lt;main+0x44\u0026gt; ; 跳转到条件检查 ; 循环体 116d: 8b 45 fc mov -0x4(%rbp),%eax ; eax = i 1170: 48 98 cltq ; rax = i (符号扩展) 1172: c7 44 85 f0 00 00 00 movl $0x0,-0x10(%rbp,%rax,4) ; arr[i] = 0 ← 越界处 1179: 00 117a: 48 8d 05 83 0e 00 00 lea 0xe83(%rip),%rax ; \u0026#34;Hello, World!\u0026#34; 1181: 48 89 c7 mov %rax,%rdi 1184: e8 c7 fe ff ff call 1050 \u0026lt;puts@plt\u0026gt; 1189: 83 45 fc 01 addl $0x1,-0x4(%rbp) ; i++ ; 条件检查 118d: 83 7d fc 03 cmpl $0x3,-0x4(%rbp) ; i \u0026lt;= 3 ? 1191: 7e da jle 116d \u0026lt;main+0x24\u0026gt; ; 是则继续循环 1193: b8 00 00 00 00 mov $0x0,%eax 1198: c9 leave 1199: c3 ret ; 无 canary 检查！ 栈布局：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 高地址 ++ | 返回地址 | ← rbp+8 ++ | 保存的 %rbp | ← rbp+0 ++ | i (4 字节) | ← rbp-4 ← arr[3] 写入目标！ ++ | arr[2] (4 字节) | ← rbp-8 ++ | arr[1] (4 字节) | ← rbp-12 ++ | arr[0] (4 字节) | ← rbp-16 ++ 低地址 arr[0] 在 rbp-16，i 在 rbp-4，间距正好 12 字节 = 3 个 int。arr[3] = 0 写入 rbp-16+12 = rbp-4 恰好覆盖 i。\n有栈保护 (ver_prot) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 0000000000001169 \u0026lt;main\u0026gt;: 1169: f3 0f 1e fa endbr64 116d: 55 push %rbp 116e: 48 89 e5 mov %rsp,%rbp 1171: 48 83 ec 20 sub $0x20,%rsp ; 分配 32 字节（更大） ; canary 插入 1175: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ; 从 TLS 读 canary 117c: 00 00 117e: 48 89 45 f8 mov %rax,-0x8(%rbp) ; 存入 rbp-8 1182: 31 c0 xor %eax,%eax ; int i = 0 1184: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp) ; i 在 rbp-24 ; int arr[3] = {0} 118b: 48 c7 45 ec 00 00 00 movq $0x0,-0x14(%rbp) ; arr[0..1] = 0 (rbp-20) 1192: 00 1193: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp) ; arr[2] = 0 (rbp-12) 119a: eb 20 jmp 11bc \u0026lt;main+0x53\u0026gt; ; 循环体（同版本 A） 119c: 8b 45 e8 mov -0x18(%rbp),%eax 119f: 48 98 cltq 11a1: c7 44 85 ec 00 00 00 movl $0x0,-0x14(%rbp,%rax,4) ; arr[i] = 0 11a8: 00 11a9: 48 8d 05 54 0e 00 00 lea 0xe54(%rip),%rax 11b0: 48 89 c7 mov %rax,%rdi 11b3: e8 a8 fe ff ff call 1060 \u0026lt;puts@plt\u0026gt; 11b8: 83 45 e8 01 addl $0x1,-0x18(%rbp) ; i++ 11bc: 83 7d e8 03 cmpl $0x3,-0x18(%rbp) ; i \u0026lt;= 3 ? 11c0: 7e da jle 119c \u0026lt;main+0x33\u0026gt; ; canary 检查 11c2: b8 00 00 00 00 mov $0x0,%eax 11c7: 48 8b 55 f8 mov -0x8(%rbp),%rdx ; 加载 canary 11cb: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx ; 与原始值比较 11d2: 00 00 11d4: 74 05 je 11db \u0026lt;main+0x72\u0026gt; ; 一致则跳过 11d6: e8 95 fe ff ff call 1070 \u0026lt;__stack_chk_fail@plt\u0026gt; ; 不一致！ 11db: c9 leave 11dc: c3 ret 栈布局：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 高地址 ++ | 返回地址 | ← rbp+8 ++ | 保存的 %rbp | ← rbp+0 ++ | canary (8 字节) | ← rbp-8 ← arr[3] 写入目标！ ++ | arr[2] (4 字节) | ← rbp-12 ++ | arr[1] (4 字节) | ← rbp-16 (movq 的一部分) ++ | arr[0] (4 字节) | ← rbp-20 ++ | i (4 字节) | ← rbp-24 ++ 低地址 Canary（金丝雀值）被编译器插入在 arr[2] 和保存的 %rbp 之间（rbp-8）。arr[3] = 0 写入 rbp-20+12 = rbp-8 恰好覆盖 canary。\n运行时结果 无栈保护 (ver_noprot) 1 2 3 4 5 6 7 8 $ timeout 1 ./ver_noprot 2\u0026gt;\u0026amp;1 Hello, World! # 无限循环输出，1 秒后被 timeout 终止 Hello, World! Hello, World! Hello, World! ...（无数行） $ echo $? # 退出码 124 # 128 + SIGALRM = timeout 结果： 无限循环 ✅。每次 i 增长到 3 时，arr[3] = 0 把 i 重置为 0。\n有栈保护 (ver_prot) 1 2 3 4 5 6 7 8 9 $ stdbuf -oL ./ver_prot 2\u0026gt;\u0026amp;1 Hello, World! # i=0 Hello, World! # i=1 Hello, World! # i=2 Hello, World! # i=3 ← 此时 arr[3] = 0 覆盖了 canary *** stack smashing detected ***: terminated Aborted (core dumped) $ echo $? 134 # 128 + SIGABRT 结果： 打印 4 次后，__stack_chk_fail 检测到 canary 被破坏，调用 abort() 终止进程。\n⚠️ 注意： 重定向到文件时 stdout 变为全缓冲，abort 会跳过缓冲区刷新。需要使用 stdbuf -oL 强制行缓冲才能看到打印内容。\nAddressSanitizer (ver_asan) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ timeout 2 ./ver_asan 2\u0026gt;\u0026amp;1 ================================================================= ==24785==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffdb27aa1dc at pc 0x559838411395 WRITE of size 4 #0 0x559838411395 in main (ver_asan+0x1395) #1 0x7febe40bad8f in __libc_start_call_main Address 0x7ffdb27aa1dc is located in stack of thread T0 at offset 44 in frame #0 0x559838411258 in main This frame has 1 object(s): [32, 44) \u0026#39;arr\u0026#39; (line 6) Memory access at offset 44 overflows this variable Shadow bytes: =\u0026gt;0x1000364ed430: 00 00 00 00 00 00 f1 f1 f1 f1 00[04]f3 f3 00 00 ^^ ← 越界写入位置 SUMMARY: AddressSanitizer: stack-buffer-overflow (ver_asan+0x1395) in main ASan 解读：\n[32, 44) 'arr' — arr 占用字节偏移 32~43（12 字节） offset 44 — 写入位置在 arr 结束后的第 1 字节 WRITE of size 4 — 4 字节写入（一个 int），正好踩在 arr 边界外的第一个 int shadow 字节 00[04] — 00 表示可寻址，04 表示越界 4 字节 对比总结 | 方面 | 版本 A：无保护 | 版本 B：有保护 | 版本 C：ASan | | - |::|:\u0026ndash;:|:-:| | 栈分配 | sub $0x10 (16 B) | sub $0x20 (32 B) | 由 ASan 自动处理 | | Canary | ❌ 无 | ✅ 有 (mov %fs:0x28) | ❌ 无（ASan 替代） | | arr 基址 | rbp-16 | rbp-20 | 由 ASan 布局 | | i 位置 | rbp-4 | rbp-24 | 由 ASan 布局 | | arr[3] 覆盖 | i（循环变量） | canary（金丝雀） | ASan redzone | | 运行时行为 | 无限循环 | 4 次打印 + abort | 3 次打印 + 越界报告 | | 退出码 | 124 (timeout) | 134 (SIGABRT) | 1 (ASan abort) | | 防护生效时机 | 永不 | 函数返回前 | 越界即刻 |\n汇编差异详解 栈帧大小不同 1 2 3 4 5 ; 版本 A sub $0x10,%rsp ; 16 字节：12 (arr) + 4 (i) = 16，紧密排列 ; 版本 B sub $0x20,%rsp ; 32 字节：12 (arr) + 4 (i) + 8 (canary) + 对齐 栈保护不仅插入了 canary 检查代码，还改变了整个栈布局，增加了栈帧大小。\narr[i] = 0 的地址计算 两版均使用 -0x10(%rbp,%rax,4) 或 -0x14(%rbp,%rax,4) 的寻址模式——base + index*scale + displacement。当 i = 3 时：\n1 2 3 4 5 ; 版本 A：base = rbp-16, i=3 → rbp-16+12 = rbp-4 = \u0026amp;i ✓ 覆盖 i movl $0x0,-0x10(%rbp,%rax,4) ; 版本 B：base = rbp-20, i=3 → rbp-20+12 = rbp-8 = \u0026amp;canary ✓ 覆盖 canary movl $0x0,-0x14(%rbp,%rax,4) 函数返回路径 1 2 3 4 5 6 7 8 9 10 11 ; 无栈保护：简单返回 1198: c9 leave 1199: c3 ret ; 有栈保护：先检查 canary 11c7: mov -0x8(%rbp),%rdx 11cb: sub %fs:0x28,%rdx 11d4: je 11db \u0026lt;main+0x72\u0026gt; 11d6: call __stack_chk_fail@plt 11db: leave 11dc: ret 安全启示 \u0026lt;= 是数组遍历的天敌 — 始终用 i \u0026lt; size 而非 i \u0026lt;= size - 1 栈保护不是万能的 canary 只能检测溢出，不能阻止溢出 只在函数返回前检查——攻击者在返回前仍有机会造成破坏 信息泄露攻击可以读取 canary 值从而绕过 ","date":"2026-07-24T14:34:08+08:00","permalink":"https://charles-7777.github.io/p/%E6%95%B0%E7%BB%84%E8%B6%8A%E7%95%8C%E4%B8%8E%E6%A0%88%E6%BA%A2%E5%87%BA/","title":"数组越界与栈溢出"},{"content":" 基于 Linux 5.15 内核源码分析\nhttps://elixir.bootlin.com/linux/v5.15/source\n什么是 mmap？ mmap 是 Linux 系统的一种内存映射机制，允许将文件或设备直接映射到进程的虚拟地址空间。进程通过操作内存指针即可读写文件，无需调用 read() / write() 等系统调用。\nmmap 在 Linux 系统中广泛被使用：内存分配（malloc 大块）、零拷贝网络传输、进程间通信、大文件处理等。\nmmap 函数原型 1 2 3 #include \u0026lt;sys/mman.h\u0026gt; void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 参数说明：\n参数 类型 描述 addr void * 期望的映射起始地址，通常为 NULL（内核选择） length size_t 映射长度（字节） prot int 保护标志：PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE flags int 映射标志：MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED 等 fd int 文件描述符（文件映射）；匿名映射传 -1 offset off_t 文件偏移量（需页对齐） 返回值： 成功返回映射的虚拟地址，失败返回 MAP_FAILED（即 (void *)-1）。\n背景知识：物理内存管理 要理解 mmap 的内核实现，必须先理解两个基础：物理内存如何组织，以及虚拟地址如何翻译为物理地址。\n页（Page） Linux 内核将物理内存划分为 4KB 大小的单元，称为页（Page）。每个物理页由一个 struct page 描述（include/linux/mm_types.h:70）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct page { unsigned long flags; /* 页面的状态标志 */ atomic_t _count; /* 页面的引用计数 */ atomic_t _mapcount; /* 页面被映射的次数 */ union { struct { struct address_space *mapping; /* 页面所属的地址空间 */ pgoff_t index; /* 页面在文件中的偏移量 */ }; struct kmem_cache *slab; /* 如果页面属于 SLAB 缓存，指向缓存 */ struct page *next; /* 链表指针 */ }; void *virtual; /* 页面在内核空间的虚拟地址 */ /* ... */ }; arm64 四级页表 ARM64 架构（4K 页、48 位虚拟地址）使用四级页表映射，各层级全称如下：\n缩写 英文全称 中文含义 PGD Page Global Directory 页全局目录 PUD Page Upper Directory 页上级目录 PMD Page Middle Directory 页中间目录 PTE Page Table Entry 页表项 P4D Page 4th Directory 页四级目录（某些配置下被折叠） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 虚拟地址 VA[47:0] 分解（arm64, 4K页, 48位VA）： ┌────────────┬────────────┬────────────┬────────────┬────────────┐ │ PGD 索引 │ PUD 索引 │ PMD 索引 │ PTE 索引 │ 页内偏移 │ │ bits │ bits │ bits │ bits │ bits │ │ [47:39] │ [38:30] │ [29:21] │ [20:12] │ [11:0] │ ├────────────┼────────────┼────────────┼────────────┼────────────┤ │ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │ │ 512 项 │ 512 项 │ 512 项 │ 512 项 │ 4KB 页 │ │ 每项 8B │ 每项 8B │ 每项 8B │ 每项 8B │ │ └─────┬──────┴─────┬──────┴─────┬──────┴─────┬──────┴────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────────┐ │ PGD │→ │ PUD │→ │ PMD │→ │ PTE │→ │ 物理页 │ │ 页表 │ │ 页表 │ │ 页表 │ │ 页表 │ │ (4KB) │ └────────┘ └────────┘ └────────┘ └────────┘ └──────────────┘ ↑ ↑ ↑ ↑ CR3/PGDIR PGD 指向 PUD 指向 PMD 指向 物理地址 PUD 表 PMD 表 PTE 表 页表层级常量（arch/arm64/include/asm/pgtable-hwdef.h:41 及之后）：\n层级 英文全称 位移宏 值（4K页, 48位VA） 每条目覆盖范围 PGD Page Global Directory PGDIR_SHIFT 39 512 GB PUD Page Upper Directory PUD_SHIFT 30 1 GB PMD Page Middle Directory PMD_SHIFT 21 2 MB PTE Page Table Entry PAGE_SHIFT 12 4 KB ⚠️ 注意：表中 \u0026ldquo;512 GB\u0026rdquo; 是 **PGD 的一个条目（entry）**所覆盖的地址范围。PGD 页表共有 PTRS_PER_PGD = 512 个条目（9 位索引），因此 PGD 整张表可覆盖的总地址空间为 512 × 512 GB = 256 TB。在 arm64 48 位 VA 下，用户空间大小 TASK_SIZE_64 = (1UL \u0026lt;\u0026lt; vabits_actual) = 1UL \u0026lt;\u0026lt; 48 = 256 TB（arch/arm64/include/asm/processor.h:53），即整个 lower 48-bit 范围。一个进程能占用的虚拟地址空间受 TASK_SIZE 和物理内存/swap 总量的双重限制。\n页表项数：每级页表 PTRS_PER_PTE = 1 \u0026lt;\u0026lt; (PAGE_SHIFT - 3) = 512 项。\n页表类型定义（arch/arm64/include/asm/pgtable-types.h）：\n1 2 3 4 typedef struct { pteval_t pte; } pte_t; /* PTE — Page Table Entry */ typedef struct { pmdval_t pmd; } pmd_t; /* PMD — Page Middle Directory */ typedef struct { pudval_t pud; } pud_t; /* PUD — Page Upper Directory */ typedef struct { pgdval_t pgd; } pgd_t; /* PGD — Page Global Directory */ 虚拟地址 → 物理地址映射过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 虚拟地址映射物理地址的过程（四级页表）： 进程 PGD 基地址记录在 task_struct-\u0026gt;mm-\u0026gt;pgd CPU 选中进程后 → 页表基地址寄存器设为该值 步骤1: PGD[VA[47:39]] → 得到 PUD 页表物理基地址 步骤2: PUD[VA[38:30]] → 得到 PMD 页表物理基地址 步骤3: PMD[VA[29:21]] → 得到 PTE 页表物理基地址 步骤4: PTE[VA[20:12]] → 得到物理页基地址 步骤5: 物理页基地址 + VA[11:0] → 最终物理地址 ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌─────────┐ │ PGD │ ──→ │ PUD │ ──→ │ PMD │ ──→ │ PTE │ ──→ │ 物理页 │ │[idx0]│ │[idx1]│ │[idx2]│ │[idx3]│ │ + offset│ └──────┘ └──────┘ └──────┘ └──────┘ └─────────┘ 9位索引 9位索引 9位索引 9位索引 12位偏移 进程虚拟地址空间布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 完整的进程虚拟地址空间 (arm64 48位VA, 4K页)： ┈┈┈ 从低地址到高地址 ┈┈┈ ┌──────────────────────────────┐ 0x0000_0000_0000_0000 │ 代码段 (Text) │ ← 每个进程独立 ├──────────────────────────────┤ │ 数据段 (Data) │ ← 已初始化全局变量（进程私有） ├──────────────────────────────┤ │ BSS 段 │ ← 未初始化全局变量 ├──────────────────────────────┤ │ 堆 (Heap) — brk/sbrk │ ↓ brk 增大（地址增大） ├──────────────────────────────┤ │ │ │ │ 文件映射和匿名映射区 │ ← mmap 在此分配 │ │ │ ├──────────────────────────────┤ │ 栈 (Stack) — 向低地址增长 │ ↑ rsp 减小（地址减小） ├──────────────────────────────┤ │ [vdso] / [vvar] │ ← 内核映射到用户空间的全局代码 ├══════════════════════════════┤ 0x0000_FFFF_FFFF_FFFF ← 用户空间结束 │ 内核空间 │ │ ┌──────────────────────────┐│ ← 全局共享，所有进程相同 │ │ kernel logical map ││ 线性映射，直接访问物理内存 │ ├──────────────────────────┤│ │ │ [kasan shadow] 32TB ││ KASAN 影子内存 │ ├──────────────────────────┤│ │ │ bpf jit 128MB ││ BPF JIT 编译区 │ ├──────────────────────────┤│ │ │ modules 128MB ││ 驱动模块 │ ├──────────────────────────┤│ │ │ vmalloc 124TB ││ vmalloc() 分配 │ ├──────────────────────────┤│ │ │ [guard] + PCI I/O ││ 固定映射 / I/O 空间 │ ├──────────────────────────┤│ │ │ vmemmap 2TB ││ struct page 数组 │ ├──────────────────────────┤│ │ │ [guard region] 2TB ││ │ └──────────────────────────┘│ ffff_FFFF_FFFF_FFFF └──────────────────────────────┘ arm64 内核地址空间完整分区表（从低到高排列）： linux/Documentation/arm64/memory.rst\nStart End Size 用途 0000000000000000 0000ffffffffffff 256 TB user（用户空间） ffff000000000000 ffff7fffffffffff 128 TB kernel logical memory map（内核线性映射，直接访问所有物理内存） ffff600000000000 ffff7fffffffffff 32 TB [kasan shadow region]（KASAN 影子内存） ffff800000000000 ffff800007ffffff 128 MB bpf jit region（BPF JIT 编译区） ffff800008000000 ffff80000fffffff 128 MB modules（模块加载区） ffff800010000000 fffffbffefffffff 124 TB vmalloc（vmalloc/ioremap 分配区） fffffbfff0000000 fffffbfffdffffff 224 MB fixed mappings（固定映射，top-down 分配） fffffbfffe000000 fffffbfffe7fffff 8 MB [guard region] fffffbfffe800000 fffffbffff7fffff 16 MB PCI I/O space（PCI IO 空间） fffffbffff800000 fffffbffffffffff 8 MB [guard region] fffffc0000000000 fffffdffffffffff 2 TB vmemmap（struct page 数组映射） fffffe0000000000 ffffffffffffffff 2 TB [guard region] 表中 \u0026ldquo;kernel logical memory map\u0026rdquo;（内核线性映射区）是内核访问物理内存的窗口：virt = phys + PAGE_OFFSET，128 TB 空间足以映射当前所有物理内存。vmalloc 用于非连续内存分配，vmemmap 用于存储每个物理页对应的 struct page。\n关键分区说明:\n· 用户空间 (0000_0000_0000_0000 ~ 0000_FFFF_FFFF_FFFF)(边界由 TASK_SIZE 定义arch/arm64/include/asm/processor.h) 每个进程独立，通过切换页表基地址寄存器隔离\n· 内核空间 (FFFF_0000_0000_0000 ~ FFFF_FFFF_FFFF_FFFF) 所有进程全局共享，上下文切换时内核页表部分不变\n· 全局变量 (.data / .bss) 位于用户空间数据段，是每个进程私有的副本（写时复制继承）。\n内核线性映射（kernel logical memory map）才是真正全局共享的物理内存访问通道。\n· [vdso] / [vvar] 内核映射到每个用户空间的\u0026quot;全局\u0026quot;代码页，加速 gettimeofday()， 所有进程看到的内容相同（映射位置可能因 ASLR 而异）\nmmap 内核实现原理 有了背景知识，现在来看 mmap 在内核中是如何一步步完成的。\n系统调用入口 arm64 入口代码 (arch/arm64/kernel/sys.c:21-29)：\n1 2 3 4 5 6 7 8 SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len, unsigned long, prot, unsigned long, flags, unsigned long, fd, unsigned long, off) { if (offset_in_page(off) != 0) return -EINVAL; return ksys_mmap_pgoff(addr, len, prot, flags, fd, off \u0026gt;\u0026gt; PAGE_SHIFT); } 先看 mmap 的完整调用链：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 用户态 mmap() │ ▼ ┌──────────────────────────────────────────────────────┐ │ arch/arm64/kernel/sys.c:21 SYSCALL_DEFINE6(mmap) │ ← 架构入口 │ 检查 offset 页对齐, off \u0026gt;\u0026gt; PAGE_SHIFT │ └──────────────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ mm/mmap.c:1583 ksys_mmap_pgoff() │ ← 内核通用 │ 解析 fd → struct file *，处理 hugepage │ └──────────────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ mm/util.c:506 vm_mmap_pgoff() │ ← 加锁审计 │ security_mmap_file() LSM 安全检查 │ │ mmap_write_lock_killable(mm) 获取写锁 │ └──────────────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ mm/mmap.c:1404 do_mmap() │ ← 核心创建 │ 地址选择、权限检查、vm_flags 计算 │ └──────────────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ mm/mmap.c:1716 mmap_region() │ ← VMA 安装 │ 分配 vm_area_struct、设置 vm_ops、插入红黑树 │ └──────────────────────────────────────────────────────┘ │ ▼ mm_populate() (若 MAP_POPULATE 或 VM_LOCKED) do_mmap() 处理流程 do_mmap() (mm/mmap.c:1404-1581) 是 mmap 的核心函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 do_mmap(file, addr, len, prot, flags, pgoff, \u0026amp;populate, \u0026amp;uf) │ ▼ ┌─────────────────────┐ │ 参数合法性检查 │ ← len==0 → EINVAL; 地址对齐; 溢出 └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ get_unmapped_area() │ ← 查找或验证可用虚拟地址 └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ 合成 vm_flags │ ← calc_vm_prot_bits + calc_vm_flag_bits └─────────┬───────────┘ │ ▼ ┌──────────────┴──────────────┐ │ │ file != NULL file == NULL (文件映射) (匿名映射) │ │ ▼ ▼ ┌──────────────────┐ ┌───────────────────────┐ │ · 检查文件权限 │ │ · MAP_SHARED: pgoff=0 │ │ · 验证 f_op-\u0026gt;mmap │ │ · MAP_PRIVATE: │ │ · MAP_SHARED │ │ pgoff=addr\u0026gt;\u0026gt;PSHIFT │ │ 需写权限 │ └──────────┬────────────┘ └────────┬─────────┘ │ │ │ └──────────┬─────────────────┘ │ ▼ ┌─────────────────────┐ │ mmap_region() │ ← 真正的 VMA 创建 └─────────────────────┘ mmap_region() — VMA 安装 mmap_region() (mm/mmap.c:1716-1871) 将 VMA 真正安装到进程地址空间：\n步骤 代码位置 说明 地址空间检查 may_expand_vm() 检查进程 RLIMIT_AS 限制 解除旧映射 munmap_vma_range() 如果有旧映射先解除 尝试合并 VMA vma_merge() 与相邻 VMA 合并 分配 VMA 结构体 vm_area_alloc() 分配 struct vm_area_struct 文件映射 call_mmap(file, vma) 调用 f_op-\u0026gt;mmap() 设置 vm_ops 匿名共享映射 shmem_zero_setup(vma) 创建 shmem 伪文件 匿名私有映射 vma_set_anonymous(vma) vm_ops = NULL 插入树/链表 vma_link() 插入红黑树和双向链表 统计 vm_stat_account() 更新内存统计 内核页表映射函数调用链 mmap 建立地址映射的最终落脚点是填充页表。ARM64 的页表填充通过 arch/arm64/mm/mmu.c 中的函数链完成：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __create_pgd_mapping(pgdir, phys, virt, size, prot, alloc, flags) ← 第 363 行 │ 遍历 PGD 条目 (按 pgd_addr_end 分段) │ └─ alloc_init_pud() ← 第 308 行 │ p4d_offset() 访问 P4D → __p4d_populate() 分配 PUD 表 │ 尝试 1GB 大块映射 (pud_set_huge) │ └─ alloc_init_cont_pmd() ← 第 255 行 │ __pud_populate() 分配 PMD 表 │ └─ init_pmd() ← 第 218 行 │ pmd_set_fixmap_offset() 定位 PMD 条目 │ 尝试 2MB 大块映射 (pmd_set_huge) │ └─ alloc_init_cont_pte() ← 第 179 行 │ __pmd_populate() 分配 PTE 表 │ └─ init_pte() ← 第 155 行 │ pte_set_fixmap_offset() 定位 PTE 条目 │ set_pte() 写入页表项 │ 逐页: pfn_pte(__phys_to_pfn(phys), prot) 文件映射 vs 匿名映射 内核通过 vm_ops 函数指针表实现多态机制，三种映射方式在 mmap_region() 中分支：\n特性 文件映射 匿名共享 匿名私有 vm_file 真实文件 struct file * shmem 伪文件 NULL vm_ops 文件系统设置（如 ext4_file_vm_ops） \u0026amp;shmem_vm_ops NULL 设置位置 call_mmap() + f_op-\u0026gt;mmap() shmem_zero_setup() vma_set_anonymous() 缺页处理 vma-\u0026gt;vm_ops-\u0026gt;fault() → 从磁盘读取 shmem_fault() → 共享内存 do_anonymous_page() → 零填充页 两种映射没有本质区别——区别仅在于映射的目的物理页不同：\n文件映射：映射的物理地址指向文件的页缓存（page cache）。Linux 将磁盘文件预读取至页缓存，访问页缓存等同于访问磁盘文件。 匿名映射：映射的物理地址指向匿名页（由 do_anonymous_page() 分配的零填充页，或 shmem 管理的共享页）。 1 2 3 4 5 6 7 8 文件映射原理： 进程虚拟地址 页缓存 磁盘文件 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 虚拟地址 │ ──页表──→ │ 物理页 │ ──回写→ │ 文件数据 │ │ (VMA) │ │ (Page) │ ←预读取─ │ on │ └──────────┘ └──────────┘ │ Disk │ └──────────┘ mmap demo 以下示例展示 mmap 的三种典型用法，并结合 readelf、objdump、/proc/ 文件系统逐一分析。\n示例程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 /* * mmap_demo.c — 演示 mmap 三种典型用法 * 编译: gcc -O2 -o mmap_demo mmap_demo.c */ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;sys/mman.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; int main(int argc, char *argv[]) { /* ─── 1. 匿名私有映射 (分配大块内存) ─── */ size_t anon_len = 4096 * 10; /* 10 页 */ void *anon = mmap(NULL, anon_len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (anon == MAP_FAILED) { perror(\u0026#34;匿名映射失败\u0026#34;); exit(1); } strcpy((char *)anon, \u0026#34;Hello from anonymous mmap!\u0026#34;); printf(\u0026#34;[匿名映射] 地址: %p, 长度: %zu bytes, 内容: %s\\n\u0026#34;, anon, anon_len, (char *)anon); printf(\u0026#34; 按 ENTER 继续...\u0026#34;); getchar(); /* ─── 2. 文件映射 (读/写文件) ─── */ int fd = open(\u0026#34;/tmp/mmap_test.txt\u0026#34;, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd \u0026lt; 0) { perror(\u0026#34;open\u0026#34;); exit(1); } /* 先准备好文件内容 */ const char *msg = \u0026#34;mmap file I/O demo\\n\u0026#34;; write(fd, msg, strlen(msg) + 1); size_t file_len = 4096; /* 映射 1 页 */ void *file_map = mmap(NULL, file_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (file_map == MAP_FAILED) { perror(\u0026#34;文件映射失败\u0026#34;); exit(1); } close(fd); /* 映射后可以关闭 fd */ printf(\u0026#34;[文件映射] 地址: %p, 长度: %zu\\n\u0026#34;, file_map, file_len); printf(\u0026#34; 文件内容: %s\\n\u0026#34;, (char *)file_map); /* 通过内存修改文件内容 */ memcpy(file_map, \u0026#34;MODIFIED via mmap!\u0026#34;, 19); printf(\u0026#34; 修改后, 查看文件: $ cat /tmp/mmap_test.txt\\n\u0026#34;); printf(\u0026#34; 按 ENTER 继续...\u0026#34;); getchar(); /* ─── 3. 匿名共享映射 (IPC 模拟) ─── */ size_t shm_len = 4096; void *shared = mmap(NULL, shm_len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) { perror(\u0026#34;共享映射失败\u0026#34;); exit(1); } sprintf((char *)shared, \u0026#34;PID=%d shared data\u0026#34;, getpid()); printf(\u0026#34;[共享映射] 地址: %p, 内容: %s\\n\u0026#34;, shared, (char *)shared); /* ─── 暂停, 便于观察 /proc/pid/maps ─── */ printf(\u0026#34;\\n查看进程内存映射: $ cat /proc/%d/maps\\n\u0026#34;, getpid()); printf(\u0026#34;按 ENTER 退出清理...\u0026#34;); getchar(); munmap(anon, anon_len); munmap(file_map, file_len); munmap(shared, shm_len); return 0; } 编译与运行 1 2 $ gcc -O0 -o mmap_demo mmap_demo.c $ ./mmap_demo 使用 readelf 分析 ELF 布局 先看编译后的程序本身有哪些内存区域会被内核加载：\n1 $ readelf -S mmap_demo # 查看节区表 (Section Headers) 关键输出片段：\n1 2 3 4 5 6 7 8 9 10 11 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [13] .text PROGBITS 0000000000001060 00001060 0000000000000b55 0000000000000000 AX 0 0 16 [16] .rodata PROGBITS 0000000000002000 00002000 0000000000000208 0000000000000000 A 0 0 8 [24] .data PROGBITS 0000000000004020 00003020 0000000000000108 0000000000000000 WA 0 0 8 [25] .bss NOBITS 0000000000004140 00003128 0000000000000028 0000000000000000 WA 0 0 8 节区 地址 文件偏移 说明 .text 0x1060 0x1060 代码段，AX（可分配+可执行） .rodata 0x2000 0x2000 只读数据（字符串常量），A .data 0x4020 0x3020 已初始化全局数据，WA（可写） .bss 0x4140 — 未初始化全局数据（不占文件空间） 再用 readelf -l 查看程序头（决定内核如何加载）：\n1 $ readelf -l mmap_demo 1 2 3 4 5 6 7 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000208 0x000208 R 0x8 LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000878 0x000878 R 0x1000 LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x00bb5 0x00bb5 R E 0x1000 LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000208 0x000208 R 0x1000 LOAD 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000258 0x000390 RW 0x1000 每个 LOAD 段对应内核加载时需要创建的一个 VMA：\n段 文件偏移 虚拟地址 文件大小 内存大小 标志 加载内容 LOAD #1 0x000000 0x000000 0x878 0x878 R ELF header, 只读 LOAD #2 0x001000 0x001000 0xbb5 0xbb5 R E .text 代码段 LOAD #3 0x002000 0x002000 0x208 0x208 R .rodata LOAD #4 0x002dd8 0x003dd8 0x258 0x390 RW .data + .bss 注意第 4 个段：MemSiz \u0026gt; FileSiz，多出的部分就是 .bss（内核启动时清零）。\n使用 objdump 反汇编 mmap 调用 反汇编 mmap 在 PLT 中的跳转和实际调用：\n1 $ objdump -d mmap_demo | grep -A 20 \u0026#39;\u0026lt;main\u0026gt;\u0026#39; 关注 call 指令后的函数调用关系：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0000000000001200 \u0026lt;main\u0026gt;: 1200: push %rbp 1201: mov %rsp,%rbp 1204: ... # 准备栈帧 # 匿名映射: mmap(NULL, 40960, PROT_READ|PROT_WRITE, # MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 1258: mov $0xffffffff,%r9d # offset = -1 125e: mov $0x22,%r8d # flags = MAP_PRIVATE|MAP_ANONYMOUS 1264: mov $0x3,%ecx # prot = PROT_READ|PROT_WRITE 1269: mov $0xa000,%edx # length = 40960 126e: xor %esi,%esi # addr = NULL 1270: xor %edi,%edi 1272: call \u0026lt;mmap@plt\u0026gt; # → PLT → glibc → syscall ... mmap@plt 会跳转到 glibc 的 mmap 包装函数，最终执行 syscall 指令陷入内核。\n通过 /proc/ 观察运行时的内存映射 运行程序并在暂停时查看 /proc/\u0026lt;pid\u0026gt;/maps：\n1 $ cat /proc/`pgrep mmap_demo`/maps 输出示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 5566c1a4d000-5566c1a4e000 r--p 00000000 08:01 2883744 mmap_demo # ELF 头(LOAD #1) 5566c1a4e000-5566c1a4f000 r-xp 00001000 08:01 2883744 mmap_demo # .text (LOAD #2) 5566c1a4f000-5566c1a50000 r--p 00002000 08:01 2883744 mmap_demo # .rodata (LOAD #3) 5566c1a50000-5566c1a51000 r--p 00003000 08:01 2883744 mmap_demo # .data relro 5566c1a51000-5566c1a52000 rw-p 00003000 08:01 2883744 mmap_demo # .data + .bss (LOAD #4) 5566c1b7d000-5566c1b7e000 rw-p 00000000 00:00 0 [heap] # 堆 7f0e8db8c000-7f0e8db9d000 rw-p 00000000 00:00 0 [anon] # ← 匿名映射 (10页) 7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt # ← 文件映射 7f0e8db9e000-7f0e8db9f000 rw-s 00000000 00:00 0 [anon] # ← 匿名共享映射 ... 7ffe96e50000-7ffe96e71000 rw-p 00000000 00:00 0 [stack] # 栈 7ffe96eef000-7ffe96ef3000 r--p 00000000 00:00 0 [vvar] 7ffe96ef3000-7ffe96ef5000 r-xp 00000000 00:00 0 [vdso] # 内核提供的 VDSO 每个字段的含义（以文件映射行为例）：\n1 2 3 4 5 6 7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt ├────── VMA 范围 ──────┤ ├┤ ├─offset─┤ ├──┼──┼─├──── inode ────┤ │ 起始地址-结束地址 ││ 文件偏移 设备 inode 文件名 ││ │└─ s = MAP_SHARED / p = MAP_PRIVATE └── 权限: r=读 w=写 x=执行 再看更详细的 /proc/\u0026lt;pid\u0026gt;/smaps：\n1 $ cat /proc/`pgrep mmap_demo`/smaps | grep -A 10 \u0026#34;7f0e8db9d000\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt Size: 4 kB # VMA 大小 KernelPageSize: 4 kB # 内核页大小 Rss: 4 kB # 实际驻留物理内存 Pss: 4 kB # 按共享比例分摊的物理内存 Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 4 kB # 私有脏页 Referenced: 4 kB Anonymous: 0 kB VmFlags: rd wr sh mr mw me ms # VMA 标志 综合分析对照表 将 ELF 静态布局、内核加载、运行时 /proc/ 观测三方面对应起来：\nELF 节区 (readelf) VMA 起始 (maps) 权限 文件偏移 说明 .text (LOAD #2) 0x5566c1a4e000 r-xp 0x1000 代码段，readelf 地址+偏移一致 .rodata (LOAD #3) 0x5566c1a4f000 r\u0026ndash;p 0x2000 只读数据 .data + .bss (LOAD #4) 0x5566c1a51000 rw-p 0x3000 可读写数据，MemSiz \u0026gt; FileSiz 匿名 mmap 0x7f0e8db8c000 rw-p 0x00 MAP_ANONYMOUS, fd=-1 文件 mmap 0x7f0e8db9d000 rw-s 0x00 MAP_SHARED, fd=/tmp/... 共享匿名 mmap 0x7f0e8db9e000 rw-s 0x00 `MAP_SHARED 关键观察 匿名映射无后备文件：/proc/pid/maps 中显示 [anon] 或 [heap]，无文件名 文件映射关联 inode：/proc/ 输出中可见设备号和 inode，与 ls -i 一致 MAP_SHARED vs MAP_PRIVATE：权限字段末位 s vs p，决定写时复制行为 文件偏移来自 ELF LOAD 段：.text 段在文件偏移 0x1000，映射后 VMA 起始也看到偏移 0x1000 缺页按需填充：smaps 中 Rss \u0026lt; Size（未访问的页尚未分配物理页） 关键源码文件索引 文件 内容 arch/arm64/kernel/sys.c:21 arm64 mmap 系统调用入口 mm/mmap.c:1404 do_mmap() 核心映射函数 mm/mmap.c:1583 ksys_mmap_pgoff() 内核通用入口 mm/mmap.c:1716 mmap_region() VMA 安装 mm/util.c:506 vm_mmap_pgoff() 加锁审计 arch/arm64/mm/mmu.c:363 __create_pgd_mapping() 页表创建 arch/arm64/include/asm/pgtable-types.h:23 页表类型（pte_t/pmd_t/pud_t/pgd_t） arch/arm64/include/asm/pgtable-hwdef.h:41 页表位移常量 include/linux/mm_types.h:319 struct vm_area_struct include/linux/mm_types.h:70 struct page include/linux/mm.h:588 struct vm_operations_struct 一张图搞懂mmap实现原理\n","date":"2026-07-19T23:56:01+08:00","permalink":"https://charles-7777.github.io/p/mmap-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/","title":"mmap 系统调用原理解析"},{"content":" 上一篇【linux trace \u0026ndash; ptrace strace ltrace ftrace 】 简要学习了ptrace strace ltrace ftrace， 本篇学习一下perf\nperf(1) — Linux manual page\nLinux perf Examples\u0026mdash;-brendangregg\nhttps://elixir.bootlin.com/linux/v5.15/source/tools/perf 内核源码: tools/perf/ (用户态工具) + kernel/events/ (内核子系统)\nperf-简介\n深入探索 perf CPU Profiling 实现原理\n一文看懂Linux性能分析｜perf源码实现\nperfwiki\n概述 perf 全称 performance counters.Performance counters for Linux are a new kernel-based subsystem that provide a framework for all things performance analysis. It covers hardware level (CPU/PMU, Performance Monitoring Unit) features and software features (software counters, tracepoints) as well. 源码位于Linux 源码中：包括用户态工具（tools/perf）和 内核子系统（kernel/events/）,还有arch/arm64/kernel/perf_xxx.c 和driver/perf/\nperf 是 Linux 内核性能事件子系统（perf_event）的用户态工具集，利用 PMU（Performance Monitoring Unit）硬件计数器、软件事件和内核 tracepoint 来量化系统行为。与 ptrace 的\u0026quot;停止-读取-继续\u0026quot;模式不同，perf 在内核侧通过环形缓冲区（ring buffer）将采样数据批量上传到用户态，不会阻塞被观测进程。perf 最初由 Thomas Gleixner 和 Ingo Molnar 等人开发，2009 年随 Linux 2.6.31 合入主线。它既是一个计数工具（\u0026ldquo;发生了多少次\u0026rdquo;），也是一个采样工具（\u0026ldquo;在哪里发生的\u0026rdquo;）。\nPerformance Event perf是一个面向event的可观测性工具，能够帮助您实现高级性能分析和故障排查功能。它可以回答的问题包括：\n为什么内核在 CPU 上运行时间这么长？具体是哪些代码路径导致的？ 哪些代码路径导致了 CPU 二级缓存缺失？ CPU 是否因内存 I/O 而停滞？ 哪些代码路径在分配内存，分配了多少？ 是什么触发了 TCP 重传？ 某个内核函数是否被调用，调用频率是多少？ 线程离开 CPU 的原因是什么？ perf 的使用依赖我们前面所说的 event(事件)。event 是不同内核工具框架的统一接口，上面的图片说明了 event 来源:\nHardware Events: CPU性能监视计数器 PMCs Software Events: 这些是基于内核计数器的低级事件。例如，CPU迁移、主次缺页异常等等。 Kernel Tracepoint Events: 硬编码在内核中的静态内核级的检测点，即静态探针 User Statically-Defined Tracing (USDT): 这些是用户级程序和应用程序的静态跟踪点。 Dynamic Tracing: 可以被放置在任何地方的动态探针。对于内核软件，它使用kprobes框架。对于用户级软件，uprobes。 Timed Profiling: 使用perf -FHz选项以指定频率收集的快照。这通常用于CPU使用情况分析，其工作原理是周期性的产生时钟中断事件。 perf源码解析 tools/perf/ 所有 perf 功能都建立在同一个系统调用perf_event_open之上\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 $grep -rni sys_perf_event_open tools/perf/ util/cloexec.c:53: fd = sys_perf_event_open(\u0026amp;attr, pid, cpu, -1, util/cloexec.c:74: fd = sys_perf_event_open(\u0026amp;attr, pid, cpu, -1, 0); util/evlist.c:1252: * as sys_perf_event_open(cpu = -1, thread = -1) is EINVAL util/evsel.c:1546: pr_debug2(\u0026#34;sys_perf_event_open: pid %d cpu %d group_fd %d flags %#lx\u0026#34;, util/evsel.c:1549: fd = sys_perf_event_open(\u0026amp;evsel-\u0026gt;core.attr, pid, cpu, group_fd, flags); util/evsel.c:1566: pr_debug2(\u0026#34;\\nsys_perf_event_open failed, error %d\\n\u0026#34;, -ENOTSUP); util/evsel.c:1687: pr_debug2(\u0026#34;\\nsys_perf_event_open failed, error %d\\n\u0026#34;, util/evsel.c:2512: \u0026#34;The sys_perf_event_open() syscall returned with %d (%s) for event (%s).\\n\u0026#34; util/record.c:36: fd = sys_perf_event_open(\u0026amp;evsel-\u0026gt;core.attr, pid, cpu, -1, flags); util/record.c:50: fd = sys_perf_event_open(\u0026amp;evsel-\u0026gt;core.attr, pid, cpu, -1, flags); util/record.c:131: fd = sys_perf_event_open(\u0026amp;attr, -1, cpu, -1, 0); util/record.c:291: fd = sys_perf_event_open(\u0026amp;evsel-\u0026gt;core.attr, pid, cpu, -1, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // tools/perf/perf-sys.h static inline int sys_perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, int group_fd, unsigned long flags) { int fd; fd = syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags); #if HAVE_ATTR_TEST if (unlikely(test_attr__enabled)) test_attr__open(attr, pid, cpu, fd, group_fd, flags); #endif return fd; } // kernel/events/core.c:11902 SYSCALL_DEFINE5(perf_event_open, struct perf_event_attr __user *, attr_uptr, pid_t, pid, int, cpu, int, group_fd, unsigned long, flags) struct perf_event_attr:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct perf_event_attr { __u32 type; // PERF_TYPE_HARDWARE / SOFTWARE / TRACEPOINT / ... __u32 size; // sizeof(struct perf_event_attr) __u64 config; // 事件具体配置（如 PERF_COUNT_HW_CPU_CYCLES） union { __u64 sample_period; // 采样周期（模式） __u64 sample_freq; // 采样频率（模式） }; __u64 sample_type; // 采样数据包含哪些字段（IP, TID, TIME, ADDR, ...） __u64 read_format; // 读取格式 // ... 更多标志位 __u64 disabled : 1; // 初始是否禁用 __u64 inherit : 1; // 是否跟踪子进程 __u64 pinned : 1; // 是否独占 PMU __u64 exclusive : 1; __u64 exclude_user : 1; // 排除用户态 __u64 exclude_kernel : 1; // 排除内核态 __u64 exclude_hv : 1; // 排除虚拟机管理程序 __u64 precise_ip : 2; // 精确指令指针（PEBS 精度）instruction pointer(ip) // ... }; sequenceDiagram participant Perf as perf tool participant Kernel as perf_event 内核 participant PMU as PMU 硬件 participant RB as Ring Buffer Perf-\u0026gt;\u0026gt;Kernel: perf_event_open(type=HARDWARE, config=CPU_CYCLES) Kernel-\u0026gt;\u0026gt;PMU: MSR 编程，启用计数器 PMU--\u0026gt;\u0026gt;Kernel: 计数器溢出 → NMI Kernel-\u0026gt;\u0026gt;RB: 写入 sample (IP, PID, time, ...) Perf-\u0026gt;\u0026gt;Kernel: read(perf_fd, buf, size) Kernel--\u0026gt;\u0026gt;Perf: 批量样本数据 Note over Perf,RB: mmap ring buffer，避免 read 系统调用 perf 支持两种核心工作模式：\n模式 机制 数据量 典型命令 用途 计数（Counting） PMU 计数器持续累加，按需读取 极低 perf stat \u0026ldquo;发生了多少次 / 多少周期\u0026rdquo; 采样（Sampling） 计数器溢出或定时触发，记录现场（IP、栈等） 较高 perf record \u0026ldquo;CPU 在哪里消耗时间\u0026rdquo; perf stat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 charles@ubuntu22:~$ perf stat -d hackbench Running in process mode with 10 groups using 40 file descriptors each (== 400 tasks) Each sender will pass 100 messages of 100 bytes Time: 0.785 Performance counter stats for \u0026#39;hackbench\u0026#39;: 1600.33 msec task-clock # 1.891 CPUs utilized 9696 context-switches # 6.059 K/sec 592 cpu-migrations # 369.924 /sec 11558 page-faults # 7.222 K/sec \u0026lt;not supported\u0026gt; cycles \u0026lt;not supported\u0026gt; instructions \u0026lt;not supported\u0026gt; branches \u0026lt;not supported\u0026gt; branch-misses \u0026lt;not supported\u0026gt; L1-dcache-loads \u0026lt;not supported\u0026gt; L1-dcache-load-misses \u0026lt;not supported\u0026gt; LLC-loads \u0026lt;not supported\u0026gt; LLC-load-misses 0.846441059 seconds time elapsed 0.733235000 seconds user 0.920203000 seconds sys cycles 我用的是virtualbox ，可能不支持PMU ,硬件信息统计不了\n指标解读 指标 (Event) 含义 (Description) 解读与分析 (Interpretation \u0026amp; Analysis) task-clock 任务占用的 CPU 时间（毫秒） CPU 利用率：该值高说明程序是 CPU 密集型（CPU-bound），反之则可能受 I/O 或调度等待影响。事件名带 :u 后缀表示仅统计用户态。 context-switches 上下文切换次数 调度开销：频繁切换会带来性能开销。如果数值过高，可能暗示线程/进程数过多或锁争用严重。 cpu-migrations CPU 迁移次数 CPU 亲和性：进程被调度器在不同 CPU 核心间迁移的次数。频繁迁移会降低缓存命中率。 page-faults 缺页异常次数 内存访问：包括次要缺页（内存已映射）和主要缺页（需磁盘 I/O）。主要缺页对性能影响很大。 cycles CPU 时钟周期数 执行时长：指示程序运行消耗的 CPU 周期。可结合 task-clock 计算 CPU 频率。 instructions 执行的指令数 指令量：程序执行的机器指令总数。是衡量代码复杂度的指标之一。 branches 分支指令数 分支操作：程序中条件跳转、循环等分支指令的总数。 branch-misses 分支预测失败次数 预测惩罚：分支预测失败会导致流水线清空。该值比例（如 4.96% of all branches）越低越好。 指标 含义 详细说明 seconds time elapsed 程序总运行时间 (墙上时钟时间) 从程序启动到结束的实际耗时，包含 CPU 计算、I/O 等待、睡眠、调度延迟等所有时间。这是用户体验到的“真实时间”。 seconds user 用户态 CPU 时间 程序在用户空间执行代码所消耗的 CPU 时间总和（多核累加）。反映应用程序自身逻辑的计算开销。 seconds sys 内核态 CPU 时间 程序在内核空间执行代码所消耗的 CPU 时间总和（多核累加）。反映系统调用、文件 I/O、内存分配、网络收发等内核服务的开销。 选择特定事件： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 列出所有可用事件 perf list # perf stat help perf stat -h # 只统计特定事件 perf stat -e cycles,instructions,branch-misses,cache-misses hackbench # 统计多个事件组（硬件限制：同时可编程的 PMC 数量有限） perf stat -e \u0026#39;{cycles,instructions},{L1-dcache-loads,L1-dcache-misses}\u0026#39; hackbench # 每秒输出一次（interval mode） perf stat -I 1000 -e cycles,instructions -p $(pgrep app) # 统计整个系统所有 CPU perf stat -a sleep 5 perf stat 精度 1 2 3 4 5 # 使用 PEBS（Precise Event-Based Sampling）提高精度 见struct perf_event_attr中的precise_ip perf stat -e instructions:u,cycles:u ./myapp # 只统计用户态 # 多次运行取平均值 + 标准差 perf stat -r 5 ./myapp perf record / perf report — 采样模式 采样模式会以固定频率（或事件周期）打断 CPU，记录当前的指令指针（IP）和调用栈，然后聚合生成热点分布图。\n1 2 3 4 5 # 以 99 Hz 频率采样整个系统（99 Hz 避免与常见周期共振） perf record -F 99 -a -g -- sleep 10 # 生成报告 perf report perf report 交互界面\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Samples: 1K of event \u0026#39;cpu-clock:pppH\u0026#39;, Event count (approx.): 19999999800 Children Self Command Shared Object Symbol + 99.85% 0.00% swapper [kernel.kallsyms] [k] secondary_startup_64_no_verify + 99.85% 0.00% swapper [kernel.kallsyms] [k] cpu_startup_entry + 99.85% 0.00% swapper [kernel.kallsyms] [k] do_idle + 99.85% 0.00% swapper [kernel.kallsyms] [k] cpuidle_idle_call + 99.85% 0.00% swapper [kernel.kallsyms] [k] default_idle_call + 99.85% 0.00% swapper [kernel.kallsyms] [k] arch_cpu_idle + 99.85% 98.64% swapper [kernel.kallsyms] [k] native_safe_halt + 49.95% 0.00% swapper [kernel.kallsyms] [k] start_secondary + 49.90% 0.00% swapper [kernel.kallsyms] [k] x86_64_start_kernel + 49.90% 0.00% swapper [kernel.kallsyms] [k] x86_64_start_reservations + 49.90% 0.00% swapper [kernel.kallsyms] [k] start_kernel + 49.90% 0.00% swapper [kernel.kallsyms] [k] arch_call_rest_init + 49.90% 0.00% swapper [kernel.kallsyms] [k] rest_init + 1.21% 0.40% swapper [kernel.kallsyms] [k] handle_softirqs + 1.21% 0.00% swapper [kernel.kallsyms] [k] asm_sysvec_apic_timer_interrupt + 1.21% 0.00% swapper [kernel.kallsyms] [k] sysvec_apic_timer_interrupt + 1.21% 0.00% swapper [kernel.kallsyms] [k] irq_exit_rcu + 0.76% 0.00% swapper [kernel.kallsyms] [k] run_timer_softirq + 0.76% 0.00% swapper [kernel.kallsyms] [k] __run_timers.part.0 火焰图生成:\n1 2 3 4 5 6 7 # 生成折叠调用栈 perf script \u0026gt; out.perf # 使用 FlameGraph 工具集 git clone https://github.com/brendangregg/FlameGraph cd FlameGraph ./stackcollapse-perf.pl ../out.perf \u0026gt; out.folded ./flamegraph.pl out.folded \u0026gt; flamegraph.svg 采集调用栈的注意事项：\n1 2 3 4 5 6 7 8 9 10 11 # -g enables call-graph recording,记录完整的调用栈（Call Stack） perf record -F 99 -a -g ./myapp # 使用 DWARF 展开调用栈（更精确但更慢） perf record -F 99 --call-graph dwarf ./myapp # 使用 LBR（Last Branch Record，硬件辅助，开销最低） perf record -F 99 --call-graph lbr ./myapp # -C 可指定cpu ; -a system-wide collection from all CPUs perf record -C 0 -a -o /tmp/perf_run_cpu0.data -- sleep 700\u0026amp; #CPU 0 上运行的所有进程的系统级采样 记录特定 PID：\n1 perf record -F 99 -p $(pgrep myapp) -g -- sleep 30 perf top — 实时热点 类似 top 但按 CPU 采样热点排序，实时显示最热的函数。\n1 2 3 4 5 6 7 8 9 10 11 # 实时查看 CPU 热点（类似 htop 但显示函数级） perf top # 只查看用户态 perf top -u # 指定 PID perf top -p $(pgrep myapp) # 显示调用链 perf top -g perf probe — 动态插桩 perf probe 利用 kprobe/uprobe 机制动态插入探测点，无需重新编译内核或程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 在内核函数入口添加探针 perf probe --add \u0026#39;tcp_sendmsg size\u0026#39; # 在内核函数返回添加探针（返回值） perf probe --add \u0026#39;tcp_sendmsg%return $retval\u0026#39; # 查看探针记录的事件 perf record -e probe:tcp_sendmsg -a -g -- sleep 5 perf script # 删除探针 perf probe --del probe:tcp_sendmsg # 探测内核函数局部变量 perf probe --add \u0026#39;tcp_v4_rcv skb-\u0026gt;len\u0026#39; # 探测用户态函数（需要调试符号） perf probe -x /usr/lib/libc.so.6 --add \u0026#39;malloc size\u0026#39; # 列出所有已注册探针 perf probe --list 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 perf probe --add \u0026#39;tcp_sendmsg size\u0026#39; curl http://www.baidu.com perf record -e probe:tcp_sendmsg -a -g -- sleep 5 perf report Samples: 3 of event \u0026#39;probe:tcp_sendmsg\u0026#39;, Event count (approx.): 3 Children Self Command Shared Object Symbol + 66.67% 66.67% sshd [kernel.kallsyms] [k] tcp_sendmsg + 66.67% 0.00% sshd [unknown] [k] 0000000000000000 + 66.67% 0.00% sshd libc.so.6 [.] write + 66.67% 0.00% sshd [kernel.kallsyms] [k] entry_SYSCALL_64_after_hwframe + 66.67% 0.00% sshd [kernel.kallsyms] [k] do_syscall_64 + 66.67% 0.00% sshd [kernel.kallsyms] [k] x64_sys_call + 66.67% 0.00% sshd [kernel.kallsyms] [k] __x64_sys_write + 66.67% 0.00% sshd [kernel.kallsyms] [k] ksys_write + 66.67% 0.00% sshd [kernel.kallsyms] [k] vfs_write + 66.67% 0.00% sshd [kernel.kallsyms] [k] new_sync_write + 66.67% 0.00% sshd [kernel.kallsyms] [k] sock_write_iter + 66.67% 0.00% sshd [kernel.kallsyms] [k] __sock_sendmsg + 33.33% 33.33% curl [kernel.kallsyms] [k] tcp_sendmsg + 33.33% 0.00% curl [unknown] [k] 0000000000000000 + 33.33% 0.00% curl libc.so.6 [.] __send + 33.33% 0.00% curl [kernel.kallsyms] [k] entry_SYSCALL_64_after_hwframe + 33.33% 0.00% curl [kernel.kallsyms] [k] do_syscall_64 + 33.33% 0.00% curl [kernel.kallsyms] [k] x64_sys_call + 33.33% 0.00% curl [kernel.kallsyms] [k] __x64_sys_sendto + 33.33% 0.00% curl [kernel.kallsyms] [k] __sys_sendto + 33.33% 0.00% curl [kernel.kallsyms] [k] __sock_sendmsg perf trace — 类 strace 系统调用追踪 perf trace 利用内核 tracepoint 实现系统调用追踪，开销远低于 strace：\n1 2 3 4 5 6 7 8 9 10 11 # 追踪指定进程的系统调用（效果类似 strace，但快得多） perf trace -p $(pgrep myapp) # 追踪特定系统调用 perf trace -e openat,read,write -p $(pgrep myapp) # 追踪所有进程的特定系统调用 perf trace -e openat -a # 显示系统调用耗时 perf trace --duration 0.1 -e poll -p $(pgrep myapp) 1 2 3 4 5 6 7 8 9 10 11 12 13 root@ubuntu22:~# ls 11 11 root@ubuntu22:~# ls 11 root@ubuntu22:~# perf trace -e read ls 0.000 ( 0.218 ms): ls/10609 read(fd: 3, buf: 0x7fff90b79428, count: 832) = 832 0.456 ( 0.155 ms): ls/10609 read(fd: 3, buf: 0x7fff90b79408, count: 832) = 832 0.834 ( 0.590 ms): ls/10609 read(fd: 3, buf: 0x7fff90b793e8, count: 832) = 832 3.198 ( 0.325 ms): ls/10609 read(fd: 3, buf: 0x5605a08b1500, count: 1024) = 400 4.035 ( 0.223 ms): ls/10609 read(fd: 3, buf: 0x5605a08b1500, count: 1024) = 0 4.491 ( 0.616 ms): ls/10609 read(fd: 3, buf: 0x5605a08b1930, count: 4096) = 2996 5.492 ( 0.725 ms): ls/10609 read(fd: 3, buf: 0x5605a08b1930, count: 4096) = 0 11 perf trace vs strace 性能对比\n1 2 3 4 5 6 7 8 9 10 11 # strace: 10~100x 降速 time strace -e openat find /usr -name \u0026#34;*.txt\u0026#34; 2\u0026gt;/dev/null eal 1m15.565s user 0m1.170s sys 0m32.853s # perf trace: 几乎无开销 time perf trace -e openat find /usr -name \u0026#34;*.txt\u0026#34; 2\u0026gt;/dev/null real 0m5.467s user 0m0.389s sys 0m0.279s perf sched — 调度器分析 专门用于分析 Linux 调度器行为的子命令：\n1 2 3 4 5 6 7 8 9 10 11 # 记录调度事件 perf sched record -- sleep 5 # 生成调度延迟报告 perf sched latency # 可视化调度时间线 perf sched timehist # 显示每个任务的调度统计 perf sched map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 root@ubuntu22:~# perf sched record -- sleep 5 [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.281 MB perf.data (916 samples) ] root@ubuntu22:~# perf sched latency ------------------------------------------------------------------------------------------------------------------------------------------- Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end | ------------------------------------------------------------------------------------------------------------------------------------------- multipathd:(2) | 0.000 ms | 2 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s kworker/0:2-eve:10236 | 14.261 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s kworker/u4:2-ev:10491 | 4.524 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s rcu_sched:14 | 4.352 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s sleep:10660 | 3.476 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s kworker/0:1H-kb:90 | 3.087 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s systemd-resolve:607 | 1.958 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s systemd-network:605 | 1.601 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s kernel/events perf_event 子系统初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ┌─────────────────────────────────────────────────────────────────┐ │ 用户空间: perf stat / perf record / perf top │ │ 调用 perf_event_open(2) syscall │ │ 通过 mmap\u0026#39;d ring buffer 读取采样数据 │ ├─────────────────────────────────────────────────────────────────┤ │ 内核 perf 核心 (kernel/events/core.c) │ │ ┌──────────┬──────────┬──────────┬──────────┬─────────────┐ │ │ │ 事件分配 │ 上下文调度│ 溢出处理 │ Ring │ 继承/分组 │ │ │ │ alloc │ ctx sched│ overflow │ Buffer │ inherit │ │ │ └──────────┴──────────┴──────────┴──────────┴─────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ PMU 抽象层 — struct pmu (vtable 多态接口) │ │ ┌──────────┬──────────┬──────────┬──────────────┬──────────┐ │ │ │ perf_ │ perf_trace│ perf_ │ perf_ │ perf_ │ │ │ │ swevent │ point │ cpu_clock│ kprobe/uprobe│ breakpoint│ │ │ │ (软件) │ (静态探针)│ (hrtimer) │ (动态探针) │ (硬件断点) │ │ │ └──────────┴──────────┴──────────┴──────────────┴──────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ 硬件 PMU 驱动 (ARM64) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ struct arm_pmu → struct pmu (core) │ │ │ │ armv8pmu_handle_irq / armv8pmu_enable_event / ... │ │ │ │ PMCR / PMOVSR / PMEVCNTR / PMEVTYPER 寄存器操作 │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // init/main.c asmlinkage __visible void __init start_kernel(void) { ... random_init(command_line); boot_init_stack_canary(); perf_event_init(); // ← perf_event 子系统初始化 profile_init(); call_function_init(); WARN(!irqs_disabled(), \u0026#34;Interrupts were enabled early\\n\u0026#34;); ... arch_call_rest_init(); //--\u0026gt;rest_init()--\u0026gt;kernel_thread(kernel_init)-\u0026gt;kernel_init_freeable();run_init_process(/sbin/init) //kernel_init_freeable()-\u0026gt;do_basic_setup()-\u0026gt;driver_init();do_initcalls(); //drivers/base/init.c:driver_init() //do_initcalls()-\u0026gt;include/kernel/init.h:#define device_initcall(fn)\t__define_initcall(fn, 6)--\u0026gt;do_initcall_level() device_initcall 编译时注册机制 //drivers\\perf\\arm_pmu_acpi.cdevice_initcall(armv8_pmu_driver_init)--\u0026gt;platform_driver_register()-\u0026gt;(\u0026amp;armv8_pmu_driver)-\u0026gt;.probe = armv8_pmu_device_probe()若没有PMU 硬件，则用不了 //\\tools\\perf\\design.txt 中提到， not have hardware performance metrics, you can still use the generic software counters based on hrtimers for sampling。没有硬件PMU ,基本的软件PMU 可以用（在perf_event_init时已启用） ... } // kernel/events/core.c void __init perf_event_init(void) { int ret; idr_init(\u0026amp;pmu_idr); // ① ID分配器 perf_event_init_all_cpus(); // ② per-CPU数据结构 init_srcu_struct(\u0026amp;pmus_srcu); // ③ SRCU锁 perf_pmu_register(\u0026amp;perf_swevent, \u0026#34;software\u0026#34;, PERF_TYPE_SOFTWARE); // ④ 注册软件PMU perf_pmu_register(\u0026amp;perf_cpu_clock, NULL, -1); // ⑤ cpu-clock PMU perf_pmu_register(\u0026amp;perf_task_clock, NULL, -1); // ⑥ task-clock PMU perf_tp_register(); // ⑦ tracepoint/kprobe/uprobe PMU perf_event_init_cpu(smp_processor_id()); // ⑧ 初始化boot CPU register_reboot_notifier(\u0026amp;perf_reboot_notifier); // ⑨ 重启通知 ret = init_hw_breakpoint(); // ⑩ 硬件断点PMU WARN(ret, \u0026#34;hw_breakpoint initialization failed with: %d\u0026#34;, ret); } perf_event_init详解：\n步骤 函数 作用 ① idr_init(\u0026amp;pmu_idr) 初始化 PMU 类型 ID 分配器，PERF_TYPE_* → struct pmu * 的映射 ② perf_event_init_all_cpus() 为每个 possible CPU 初始化：swevent_htable（软件事件哈希表）、active_ctx_list、pmu_sb_events（side-band 事件列表） ③ init_srcu_struct(\u0026amp;pmus_srcu) 初始化保护 PMU 链表和 IDR 的 SRCU（Sleepable RCU） ④⑤⑥ perf_pmu_register() ×3 注册三个内置软件 PMU：perf_swevent（类型=PERF_TYPE_SOFTWARE，负责 page-fault/context-switch 等）、perf_cpu_clock、perf_task_clock ⑦ perf_tp_register() 注册 perf_tracepoint PMU（类型=PERF_TYPE_TRACEPOINT），以及可选的 perf_kprobe/perf_uprobe PMU ⑧ perf_event_init_cpu(boot_cpu) 初始化启动 CPU 的 swevent hlist、标记已注册 PMU 的 CPU 上下文 online ⑨ register_reboot_notifier() 注册优先级 INT_MIN 的重启通知（最后执行，确保 watchdog 尽可能久运行） ⑩ init_hw_breakpoint() 查询硬件断点寄存器数量、分配 per-CPU 数据、注册 perf_breakpoint PMU 关键结构体关系总览 classDiagram class perf_event_attr { +u32 type +u64 config +u64 sample_period / sample_freq +u64 sample_type +bits: disabled, inherit, pinned, exclude_user, exclude_kernel... +u64 branch_sample_type +u64 sample_regs_user } class perf_event { +struct pmu *pmu +struct perf_event_context *ctx +struct hw_perf_event hw +struct perf_buffer *rb +perf_overflow_handler_t overflow_handler +struct irq_work pending +local64_t count +struct perf_event_attr attr +struct perf_event *group_leader, *parent +struct list_head sibling_list, child_list +u64 id +int state } class pmu { +event_init() +add() / del() +start() / stop() +read() +pmu_enable() / pmu_disable() +sched_task() +attr_groups +int capabilities } class hw_perf_event { +local64_t period_left +u64 sample_period +u64 last_period +u64 interrupts +int state +int idx +struct task_struct *target +local64_t prev_count } class perf_event_context { +struct pmu *pmu +struct task_struct *task +struct rb_root pinned_groups +struct rb_root flexible_groups +struct list_head event_list +u64 time, timestamp } class perf_buffer { +int nr_pages +int overwrite +local_t head +unsigned int nest +local_t lost +atomic_t mmap_count +struct perf_event_mmap_page *user_page +void *data_pages[] } class perf_output_handle { +struct perf_buffer *rb +struct perf_event *event +void *addr +unsigned long size +int page } class perf_sample_data { +u64 ip +u32 pid, tid +u64 time, addr, id +u64 period +struct perf_callchain_entry *callchain +struct perf_raw_record *raw +struct perf_branch_stack *br_stack +struct perf_regs regs_user +u64 stack_user_size +u64 weight, phys_addr } perf_event_attr --\u0026gt; perf_event : embedded attr perf_event --\u0026gt; pmu : points to perf_event --\u0026gt; hw_perf_event : embedded hw perf_event --\u0026gt; perf_event_context : ctx perf_event --\u0026gt; perf_buffer : rb (RCU) perf_output_handle --\u0026gt; perf_buffer : rb perf_output_handle --\u0026gt; perf_event : event perf_sample_data --\u0026gt; perf_event : sampled from 结构体 定义位置 作用 perf_event_attr include/uapi/linux/perf_event.h:320 用户态配置：type, config, sample_period/freq, sample_type 等 perf_event include/linux/perf_event.h 核心事件对象：绑定 PMU → ctx → ring buffer，持有事件状态、计数 pmu include/linux/perf_event.h PMU 驱动虚函数表：event_init/add/del/start/stop/read，多态核心 hw_perf_event include/linux/perf_event.h 硬件计数器状态：period_left, idx, prev_count, interrupts（节流用） perf_event_context include/linux/perf_event.h 事件调度容器：per-task 或 per-CPU，管理 pinned/flexible 两组红黑树 perf_buffer kernel/events/internal.h:13 mmap Ring Buffer：head/tail 游标、data_pages[]、user_page（控制页） perf_output_handle include/linux/perf_event.h 写入游标：addr/size/page，封装跨页写入 perf_sample_data include/linux/perf_event.h 采样数据暂存区：IP/TID/TIME/CALLCHAIN/REGS 等 系统调用入口：perf_event_open 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // kernel/events/core.c SYSCALL_DEFINE5(perf_event_open, struct perf_event_attr __user *, attr_uptr, pid_t, pid, int, cpu, int, group_fd, unsigned long, flags) { // 1. 复制用户态 attr err = perf_copy_attr(attr_uptr, \u0026amp;attr); // 2. LSM 安全检查 err = security_perf_event_open(\u0026amp;attr, PERF_SECURITY_OPEN); // 3. exclude_kernel 需要 CAP_PERFMON if (!attr.exclude_kernel) { err = perf_allow_kernel(\u0026amp;attr); } // 4. 分配 core 结构 event = perf_event_alloc(\u0026amp;attr, cpu, task, group_leader, ...); // 5. 获取/创建上下文 ctx = find_get_context(task, event, \u0026amp;err); // 6. 创建匿名文件（fd 最终关联此文件） event_file = anon_inode_getfile(\u0026#34;[perf_event]\u0026#34;, \u0026amp;perf_fops, event, f_flags); // 7. 安装事件到上下文（通过 IPI 跨 CPU 通知） perf_install_in_context(ctx, event, event-\u0026gt;cpu); // 8. 返回 fd fd_install(event_fd, event_file); return event_fd; } struct perf_event — 事件对象（Central Object） struct perf_event 是整个 perf 子系统的核心对象，定义在 include/linux/perf_event.h 中。每个 perf_event_open() 系统调用创建一个 perf_event 实例，贯穿事件从创建到销毁的整个生命周期。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 // include/linux/perf_event.h — Linux v5.10 // 关键字段按逻辑分组展示 struct perf_event { /* --- 1. PMU 后端绑定 --- */ struct pmu *pmu; // PMU 后端（perf_init_event 选择） local64_t count; // 累计计数值（用户最终读到） atomic64_t child_count; // 子事件累计（inherit 场景求和） /* --- 2. 状态与标识 --- */ enum perf_event_state state; // DEAD/EXIT/ERROR/OFF/INACTIVE/ACTIVE unsigned int attach_state; // PERF_ATTACH_TASK 等 local64_t total_time_enabled; // 累计启用时间 local64_t total_time_running; // 累计运行时间（不含 multiplexing） u64 tstamp; // 时间戳（用于核算） /* --- 3. CPU 与上下文关联 --- */ int cpu; // 目标 CPU（-1 = 不限） int oncpu; // 当前运行的 CPU（ACTIVE 时有效） struct perf_event_context *ctx; // 所属上下文（task or per-CPU） /* --- 4. 事件关系（分组 + 继承） --- */ struct perf_event *parent; // 父事件（inherit 场景的 clone 来源） struct perf_event *group_leader; // 组 leader（单事件以自身为 leader） struct list_head sibling_list; // 组内兄弟链表 struct list_head child_list; // 子事件链表 struct perf_event *next_sibling; // 组内下一个兄弟 int group_caps; // 组能力位 /* --- 5. 调度与激活 --- */ struct list_head active_list; // ctx-\u0026gt;pinned_active 或 flexible_active struct list_head event_entry; // ctx-\u0026gt;event_list struct rb_node group_node; // pinned/flexible RB-tree 节点 u64 group_index; // RB-tree 排序键 /* --- 6. 硬件状态 --- */ struct hw_perf_event hw; // 硬件细节（见下方展开） int event_caps; // PERF_EV_CAP_SOFTWARE 等 /* --- 7. 采样与 ring buffer --- */ struct perf_buffer *rb; // ring buffer（RCU 指针） struct list_head rb_entry; // rb-\u0026gt;event_list struct perf_event *aux_event; // 关联的 AUX 事件 /* --- 8. 溢出处理 --- */ perf_overflow_handler_t overflow_handler; // 溢出回调函数 void *overflow_handler_context; int pending_disable; // 延期关闭标志 struct irq_work pending; // 延期执行（irq_work） atomic_t event_limit; // 触发次数上限 /* --- 9. fd 与生命期管理 --- */ struct file *filp; // 关联的文件对象 atomic_t mmap_count; // mmap 引用计数 atomic_long_t refcount; // 引用计数（put_event 释放） struct rcu_head rcu_head; // RCU 延迟释放 /* --- 10. 地址过滤 --- */ struct perf_addr_filters_head addr_filters; struct perf_addr_filter_range *addr_filter_ranges; unsigned long addr_filters_gen; /* --- 11. 其他 --- */ struct list_head sb_list; // 侧带宽事件 struct hlist_node hlist_entry; // PMU 的 owner 链表 wait_queue_head_t waitq; // poll 等待队列 struct fasync_struct *fasync; // 异步通知 struct mutex mmap_mutex; // mmap 操作互斥锁 /* ... */ }; struct hw_perf_event — 硬件状态展开： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct hw_perf_event { union { struct { /* 硬件 PMU 计数器 */ u64 config; // 事件编码 unsigned long config_base; // 控制寄存器地址（如 PMXEVTYPER） unsigned long event_base; // 计数寄存器地址（如 PMEVCNTRn） int event_base_rdpmc;// RDPMC 索引 int idx; // 硬件计数器槽位索引 int last_cpu; int flags; struct hw_perf_event_extra extra_reg, branch_reg; }; struct { /* 软件事件 */ struct hrtimer hrtimer; // 高精度定时器（采样周期） }; struct { /* tracepoint 事件 */ struct list_head tp_list; }; }; struct task_struct *target; // per-task 指向目标 task int state; // PERF_HES_STOPPED/UPTODATE/ARCH local64_t prev_count; // 上一次读取的硬件计数值 u64 sample_period; // 采样周期 u64 last_period; // 本采样周期的起始值 local64_t period_left; // 周期剩余值 u64 interrupts_seq; // 中断序列号（throttle 跟踪） u64 interrupts; // 中断计数 u64 freq_time_stamp; // 频率调整时间戳 u64 freq_count_stamp;// 频率调整计数值 }; perf_event 各大字段组详解 PMU 绑定 — pmu / count 1 2 3 4 5 6 7 8 9 10 // kernel/events/core.c — perf_event_alloc() pmu = perf_init_event(event); // 遍历已注册 PMU，调用 pmu-\u0026gt;event_init() // 返回后：event-\u0026gt;pmu = pmu // 读取计数值 static inline u64 perf_event_count(struct perf_event *event) { return local64_read(\u0026amp;event-\u0026gt;count) + atomic64_read(\u0026amp;event-\u0026gt;child_count); } // child_count 用于 inherit 事件：子进程结束时将计数合并到父事件 状态机 — state 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // include/linux/perf_event.h — 状态枚举 enum perf_event_state { PERF_EVENT_STATE_DEAD = -4, // 已销毁 PERF_EVENT_STATE_EXIT = -3, // 进程退出 PERF_EVENT_STATE_ERROR = -2, // 硬件错误 PERF_EVENT_STATE_OFF = -1, // 初始/关闭 PERF_EVENT_STATE_INACTIVE = 0, // 已安装但未调度到 PMU PERF_EVENT_STATE_ACTIVE = 1, // 正在 PMU 上运行 }; // 状态转换 static inline void perf_event_set_state(struct perf_event *event, enum perf_event_state state) { event-\u0026gt;state = state; } // 有效状态的复合判断 static inline enum perf_event_state __perf_effective_state(struct perf_event *event) { if (event-\u0026gt;state == PERF_EVENT_STATE_OFF \u0026amp;\u0026amp; event-\u0026gt;attr.enable_on_exec) return PERF_EVENT_STATE_INACTIVE; return event-\u0026gt;state; } // 关键逻辑：OFF 但设置了 enable_on_exec 的 event // 在 fork/exec 时会自动变成 INACTIVE 上下文关联 — cpu / oncpu / ctx 1 2 3 4 5 6 7 8 9 10 11 12 // 写入 oncpu（event_sched_in 的开头） event-\u0026gt;oncpu = smp_processor_id(); // 写入当前 CPU smp_wmb(); // 保证 oncpu 对其他 CPU 可见 event-\u0026gt;state = PERF_EVENT_STATE_ACTIVE; // 当跨 CPU 读取时（ __perf_event_read_cpu） static int __perf_event_read_cpu(struct perf_event *event, int event_cpu) { if (event-\u0026gt;oncpu == event_cpu) // 事件是否还在这个 CPU 上 return event_cpu; return smp_processor_id(); // 已迁移 → 在本 CPU 读取 } 事件关系 — parent / group_leader / sibling_list 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 分组（perf_group_attach） static void perf_group_attach(struct perf_event *event) { struct perf_event *group_leader = event-\u0026gt;group_leader; if (group_leader == event) // 单事件 = 自己的 leader return; if (group_leader-\u0026gt;nr_siblings \u0026gt;= group_leader-\u0026gt;attr.wakeup_events) return; // 触发了 watermark list_add_tail(\u0026amp;event-\u0026gt;sibling_list, \u0026amp;group_leader-\u0026gt;sibling_list); group_leader-\u0026gt;nr_siblings++; } // 继承（ — perf_event_alloc） event-\u0026gt;parent = parent_event; // 父事件指针 // 子 event 共享 parent 的 ring buffer 和配置 // 子进程 exit 时，子 event 的计数合并到 parent-\u0026gt;child_count 调度 — active_list / event_entry / group_node / group_index 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 加入上下文（add_event_to_ctx） static void add_event_to_ctx(struct perf_event *event, struct perf_event_context *ctx) { list_add_event(event, ctx); // → ctx-\u0026gt;event_list perf_event_groups_insert(event, ctx); // → RB-tree (pinned/flexible) } // RB-tree 插入（ perf_event_groups_insert） // 按 {event-\u0026gt;cpu, event-\u0026gt;group_index} 排序 // pinned_groups 中所有事件必须被容纳 // flexible_groups 中事件可轮转（multiplexing） // 轮转（rotate_ctx） static void rotate_ctx(struct perf_event_context *ctx, struct perf_event *event) { perf_event_groups_delete(\u0026amp;ctx-\u0026gt;flexible_groups, event); event-\u0026gt;group_index++; // 增大排序键 → 下轮排到末尾 perf_event_groups_insert(\u0026amp;ctx-\u0026gt;flexible_groups, event); // 每次定时器中断调用 rotate_ctx，实现 round-robin } 硬件状态 — hw 在 Arm64 上的具体用法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // armv8pmu_enable_event（arch/arm64/kernel/perf_event.c） static void armv8pmu_enable_event(struct perf_event *event) { struct hw_perf_event *hwc = \u0026amp;event-\u0026gt;hw; // hwc-\u0026gt;config = 事件编码（如 0x11 = CPU_CYCLES） // hwc-\u0026gt;config_base = 写入 PMXEVTYPER_EL0 的值 // hwc-\u0026gt;idx = 分配的计数器槽位索引 armv8pmu_write_evtype(idx, hwc-\u0026gt;config); // 编程事件类型 armv8pmu_enable_counter(idx); // 启用计数器 armv8pmu_enable_intens(idx); // 启用中断 } // armv8pmu_read_counter static inline u64 armv8pmu_read_counter(struct perf_event *event) { struct hw_perf_event *hwc = \u0026amp;event-\u0026gt;hw; int idx = hwc-\u0026gt;idx; if (idx == ARMV8_IDX_CYCLE_COUNTER) return read_sysreg(pmccntr_el0); // 读取周期计数器 else return armv8pmu_read_evcntr(idx); // 读取 PMEVCNTRn_EL0 } Ring Buffer — rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // kernel/events/core.c — 获取 ring buffer struct perf_buffer *ring_buffer_get(struct perf_event *event) { struct perf_buffer *rb; rcu_read_lock(); rb = rcu_dereference(event-\u0026gt;rb); // RCU 保护 if (rb) { if (!refcount_inc_not_zero(\u0026amp;rb-\u0026gt;refcount)) rb = NULL; } rcu_read_unlock(); return rb; } // 溢出时写 sample（ __perf_event_output） static void __perf_event_output(struct perf_event *event, ...) { struct perf_output_handle handle; struct perf_sample_data data; perf_prepare_sample(\u0026amp;data, event, regs); // 填充 IP/TID/TIME/... if (output_begin(\u0026amp;handle, \u0026amp;data, event, header.size)) // 预留 rb 空间 return; perf_output_sample(\u0026amp;handle, \u0026amp;header, \u0026amp;data, event); // 写入 ring buffer perf_output_end(\u0026amp;handle); // 提交 + 唤醒 poll } 溢出处理 — overflow_handler / pending 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 内核默认溢出回调（ perf_event_output_forward） void perf_event_output_forward(struct perf_event *event, ...) { __perf_event_output(event, data, regs); // 写入 ring buffer } // 延期关闭（perf_event_disable_inatomic） void perf_event_disable_inatomic(struct perf_event *event) { event-\u0026gt;pending_disable = 1; // 置标志 irq_work_queue(\u0026amp;event-\u0026gt;pending); // 调度 irq_work // 在 NMI/IRQ 上下文中不能直接调用关闭函数 // 通过 irq_work 延期到 softirq 执行 } // irq_work 处理函数（perf_pending_event） static void perf_pending_event(struct irq_work *entry) { struct perf_event *event = container_of(entry, struct perf_event, pending); if (event-\u0026gt;pending_disable) { event-\u0026gt;pending_disable = 0; perf_event_disable_local(event); // 关闭事件 } if (event-\u0026gt;pending_wakeup) { event-\u0026gt;pending_wakeup = 0; perf_event_wakeup(event); // 唤醒 poll } } 生命周期 — refcount / filp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 引用计数管理（put_event） static void put_event(struct perf_event *event) { if (!atomic_long_dec_and_test(\u0026amp;event-\u0026gt;refcount)) return; // refcount == 0 → 真正释放 free_event(event); } // 释放路径（free_event） static void free_event(struct perf_event *event) { // 1. 关闭所有子事件 perf_event_free_child(event); // 2. 关闭地址过滤 perf_addr_filters_splice(event, NULL); // 3. 关闭 BPF 程序 perf_event_free_bpf_prog(event); // 4. PMU 释放 event-\u0026gt;pmu-\u0026gt;read(event); event-\u0026gt;pmu-\u0026gt;del(event, 0); // 5. 释放 ring buffer ring_buffer_put(event-\u0026gt;rb); // 6. 释放 task 引用 put_task_struct(event-\u0026gt;hw.target); // 7. RCU 释放 call_rcu(\u0026amp;event-\u0026gt;rcu_head, free_event_rcu); } perf_event_open 系统调用流程 文件: kernel/events/core.c\n调用流程图 flowchart TD A[\u0026#34;用户态: perf_event_open(attr, pid, cpu, group_fd, flags)\u0026#34;] --\u0026gt; B[\u0026#34;perf_copy_attr() 复制用户attr\u0026#34;] B --\u0026gt; C[\u0026#34;security_perf_event_open() LSM检查\u0026#34;] C --\u0026gt; D[\u0026#34;perf_init_event() 查找匹配PMU\u0026#34;] D --\u0026gt; E{\u0026#34;attr.type 是什么?\u0026#34;} E --\u0026gt;|\u0026#34;PERF_TYPE_HARDWARE / HW_CACHE\u0026#34;| F[\u0026#34;改写 type = PERF_TYPE_RAW\u0026#34;] E --\u0026gt;|\u0026#34;其他已知type\u0026#34;| G[\u0026#34;idr_find(\u0026amp;pmu_idr, type) 查IDR\u0026#34;] E --\u0026gt;|\u0026#34;动态PMU (kprobe/uprobe)\u0026#34;| H[\u0026#34;遍历pmus链表 match\u0026#34;] F --\u0026gt; G G --\u0026gt; I{\u0026#34;perf_try_init_event(pmu, event)\u0026#34;} H --\u0026gt; I I --\u0026gt;|\u0026#34;成功\u0026#34;| J[\u0026#34;pmu-\u0026gt;event_init(event)\u0026#34;] I --\u0026gt;|\u0026#34;ENOENT\u0026#34;| H J --\u0026gt; K[\u0026#34;perf_event_alloc() 分配perf_event\u0026#34;] K --\u0026gt; L[\u0026#34;find_get_context() 获取per-task或per-CPU ctx\u0026#34;] L --\u0026gt; M[\u0026#34;perf_install_in_context() 将事件加入ctx\u0026#34;] M --\u0026gt; N[\u0026#34;返回fd给用户态\u0026#34;] PMU 匹配逻辑 (perf_init_event, core.c) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static struct pmu *perf_init_event(struct perf_event *event) { // ① 先尝试父事件的 PMU（用于继承场景） if (event-\u0026gt;parent \u0026amp;\u0026amp; event-\u0026gt;parent-\u0026gt;pmu) { pmu = event-\u0026gt;parent-\u0026gt;pmu; ret = perf_try_init_event(pmu, event); if (!ret) goto unlock; } // ② HARDWARE/HW_CACHE 改写为 RAW type = event-\u0026gt;attr.type; if (type == PERF_TYPE_HARDWARE || type == PERF_TYPE_HW_CACHE) type = PERF_TYPE_RAW; // ③ IDR 快速查找（固定 type） pmu = idr_find(\u0026amp;pmu_idr, type); if (pmu) { ret = perf_try_init_event(pmu, event); // 调用 pmu-\u0026gt;event_init // 若 -ENOENT 且 type 被改写了，用原始 type 重试 } // ④ 遍历 PMU 链表（动态 PMU：kprobe/uprobe/raw 找不到时） list_for_each_entry_rcu(pmu, \u0026amp;pmus, entry) { ret = perf_try_init_event(pmu, event); if (!ret) goto unlock; } } perf stat — 计数模式 计数原理 perf stat 不采样（不设 sample_period），只关心事件的累计计数。关键路径：\n1 2 3 4 5 6 7 perf_event_open() → event被加到ctx → pmu-\u0026gt;add() 将事件分配到硬件计数器 → pmu-\u0026gt;start() 启动计数 用户态 read(fd) → perf_read() → __perf_event_read() → IPI到目标CPU → __perf_event_read() (在目标CPU上执行) → pmu-\u0026gt;read(event) 读取硬件计数器 → perf_event_count() 返回 event-\u0026gt;count 读取路径核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // kernel/events/core.c static void __perf_event_read(void *info) { // 在目标 CPU 上运行 // ① 更新上下文时间（用于 multiplexing 缩放） perf_event_update_time(event); // ② 调用 PMU 驱动的 read 方法 // ARM64: armpmu_read() → armpmu_event_update() // 计算 delta = 当前硬件计数器 - prev_count // 累加到 event-\u0026gt;count pmu-\u0026gt;read(event); } // kernel/events/core.c static u64 __perf_event_read_value(struct perf_event *event, ...) { (void)perf_event_read(event, false); total += perf_event_count(event); // local64_read(\u0026amp;event-\u0026gt;count) // 累加子事件的计数（inherit） *enabled += event-\u0026gt;total_time_enabled; *running += event-\u0026gt;total_time_running; // 多路复用缩放: scaled = count * enabled / running } ARM64 上的计数器读取 1 2 3 4 5 6 // arch/arm64/kernel/perf_event.c static u64 armv8pmu_read_counter(struct perf_event *event) { // 通过 PMEVCNTR\u0026lt;n\u0026gt;_EL0 或 PMCCNTR_EL0（cycle counter）读取 // 若为 32-bit 计数器且需要 64-bit，则读取相邻的 chain 计数器 } perf record — 采样模式 采样完整路径 flowchart TD A[\u0026#34;硬件计数器溢出 (PMU overflow)\u0026#34;] --\u0026gt; B[\u0026#34;armv8pmu_handle_irq()\u0026#34;] B --\u0026gt; C[\u0026#34;armv8pmu_getreset_flags()\u0026lt;br/\u0026gt;读取 PMOVSR 获取溢出位\u0026#34;] C --\u0026gt; D{\u0026#34;有溢出?\u0026#34;} D --\u0026gt;|\u0026#34;否\u0026#34;| E[\u0026#34;return IRQ_NONE\u0026#34;] D --\u0026gt;|\u0026#34;是\u0026#34;| F[\u0026#34;armv8pmu_stop()\u0026lt;br/\u0026gt;停止PMU防止组内偏差\u0026#34;] F --\u0026gt; G[\u0026#34;遍历每个计数器\u0026#34;] G --\u0026gt; H{\u0026#34;该计数器溢出?\u0026#34;} H --\u0026gt;|\u0026#34;是\u0026#34;| I[\u0026#34;armpmu_event_update()\u0026lt;br/\u0026gt;读取硬件计数器,计算delta\u0026#34;] I --\u0026gt; J[\u0026#34;armpmu_event_set_period()\u0026lt;br/\u0026gt;重新设定采样周期\u0026#34;] J --\u0026gt; K[\u0026#34;perf_event_overflow(event, data, regs)\u0026#34;] K --\u0026gt; L[\u0026#34;__perf_event_overflow()\u0026#34;] L --\u0026gt; M[\u0026#34;节流检查\u0026lt;br/\u0026gt;__perf_event_account_interrupt()\u0026#34;] M --\u0026gt; N[\u0026#34;event-\u0026gt;overflow_handler()\u0026#34;] N --\u0026gt; O[\u0026#34;perf_event_output_forward()\u0026#34;] O --\u0026gt; P[\u0026#34;perf_output_begin()\u0026lt;br/\u0026gt;在ring buffer中分配空间\u0026#34;] P --\u0026gt; Q[\u0026#34;perf_output_sample()\u0026lt;br/\u0026gt;写入IP/TID/TIME/CALLCHAIN等\u0026#34;] Q --\u0026gt; R[\u0026#34;perf_output_end()\u0026lt;br/\u0026gt;更新user_page-\u0026gt;data_head\u0026#34;] R --\u0026gt; S[\u0026#34;armv8pmu_start() 重启PMU\u0026#34;] ARM64 中断处理 (arch/arm64/kernel/perf_event.c:749) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static irqreturn_t armv8pmu_handle_irq(struct arm_pmu *cpu_pmu) { u32 pmovsr; struct pmu_hw_events *cpuc = this_cpu_ptr(cpu_pmu-\u0026gt;hw_events); // ① 读取并清除溢出状态寄存器 (PMOVSR) pmovsr = armv8pmu_getreset_flags(); // ② 无溢出 → IRQ_NONE if (!armv8pmu_has_overflowed(pmovsr)) return IRQ_NONE; regs = get_irq_regs(); // ③ 停止 PMU（清 PMCR.E），防止组内计数器偏差 armv8pmu_stop(cpu_pmu); // ④ 遍历所有计数器 for (idx = 0; idx \u0026lt; cpu_pmu-\u0026gt;num_events; ++idx) { struct perf_event *event = cpuc-\u0026gt;events[idx]; if (!event) continue; if (!armv8pmu_counter_has_overflowed(pmovsr, idx)) continue; hwc = \u0026amp;event-\u0026gt;hw; armpmu_event_update(event); // 读计数器, 更新event-\u0026gt;count perf_sample_data_init(\u0026amp;data, 0, hwc-\u0026gt;last_period); if (!armpmu_event_set_period(event)) // 重设采样周期 continue; // ⑤ 调用 perf 核心溢出处理 → 写入 ring buffer if (perf_event_overflow(event, \u0026amp;data, regs)) cpu_pmu-\u0026gt;disable(event); // 节流：关闭事件 } // ⑥ 重新启动 PMU armv8pmu_start(cpu_pmu); return IRQ_HANDLED; } 溢出处理 → Ring Buffer 写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // core.c static int __perf_event_overflow(struct perf_event *event, int throttle, struct perf_sample_data *data, struct pt_regs *regs) { // 非采样事件不处理 if (unlikely(!is_sampling_event(event))) return 0; // 节流：如果中断速率过高，禁用事件 ret = __perf_event_account_interrupt(event, throttle); event-\u0026gt;pending_kill = POLL_IN; if (events \u0026amp;\u0026amp; atomic_dec_and_test(\u0026amp;event-\u0026gt;event_limit)) { ret = 1; event-\u0026gt;pending_kill = POLL_HUP; perf_event_disable_inatomic(event); // 强制关闭 } // 调用 overflow_handler → 默认 perf_event_output_forward() READ_ONCE(event-\u0026gt;overflow_handler)(event, data, regs); // 如果有 fasync 等待者，排队 irq_work 唤醒 if (*perf_event_fasync(event) \u0026amp;\u0026amp; event-\u0026gt;pending_kill) { event-\u0026gt;pending_wakeup = 1; irq_work_queue(\u0026amp;event-\u0026gt;pending); } return ret; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // ring_buffer.c — Ring Buffer 空间分配 static int __perf_output_begin(struct perf_output_handle *handle, ...) { rb = rcu_dereference(event-\u0026gt;rb); // 计算 tail（用户态读位置） vs head（内核写位置） do { tail = READ_ONCE(rb-\u0026gt;user_page-\u0026gt;data_tail); head = local_read(\u0026amp;rb-\u0026gt;head); if (!rb-\u0026gt;overwrite) { // 空间不足 → 记录 lost，返回 -ENOSPC if (!ring_buffer_has_space(head, tail, ...)) goto fail; } // CAS 原子前进 head } while (local_cmpxchg(\u0026amp;rb-\u0026gt;head, offset, head) != offset); // 计算写入地址 handle-\u0026gt;addr = rb-\u0026gt;data_pages[handle-\u0026gt;page] + offset; handle-\u0026gt;size = (1UL \u0026lt;\u0026lt; page_shift) - offset; } perf event 总览 所有事件类型及 PMU 对照表 类型 perf_type_id 对应 PMU event_init 触发机制 典型用法 硬件事件 PERF_TYPE_HARDWARE (0) arm_pmu.pmu (ARM64) armpmu_event_init() 硬件计数器溢出中断 (PMU IRQ) perf stat -e cycles, perf record -e cache-misses 软件事件 PERF_TYPE_SOFTWARE (1) perf_swevent perf_swevent_init() 内核代码路径主动调用 perf_sw_event() perf stat -e page-faults,context-switches Tracepoint PERF_TYPE_TRACEPOINT (2) perf_tracepoint perf_tp_event_init() 静态埋点 trace_xxx() 宏展开 perf record -e sched:sched_switch HW Cache PERF_TYPE_HW_CACHE (3) 改写为 RAW 同硬件 PMU 同硬件事件 perf stat -e L1-dcache-loads Raw PERF_TYPE_RAW (4) 遍历 PMU 链表 各 PMU 自行处理 取决于匹配到的 PMU perf stat -e r11 (ARM64 raw event) Breakpoint PERF_TYPE_BREAKPOINT (5) perf_breakpoint hw_breakpoint_event_init() 调试异常 (debug exception) perf record -e mem:0xaddr Kprobe 动态分配 perf_kprobe perf_kprobe_event_init() kprobe 断点 → kprobe_dispatcher() perf probe --add func Uprobe 动态分配 perf_uprobe perf_uprobe_event_init() uprobe 断点 → uprobe_dispatcher() perf probe -x /bin/bash func cpu-clock 动态分配 perf_cpu_clock cpu_clock_event_init() hrtimer 周期触发 perf record -e cpu-clock task-clock 动态分配 perf_task_clock task_clock_event_init() hrtimer 周期触发 perf record -e task-clock 软件事件触发点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // core.c — 所有软件类事件最终汇聚此函数 static void perf_swevent_event(struct perf_event *event, u64 nr, struct perf_sample_data *data, struct pt_regs *regs) { local64_add(nr, \u0026amp;event-\u0026gt;count); // ① 累加计数 if (!regs) return; if (!is_sampling_event(event)) // ② 非采样事件只计数 return; // ③ 采样模式：检查采样周期 if (nr == 1 \u0026amp;\u0026amp; hwc-\u0026gt;sample_period == 1 \u0026amp;\u0026amp; !event-\u0026gt;attr.freq) return perf_swevent_overflow(event, 1, data, regs); if (local64_add_negative(nr, \u0026amp;hwc-\u0026gt;period_left)) return; perf_swevent_overflow(event, 0, data, regs); // ④ 溢出→写入ring buffer } Tracepoint 事件流转 flowchart TD A[\u0026#34;内核代码: trace_sched_switch(prev, next)\u0026#34;] --\u0026gt; B[\u0026#34;tracepoint 宏展开\u0026#34;] B --\u0026gt; C[\u0026#34;RCU遍历 tp_funcs 链表\u0026#34;] C --\u0026gt; D[\u0026#34;perf_trace_##name() 即 class-\u0026gt;perf_probe\u0026#34;] D --\u0026gt; E[\u0026#34;perf_trace_buf_alloc()\u0026lt;br/\u0026gt;分配临时缓冲区填数据\u0026#34;] E --\u0026gt; F[\u0026#34;perf_trace_run_bpf_submit()\u0026#34;] F --\u0026gt; G{\u0026#34;有BPF程序?\u0026#34;} G --\u0026gt;|\u0026#34;是\u0026#34;| H[\u0026#34;trace_call_bpf() 执行BPF\u0026#34;] G --\u0026gt;|\u0026#34;否/BPF通过\u0026#34;| I[\u0026#34;perf_tp_event()\u0026lt;br/\u0026gt;遍历per-CPU hlist\u0026#34;] I --\u0026gt; J[\u0026#34;perf_swevent_event()\u0026lt;br/\u0026gt;累加计数 + overflow检查\u0026#34;] J --\u0026gt; K[\u0026#34;perf_swevent_overflow()\u0026lt;br/\u0026gt;→ overflow_handler → ring buffer\u0026#34;] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // core.c void perf_trace_run_bpf_submit(void *raw_data, int size, int rctx, struct trace_event_call *call, u64 count, struct pt_regs *regs, struct hlist_head *head, struct task_struct *task) { if (bpf_prog_array_valid(call)) { *(struct pt_regs **)raw_data = regs; if (!trace_call_bpf(call, raw_data) || hlist_empty(head)) { perf_swevent_put_recursion_context(rctx); return; // BPF 过滤掉了该事件 } } perf_tp_event(call-\u0026gt;event.type, count, raw_data, size, regs, head, rctx, task); } Kprobe/Uprobe 动态探针 flowchart TD subgraph 注册阶段 A[\u0026#34;perf_event_open(type=kprobe, config=func_name)\u0026#34;] --\u0026gt; B[\u0026#34;perf_kprobe_event_init()\u0026#34;] B --\u0026gt; C[\u0026#34;perf_kprobe_init() → create_local_trace_kprobe()\u0026#34;] C --\u0026gt; D[\u0026#34;register_kprobe() 在目标函数入口安装断点\u0026#34;] end subgraph 触发阶段 E[\u0026#34;目标函数被调用\u0026#34;] --\u0026gt; F[\u0026#34;kprobe 断点触发\u0026#34;] F --\u0026gt; G[\u0026#34;kprobe_dispatcher()\u0026#34;] G --\u0026gt; H[\u0026#34;kprobe_perf_func()\u0026#34;] H --\u0026gt; I[\u0026#34;perf_trace_buf_submit()\u0026#34;] I --\u0026gt; J[\u0026#34;perf_trace_run_bpf_submit()\u0026#34;] J --\u0026gt; K[\u0026#34;perf_tp_event() → perf_swevent_event() → ring buffer\u0026#34;] end Kprobe/Uprobe 的 PMU 结构（core.c）：\n1 2 3 4 5 6 7 8 9 10 static struct pmu perf_kprobe = { .task_ctx_nr = perf_sw_context, .event_init = perf_kprobe_event_init, // 创建 trace_kprobe .add = perf_trace_add, // 加入 per-CPU hlist .del = perf_trace_del, .start = perf_swevent_start, .stop = perf_swevent_stop, .read = perf_swevent_read, .attr_groups = kprobe_attr_groups, }; tracepoint PMU (核心) 1 2 3 4 5 6 7 8 9 10 11 // core. static struct pmu perf_tracepoint = { .task_ctx_nr = perf_sw_context, .event_init = perf_tp_event_init, // 匹配 trace_event_call .add = perf_trace_add, // 加入 per-CPU hlist .del = perf_trace_del, .start = perf_swevent_start, .stop = perf_swevent_stop, .read = perf_swevent_read, }; perf_tp_register() 注册 1 2 3 4 5 6 7 8 9 10 11 // core.c static inline void perf_tp_register(void) { perf_pmu_register(\u0026amp;perf_tracepoint, \u0026#34;tracepoint\u0026#34;, PERF_TYPE_TRACEPOINT); #ifdef CONFIG_KPROBE_EVENTS perf_pmu_register(\u0026amp;perf_kprobe, \u0026#34;kprobe\u0026#34;, -1); #endif #ifdef CONFIG_UPROBE_EVENTS perf_pmu_register(\u0026amp;perf_uprobe, \u0026#34;uprobe\u0026#34;, -1); #endif } 事件调度与 Multiplexing\nContext 调度 每个 perf_event_context 维护两组红黑树：\n组 说明 pinned_groups 钉住组：必须在 PMU 上始终运行，失败则 events 返回 0 flexible_groups 弹性组：硬件计数器不够时，按时间片轮转 (round-robin multiplexing) 1 2 3 4 5 6 7 8 9 10 11 // perf_event_context 结构（概念） struct perf_event_context { struct pmu *pmu; struct rb_root pinned_groups; // 红黑树: 固定事件组 struct rb_root flexible_groups; // 红黑树: 弹性事件组 struct list_head event_list; // 所有事件链表 struct list_head pinned_active; // 正在硬件上跑的固定组 struct list_head flexible_active; // 正在硬件上跑的弹性组 u64 time; // 上下文累计使能时间 u64 timestamp; // 上次更新时间戳 }; Multiplexing 缩放公式 当硬件计数器不够时，弹性组事件被轮转调度。用户态需要用时间比例缩放读数：\n1 scaled_count = raw_count × (time_enabled / time_running) 调度流程 flowchart TD A[\u0026#34;需要调度 ctx (add/del/enable/disable/sched_in)\u0026#34;] --\u0026gt; B[\u0026#34;ctx_sched_out() 切出旧ctx\u0026#34;] B --\u0026gt; C[\u0026#34;遍历pinned_active: pmu-\u0026gt;del() 从硬件移除\u0026#34;] C --\u0026gt; D[\u0026#34;遍历flexible_active: pmu-\u0026gt;del() 从硬件移除\u0026#34;] D --\u0026gt; E[\u0026#34;ctx_sched_in() 切入新ctx\u0026#34;] E --\u0026gt; F[\u0026#34;遍历pinned_groups: pmu-\u0026gt;add() 尝试分配硬件计数器\u0026#34;] F --\u0026gt; G{\u0026#34;pinned 全部分配成功?\u0026#34;} G --\u0026gt;|\u0026#34;否\u0026#34;| H[\u0026#34;pinned事件返回0计数\u0026#34;] G --\u0026gt;|\u0026#34;是\u0026#34;| I[\u0026#34;遍历flexible_groups: pmu-\u0026gt;add() 剩余计数器\u0026#34;] I --\u0026gt; J{\u0026#34;有事件未分配?\u0026#34;} J --\u0026gt;|\u0026#34;是\u0026#34;| K[\u0026#34;按时间片轮转 (multiplexing)\u0026lt;br/\u0026gt;记录time_enabled/time_running\u0026#34;] J --\u0026gt;|\u0026#34;否\u0026#34;| L[\u0026#34;所有事件在硬件上运行\u0026#34;] 数据流总结图 flowchart TD subgraph 计数模式 C1[\u0026#34;pmu-\u0026gt;start()\u0026#34;] --\u0026gt; C2[\u0026#34;硬件计数器累加\u0026#34;] C2 --\u0026gt; C3[\u0026#34;read(): pmu-\u0026gt;read()\u0026#34;] C3 --\u0026gt; C4[\u0026#34;armpmu_event_update()\u0026lt;br/\u0026gt;delta = curr - prev_count\u0026#34;] C4 --\u0026gt; C5[\u0026#34;local64_add(delta, \u0026amp;event-\u0026gt;count)\u0026#34;] C5 --\u0026gt; C6[\u0026#34;返回给用户态\u0026#34;] end subgraph 采样模式 S1[\u0026#34;硬件计数器溢出\u0026#34;] --\u0026gt; S2[\u0026#34;PMU IRQ\u0026#34;] S2 --\u0026gt; S3[\u0026#34;armv8pmu_handle_irq()\u0026#34;] S3 --\u0026gt; S4[\u0026#34;perf_event_overflow()\u0026#34;] S4 --\u0026gt; S5[\u0026#34;overflow_handler()\u0026#34;] S5 --\u0026gt; S6[\u0026#34;perf_output_begin()\u0026lt;br/\u0026gt;CAS 争夺 ring buffer 空间\u0026#34;] S6 --\u0026gt; S7[\u0026#34;perf_output_sample()\u0026lt;br/\u0026gt;写入 PERF_RECORD_SAMPLE\u0026#34;] S7 --\u0026gt; S8[\u0026#34;perf_output_end()\u0026lt;br/\u0026gt;更新 data_head 唤醒 poll\u0026#34;] end 模式对比表 维度 perf stat (计数) perf record (采样) Tracepoint Kprobe/Uprobe 事件来源 硬件PMC或软件计数器 硬件PMC溢出或hrtimer 内核静态埋点 动态断点 采样? 否 是 可计数/可采样 可计数/可采样 触发机制 read() 读取累计值 溢出中断 → overflow_handler tracepoint回调 kprobe/uprobe回调 Ring Buffer 不用 (或只用于metadata) 用 (写入PERF_RECORD_SAMPLE) 可选用 可选用 开销 极低 (一次read) 中等 (每次溢出) 低 (tracepoint原生) 高 (断点陷入) Overhead类型 rdpmc/read syscall PMU中断+ring buffer写入 函数调用+遍历hlist int3/uprobe异常 ARM64寄存器 PMEVCNTR, PMCCNTR PMOVSR→溢出IRQ — (软件路径) — (软件路径) PMU type HARDWARE/SOFTWARE HARDWARE/SOFTWARE TRACEPOINT 动态分配 初始化路径 perf_event_init 注册PMU 同左 perf_tp_register() perf_tp_register() 关键文件索引 文件 内容 init/main.c:980 perf_event_init() 调用点 kernel/events/core.c:13258 perf_event_init() 定义 kernel/events/core.c:11902 perf_event_open() syscall kernel/events/core.c:11210 perf_init_event() PMU匹配 kernel/events/core.c:9204 perf_swevent_event() 软件事件汇聚 kernel/events/core.c:9087 __perf_event_overflow() 溢出处理 kernel/events/core.c:9610 perf_trace_run_bpf_submit() tracepoint→BPF→perf kernel/events/core.c:9709 perf_tracepoint PMU 定义 kernel/events/core.c:9761 perf_kprobe PMU 定义 kernel/events/core.c:9820 perf_uprobe PMU 定义 kernel/events/core.c:9861 perf_tp_register() kernel/events/core.c:4354 __perf_event_read() 计数器读取 kernel/events/core.c:5227 __perf_event_read_value() 读值（含继承） kernel/events/ring_buffer.c:149 __perf_output_begin() ring buffer 空间分配 kernel/events/internal.h:13 perf_buffer 结构体定义 arch/arm64/kernel/perf_event.c:749 armv8pmu_handle_irq() ARM64 PMU中断处理 arch/arm64/kernel/perf_event.c:1021 __armv8pmu_probe_pmu() ARM64 PMU探测 arch/arm64/kernel/perf_event.c:1253 armv8_pmu_driver_init() ARM64 PMU驱动入口 drivers/perf/arm_pmu.c ARM PMU 通用框架（alloc/register/irq） include/uapi/linux/perf_event.h:320 perf_event_attr UAPI 定义 tools/include/uapi/linux/perf_event.h:320 UAPI 副本 perf命令 子命令 用途 示例 perf stat 事件计数统计 perf stat -e cycles,instructions ./myapp perf record 采样记录 + perf.data perf record -F 99 -g ./myapp perf report 分析 perf.data perf report -g graph perf top 实时热点 perf top -e cache-misses perf trace 系统调用追踪 perf trace ls perf sched 调度器分析 perf sched latency perf annotate 源码/汇编标注 perf annotate hot_func perf c2c 缓存一致性分析 perf c2c record ./myapp perf kmem slab 分配器追踪 perf kmem stat perf script 原始转储 perf script \u0026gt; out.perf perf diff perf.data 差分 perf diff perf.data.old perf.data.new perf 的优势与局限 优势：\n硬件计数器直读——利用 PMU 零开销统计 CPU 周期、cache miss、分支预测等 低采样开销——硬件中断驱动，支持 precise_ip 精确采样 支持双端（用户态 + 内核态）——同时采样用户态和内核态调用链 内核子系统架构清晰——struct perf_event 为中心，PMU 通过 struct pmu 回调抽象，架构扩展性强 生产环境友好——低 overhead，可在负载下运行 上下文切换深度集成——perf 事件调度与 CPU 调度器天然协同 局限：\nPMU 计数器有限——Arm64 通常只有 4-7 个通用计数器和 1 个周期计数器 硬件依赖——某些事件需要架构支持，不同 SoC 实现差异大 采样精度 skid——即使 precise_ip=3，PMU 溢出到记录 PC 之间仍有延迟（SPE 可弥补） 学习曲线——perf_event_attr 配置复杂，子命令繁多 ","date":"2026-07-15T17:17:02+08:00","permalink":"https://charles-7777.github.io/p/linux-perf/","title":"Linux perf"},{"content":" flowchart TD A[Linux 追踪与性能工具全景] --\u0026gt; B[基础机制 / 经典工具] A --\u0026gt; C[内核追踪框架] A --\u0026gt; D[现代可编程平台] B --\u0026gt; B1[ptrace\u0026lt;br\u0026gt;进程调试与追踪的系统调用] B1 --\u0026gt; B2[strace\u0026lt;br\u0026gt;追踪系统调用] B1 --\u0026gt; B3[ltrace\u0026lt;br\u0026gt;追踪动态库函数调用] C --\u0026gt; C1[ftrace\u0026lt;br\u0026gt;内核内置追踪器] C --\u0026gt; C2[perf\u0026lt;br\u0026gt;性能事件与硬件计数器] D --\u0026gt; D1[eBPF\u0026lt;br\u0026gt;内核沙箱程序] D1 --\u0026gt; D2[kprobe / kretprobe\u0026lt;br\u0026gt;动态追踪内核函数] D1 --\u0026gt; D3[uprobe / uretprobe\u0026lt;br\u0026gt;动态追踪用户态函数] D1 --\u0026gt; D4[tracepoint\u0026lt;br\u0026gt;内核预置静态追踪点] ptrace - process trace 核心原理 https://www.man7.org/linux/man-pages/man2/ptrace.2.html\n1 2 3 4 #include \u0026lt;sys/ptrace.h\u0026gt; long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); ptrace() 系统调用(system call)提供了一种机制，使得一个进程（tracer）可以观察和控制另一个进程（tracee）的执行，并检查和更改被追踪者的内存和寄存器。它主要用于实现断点调试和系统调用跟踪。它是一切用户态调试工具的基石——gdb、strace、ltrace 底层都依赖它。\ntracee首先需要被附加到tracer上。附加及其后续命令都是按线程进行的：在一个多线程进程中，每个线程都可以被单独附加到一个（可能是不同的）追踪器上，或者保持未附加状态从而不被调试。因此，“被追踪者”始终指代“（一个）线程”，而绝不是“一个（可能是多线程的）进程” , ptrace 命令始终通过以下形式的调用来发送给特定的被追踪者：ptrace(PTRACE_foo, pid, ...)其中 pid 是对应 Linux 线程的线程 ID\n一个进程可以通过调用 fork() 并让生成的子进程执行 PTRACE_TRACEME，随后（通常）再执行 execve() 来启动跟踪。或者，一个进程可以使用 PTRACE_ATTACH或 PTRACE_SEIZE 开始跟踪另一个进程。\n在被跟踪期间，即使信号被忽略，被追踪者也会在每次收到信号时停止。（SIGKILL 是例外，它会照常生效。）追踪器将在其下一次调用 waitpid()（或相关的“wait”系列系统调用）时收到通知；该调用将返回一个状态值，其中包含指示被追踪者停止原因的信息。当被追踪者停止时，追踪器可以使用各种 ptrace 请求来检查和修改被追踪者。然后，追踪器使被追踪者继续执行，并可以选择忽略已传递的信号。\nsequenceDiagram participant Tracer as Tracer (父进程) participant Kernel as Linux Kernel participant Tracee as Tracee (子进程) Tracer-\u0026gt;\u0026gt;Kernel: ptrace(PTRACE_TRACEME) Tracer-\u0026gt;\u0026gt;Kernel: fork() Kernel--\u0026gt;\u0026gt;Tracer: child pid Note over Tracee: 子进程开始执行 Tracee-\u0026gt;\u0026gt;Kernel: execve(\u0026#34;target\u0026#34;) Kernel--\u0026gt;\u0026gt;Tracer: SIGTRAP — 子进程暂停 Tracer-\u0026gt;\u0026gt;Kernel: ptrace(PTRACE_GETREGS, pid, ...) Kernel--\u0026gt;\u0026gt;Tracer: 寄存器内容 Tracer-\u0026gt;\u0026gt;Kernel: ptrace(PTRACE_PEEKDATA, pid, addr, ...) Kernel--\u0026gt;\u0026gt;Tracer: 内存内容 Tracer-\u0026gt;\u0026gt;Kernel: ptrace(PTRACE_SYSCALL, pid, ...) Note over Tracee: 恢复执行，进入系统调用 Kernel--\u0026gt;\u0026gt;Tracer: 系统调用入口/出口 暂停 Tracer-\u0026gt;\u0026gt;Kernel: ptrace(PTRACE_CONT, pid, ...) Note over Tracee: 继续执行 关键请求（request） 请求常量 作用 PTRACE_TRACEME 子进程标记自己为\u0026quot;可被追踪\u0026quot; PTRACE_ATTACH 附加到已运行的进程 PTRACE_DETACH 解除附加 PTRACE_CONT 继续执行 tracee PTRACE_SYSCALL 在系统调用入口/出口暂停 PTRACE_SINGLESTEP 单步执行一条指令 PTRACE_GETREGS/SETREGS 读写寄存器 PTRACE_PEEKDATA/POKEDATA 读写内存 demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 // minimal_ptrace_tracer.c #include \u0026lt;sys/ptrace.h\u0026gt; #include \u0026lt;sys/wait.h\u0026gt; #include \u0026lt;sys/user.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int main(int argc, char *argv[]) { if (argc \u0026lt; 2) { fprintf(stderr, \u0026#34;Usage: %s \u0026lt;program\u0026gt; [args...]\\n\u0026#34;, argv[0]); exit(1); } pid_t pid = fork(); if (pid == 0) { // --- 子进程 (tracee) --- ptrace(PTRACE_TRACEME, 0, NULL, NULL); execvp(argv[1], \u0026amp;argv[1]); // 执行目标程序 perror(\u0026#34;execvp\u0026#34;); exit(1); } // --- 父进程 (tracer) --- int status; waitpid(pid, \u0026amp;status, 0); // 等待 exec 后的 SIGTRAP ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD); // 让 syscall 暂停可识别 struct user_regs_struct regs; int call_count = 0; while (1) { // 进入系统调用 ptrace(PTRACE_SYSCALL, pid, 0, 0); waitpid(pid, \u0026amp;status, 0); if (WIFEXITED(status)) break; // 读取寄存器，获取系统调用号 ptrace(PTRACE_GETREGS, pid, 0, \u0026amp;regs); #ifdef __x86_64__ long syscall_nr = regs.orig_rax; // x86_64 下系统调用号在 orig_rax #else long syscall_nr = regs.orig_eax; // x86 下在 orig_eax #endif printf(\u0026#34;[syscall #%d] nr = %ld\\n\u0026#34;, ++call_count, syscall_nr); // 退出系统调用 ptrace(PTRACE_SYSCALL, pid, 0, 0); waitpid(pid, \u0026amp;status, 0); if (WIFEXITED(status)) break; // 读取返回值 ptrace(PTRACE_GETREGS, pid, 0, \u0026amp;regs); #ifdef __x86_64__ long ret = regs.rax; #else long ret = regs.eax; #endif printf(\u0026#34;[syscall #%d] return = %ld\\n\u0026#34;, call_count, ret); } printf(\u0026#34;--- total syscalls: %d ---\\n\u0026#34;, call_count); return 0; } 编译运行：\n1 2 gcc -o minimal_ptrace_tracer minimal_ptrace_tracer.c ./minimal_ptrace_tracer /bin/ls /tmp 输出片段：\n1 2 3 4 5 6 7 8 [syscall #1] nr = 0 # read [syscall #1] return = 0 [syscall #2] nr = 1 # write [syscall #2] return = 24 [syscall #3] nr = 2 # open [syscall #3] return = 3 ... ​--- total syscalls: 127 --- ubuntu 上 系统调用号可以通过 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 或 man syscalls 查阅。\n1 2 3 4 5 6 7 8 9 10 11 // cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h #ifndef _ASM_UNISTD_64_H #define _ASM_UNISTD_64_H #define __NR_read 0 #define __NR_write 1 #define __NR_open 2 #define __NR_close 3 #define __NR_stat 4 #define __NR_fstat 5 #define __NR_lstat 6 ptrace 的局限 每次系统调用产生 4 次上下文切换（tracee → kernel → tracer → kernel → tracee），性能开销极大 每次暂停都会触发 waitpid()，大量系统调用时退化严重 不支持同时追踪多个线程的独立系统调用（需 PTRACE_O_TRACECLONE 等选项配合） ptrace 不是为生产环境设计的——它是调试基础设施，不是性能工具 这就是为什么后来出现了 bpftrace、perf、eBPF 等更现代的工具——它们在内核中直接处理事件，无需反复陷入 tracer 进程。\nptrace 源码 elixir.bootlin.com/glibc\u0026mdash;\u0026ndash;ptrace.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // glibc/sysdeps/unix/sysv/linux/ptrace.c long int ptrace (enum __ptrace_request request, ...) { long int res, ret; va_list ap; pid_t pid; void *addr, *data; va_start (ap, request); pid = va_arg (ap, pid_t); addr = va_arg (ap, void *); data = va_arg (ap, void *); va_end (ap); if (request \u0026gt; 0 \u0026amp;\u0026amp; request \u0026lt; 4) data = \u0026amp;ret; res = INLINE_SYSCALL (ptrace, 4, request, pid, addr, data); if (res \u0026gt;= 0 \u0026amp;\u0026amp; request \u0026gt; 0 \u0026amp;\u0026amp; request \u0026lt; 4) { __set_errno (0); return ret; } return res; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // glibc/sysdeps/unix/sysv/linux/aarch64/sysdep.h INLINE_SYSCALL (ptrace, 4, request, pid, addr, data); # define INLINE_SYSCALL(name, nr, args...) \\ ({ unsigned long _sys_result = INTERNAL_SYSCALL (name, , nr, args); \\ if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0))\\ { \\ __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \\ _sys_result = (unsigned long) -1; \\ } \\ (long) _sys_result; }) # define INTERNAL_SYSCALL(name, err, nr, args...) \\ INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args) #define SYS_ify(syscall_name) (__NR_##syscall_name) # define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \\ ({ long _sys_result; \\ { \\ LOAD_ARGS_##nr (args) \\ register long _x8 asm (\u0026#34;x8\u0026#34;) = (name); \\ asm volatile (\u0026#34;svc 0 // syscall \u0026#34; # name \\ : \u0026#34;=r\u0026#34; (_x0) : \u0026#34;r\u0026#34;(_x8) ASM_ARGS_##nr : \u0026#34;memory\u0026#34;); \\ _sys_result = _x0; \\ } \\ _sys_result; }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // arch/arm64/kernel/sys.c const syscall_fn_t sys_call_table[__NR_syscalls] = { [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall, #include \u0026lt;asm/unistd.h\u0026gt; }; // include/uapi/asm-generic/unistd.h /* kernel/ptrace.c */ #define __NR_ptrace 117 __SYSCALL(__NR_ptrace, sys_ptrace) //对于 ptrace，最终会指向 kernel/ptrace.c 中定义的 sys_ptrace 函数 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data) { ... } flowchart LR A[\u0026#34;用户程序调用 ptrace()\u0026#34;] --\u0026gt; B[\u0026#34;glibc 封装函数\u0026lt;br\u0026gt;处理可变参数\u0026#34;] B --\u0026gt; C[\u0026#34;执行软中断 / 专用指令\u0026lt;br\u0026gt;（如 int $0x80 / syscall）\u0026#34;] C --\u0026gt; D[\u0026#34;CPU 切换至内核态\u0026lt;br\u0026gt;根据系统调用号查找表\u0026#34;] D --\u0026gt; E[\u0026#34;内核系统调用入口\u0026lt;br\u0026gt;（如 kernel/ptrace.c 中的 sys_ptrace）\u0026#34;] E --\u0026gt; F[\u0026#34;执行具体功能分支\u0026lt;br\u0026gt;（如 PTRACE_ATTACH）\u0026#34;] F --\u0026gt; G[\u0026#34;调用内核内部函数\u0026lt;br\u0026gt;（如 ptrace_attach）\u0026#34;] strace - trace system calls and signals strace (1) - Linux manual page - man7.org\nhttps://github.com/strace/strace/blob/master/src/strace.c\nstrace 是 ptrace 最著名的上层封装。它拦截并记录进程发起的所有系统调用（syscall），包括参数、返回值和执行时间。\nstrace 工作流程 graph TD A[ptrace] --\u0026gt; B{选择模式} B --\u0026gt; C[PTRACE_ATTACH attach模式] B --\u0026gt; D[strace启动] C --\u0026gt; E[执行tracer逻辑] E --\u0026gt; F[wait] F --\u0026gt; G[系统调用?] G --\u0026gt; H[ptrace PTRACE_SYSCALL] H --\u0026gt; I[wait] I --\u0026gt; J[等待系统调用结束] J --\u0026gt; K[屏幕输出] D --\u0026gt; L[父进程 fork] L --\u0026gt; M[子进程] L --\u0026gt; N[父进程] M --\u0026gt; O[ptrace PTRACE_TRACEME] O --\u0026gt; P[execv 执行命令] N --\u0026gt; Q[wait] Q --\u0026gt; R[等待system call执行] R --\u0026gt; S[ptrace PTRACE_SYSCALL] S --\u0026gt; T[wait] T --\u0026gt; U[等待system call结束] U --\u0026gt; V[屏幕输出] K --\u0026gt; W[停止trace?] V --\u0026gt; W W --\u0026gt; X[exit?] strace option 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 strace -h ## Startup Attaches to the process with the process ID pid and begin tracing -p pid -p \u0026#34;$(pidof PROG)\u0026#34; ; -p \u0026#34;$(pidof PROG)\u0026#34; -E var=val Runs the command with the environment variable var=val set for execution ## Tracing -f Traces child processes as they are created by currently traced processes as a result of the fork(), vfork() and clone() system calls. ### 追踪子进程（fork/vfork/clone） strace -f gcc -c main.c ## Filtering -e expr ### 只追踪特定的系统调用（过滤器） strace -e trace=open,read,write ls ### 追踪所有以 open 开头的系统调用 strace -e trace=%file ls ### 追踪网络相关 syscall strace -e trace=%network curl https://example.com ## Output format -o filename ### 将输出保存到文件 strace -o trace.log ls ## Statistics -c Counts time, calls, and errors 实战案例：诊断 \u0026ldquo;No such file\u0026rdquo; 问题 1 2 # 假设程序启动时报错找不到文件 strace -e trace=open,openat,stat,fstat,access ./myapp 2\u0026gt;\u0026amp;1 | grep -i \u0026#34;ENOENT\u0026#34; 输出示例：\n1 openat(AT_FDCWD, \u0026#34;/opt/myapp/config.yaml\u0026#34;, O_RDONLY) = -1 ENOENT (No such file or directory) 实战案例：分析性能瓶颈 1 strace -c -p $(pgrep my_server) 1 2 3 4 5 6 7 8 % time seconds usecs/call calls errors syscall ​------ ----------- ----------- --------- --------- ---------------- 45.32 0.423452 2117 200 poll 32.18 0.300123 150 200 read 12.45 0.116234 58 200 write 10.05 0.093812 93 100 accept4 ​------ ----------- ----------- --------- --------- ---------------- 100.00 0.933621 700 700 0 total 如果 poll 占用 45% 的时间且调用次数不多但每次耗时很长，通常说明程序在等待 I/O 事件——可能是 epoll 超时设置太长。\nstrace 的代价 strace 会使目标进程的运行速度下降 10~100 倍。原因：\nflowchart LR subgraph Normal [正常执行] A1[用户态代码] --\u0026gt;|syscall| B1[内核] B1 --\u0026gt;|返回| A1 end subgraph Strace [strace 下] A2[用户态代码] --\u0026gt;|syscall| C[内核会暂停] C --\u0026gt; D[发送 SIGTRAP] D --\u0026gt; E[strace 进程被调度] E --\u0026gt; F[读取 tracee 寄存器] F --\u0026gt; G[strace 打印到终端] G --\u0026gt;|PTRACE_SYSCALL| C C --\u0026gt;|syscall 执行| C C --\u0026gt; D2[再次发送 SIGTRAP] D2 --\u0026gt; E2[strace 读取返回值] E2 --\u0026gt;|PTRACE_CONT| A2 end 每个系统调用从2 次上下文切换变成 4 次以上的上下文切换 + 额外的 tracer 解码工作。生产环境慎用！\nltrace - A library call tracer ltrace (1) - Linux manual page - man7.org https://github.com/dkogan/ltrace\nltrace就是library trace，相比于strace，可以追踪用户态的动态库函数。原理也是ptrace。那为什么ltrace可以追踪动态库函数呢？程序调用动态库函数需要走ld-linux.so（linux的动态链接器），ltrace会劫持这个跳转过程，从而记录动态库函数信息。\nltrace 拦截并记录进程对共享库（.so）的函数调用，例如 malloc、free、printf、pthread_create 等。\n工作原理 ltrace 的核心机制与 strace 不同——它依赖动态链接器的符号解析机制，通过在 PLT（Procedure Linkage Table）条目上设断点来拦截库函数调用。\nflowchart TD A[目标程序] --\u0026gt;|调用 printf| B[PLT 条目] B --\u0026gt;|首次调用?| C{已解析?} C --\u0026gt;|否| D[\u0026#34;动态链接器\\nld-linux.so\u0026#34;] D --\u0026gt;|解析符号地址| E[GOT 表] E --\u0026gt;|写入实际地址| B C --\u0026gt;|是| F[实际函数] subgraph ltrace介入 L1[\u0026#34;ltrace 在 PLT/GOT 处\\n设 PTRACE_POKEDATA 断点\u0026#34;] L2[\u0026#34;断点触发 → ltrace\\n读取参数\u0026#34;] L3[\u0026#34;恢复执行 → 断点再触发\\n读取返回值\u0026#34;] end B --\u0026gt; L1 L1 --\u0026gt; L2 L2 --\u0026gt; F F --\u0026gt; L3 具体步骤：\nltrace 通过 ptrace(PTRACE_TRACEME) 或 PTRACE_ATTACH 附加到目标进程\n它读取目标进程的 ELF 动态符号表（.dynsym 节）和 PLT 表\n对每个要追踪的库函数，在对应的 GOT/PLT 入口处插入断点（即写入 int3 指令 → 0xCC）\n当目标进程调用该函数时，触发断点 → ltrace 暂停，读取参数\nltrace 将断点临时移除，单步执行真正的函数，再重新下断点来捕获返回值\nltrace 的追踪范围 能追踪 不能追踪 通过 PLT 调用的共享库函数（printf, malloc 等） 内联函数（编译时被展开） 显式 dlsym() 查找的函数调用 静态链接的函数 C++ 的虚函数（通过 vtable 间接调用）——部分支持 不经过 PLT 的内部调用 系统调用（通过退化为 strace 模式 -S） 汇编级别的直接 syscall（syscall/sysenter 指令） 常用命令与实战 基本使用 1 2 3 4 5 6 7 8 # 追踪 ls 的库函数调用 ltrace ls # 只看 C 标准库调用 ltrace -e \u0026#34;libc.so*\u0026#34; ls # 只看 malloc/free ltrace -e malloc+free ls 输出：\n1 2 3 4 5 6 7 8 9 10 11 ls-\u0026gt;strlen(\u0026#34;.\u0026#34;) = 1 ls-\u0026gt;strlen(\u0026#34;..\u0026#34;) = 2 ls-\u0026gt;opendir(\u0026#34;.\u0026#34;) = 0x55a8... ls-\u0026gt;readdir(0x55a8...) = 0x55a8... ls-\u0026gt;strlen(\u0026#34;file1.txt\u0026#34;) = 10 ls-\u0026gt;__errno_location() = 0x7f... ls-\u0026gt;malloc(4096) = 0x55a9... ls-\u0026gt;readdir(0x55a8...) = NULL ls-\u0026gt;closedir(0x55a8...) = 0 ... +++ exited (status 0) +++ 混合追踪——库函数 + 系统调用 1 2 # -S 让 ltrace 同时显示系统调用 ltrace -S ls 2\u0026gt;\u0026amp;1 | head -20 输出同时包含 SYS_xxx（系统调用）和普通库函数：\n1 2 3 4 5 6 7 8 9 10 11 libc_start_main([0x...]) = 0x... SYS_brk(NULL) = 0x55... SYS_access(\u0026#34;/etc/ld.so.preload\u0026#34;, 4) = -2 SYS_openat(0, 0x7f..., 0x20000, 0, 0) = 3 SYS_fstat(3, 0x7ff...) = 0 SYS_mmap(0, 0x220c0, 1, 2050, 3, 0) = 0x7f... SYS_close(3) = 0 ... strlen(\u0026#34;.\u0026#34;) = 1 SYS_getdents64(3, 0x55a..., 32768) = 112 ... 按库或函数名过滤 1 2 3 4 5 6 7 8 9 10 11 # 只追踪 libc 中的调用 ltrace -e \u0026#34;libc.so*\u0026#34; ./myapp # 只追踪数学库的函数 ltrace -e \u0026#34;libm.so*\u0026#34; ./compute # 排除某些函数（不追踪 putchar） ltrace -e \u0026#34;!putchar\u0026#34; ./myapp # 只显示函数名，不显示参数和返回值 ltrace -n 2 ./myapp 实战案例：诊断内存泄漏 1 2 # 统计 malloc/free/realloc 调用次数和内存分配量 ltrace -e malloc+free+realloc -c ./leaky_app 1 2 3 4 5 6 % time seconds usecs/call calls function ------ ----------- ----------- --------- -------------------- 60.32 0.523412 132 3965 malloc 39.68 0.344123 87 3955 free ------ ----------- ----------- --------- -------------------- 100.00 0.867535 7920 total 如果 malloc 比 free 多很多次，基本可以断定有内存泄漏。\n实战案例：排查在哪次库调用中崩溃 1 2 3 4 5 # 追踪并显示调用栈 ltrace -n 3 -o trace.log ./crashy_app # -n 3 显示 3 层调用深度 # -o 输出到文件 ltrace 的局限 仅适用于动态链接的程序——静态链接的程序没有 PLT，ltrace 无法工作 无法追踪不经过 PLT 的函数调用（如 static 函数、内联函数、宏） 对多线程程序的支持不如 strace 完善（-f 选项在 ltrace 中较新，仍有边缘情况） 断点机制本身会引入额外延迟（每次调用多出 2 次 trap） 较新版本的 glibc 使用 VDSO（虚拟动态共享对象）实现的系统调用（如 gettimeofday）可能跳过了 PLT ftrace - function trace function trace是Linux内核提供的trace框架。主要用来追踪linux内核函数的执行流程。ftrace的原理是编译器会在编译内核时给所有函数预埋探针指令，不使用时零开销，需要使用时探针指令走到ftrace的逻辑。所以，从原理上看ftrace也是只能对内核函数做操作和kprobe差不多，但是没有kprobe灵活。\nftrace 能帮我们分析内核特定的事件，譬如调度，中断等，也能帮我们去追踪动态的内核函数，以及这些函数的调用栈还有栈的使用这些。它也能帮我们去追踪延迟，譬如中断被屏蔽，抢占被禁止的时间，以及唤醒一个进程之后多久开始执行的时间。\nftrace - Function Tracer — The Linux Kernel documentation\nftrace.c - kernel/trace/ftrace.c - Linux source code v7.1.3 - Bootlin Elixir Cross Referencer\n工作原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // init/main.c asmlinkage __visible void __init __no_sanitize_address start_kernel(void) { ... trap_init(); mm_init(); // 内存分配器就绪 poking_init(); ftrace_init(); // ← 这里 /* trace_printk can be enabled here */ early_trace_init(); sched_init(); // 调度器初始化 ... } // kernel/trace/ftrace.c void __init ftrace_init(void) { extern unsigned long __start_mcount_loc[]; extern unsigned long __stop_mcount_loc[]; unsigned long count, flags; int ret; local_irq_save(flags); ret = ftrace_dyn_arch_init(); // 架构相关的动态 ftrace 初始化 local_irq_restore(flags); if (ret) goto failed; count = __stop_mcount_loc - __start_mcount_loc; // ↑ 链接器生成的段，记录了所有 -pg 编译时插入的 mcount 调用点 if (!count) { ... goto failed; } last_ftrace_enabled = ftrace_enabled = 1; // 启用 ftrace ret = ftrace_process_locs(NULL, __start_mcount_loc, __stop_mcount_loc); // ↑ 将每个 mcount 调用点的地址记录到动态 ftrace 的哈希表中， // 并将它们初始化为 nop (空操作) set_ftrace_early_filters(); // 应用内核启动参数指定的早期 filter return; failed: ftrace_disabled = 1; // 失败则禁用 ftrace } ftrace_init() 的本质工作就是：把内核中所有函数的动态跟踪入口从 call 替换为 nop，让内核在默认情况下零开销运行，同时准备好数据结构，使得运行时随时可以通过写 tracefs 文件动态激活任意函数的跟踪\n编译器留下的钩子 当内核用 -pg 或 -mfentry 编译时，每个函数的第一条指令都会被编译器插入一个调用; 如果不编译 ftrace → 函数入口没有这个 call\n1 2 3 func: call __fentry__ # 或者 call mcount \u0026lt;函数实际代码\u0026gt; 这个call __fentry__ 是所谓的动态跟踪入口——它是一个空壳，用来让 ftrace \u0026ldquo;挂钩子\n初始化为 nop（零开销） 启动时 ftrace_init() 做的事情就是把这些 call 全部替换成 nop：\n1 2 3 4 5 6 7 初始状态（编译后）: func: call __fentry__ ← 5字节调用指令 ftrace_init 后（默认状态）: func: nop; nop; nop; nop; nop ← 5字节空操作 这样默认情况下内核运行时没有任何额外开销——每个函数入口就是 5 个 nop\n运行时动态 patch 当用户启用跟踪时（例如 echo do_sys_open \u0026gt; /sys/kernel/tracing/set_ftrace_filter）：\n1 2 3 4 5 6 // ftrace 内部用 stop_machine 将指定函数的 nop 替换回 call write_cr3(...) // 同步所有 CPU func: call ftrace_caller ← 再次 patch 成 call，指向 ftrace 框架 \u0026lt;函数实际代码\u0026gt; 这次 call 指向的是 ftrace_caller，它负责调用注册的回调函数（比如记录函数调用日志、追踪器、kprobe 等）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # sudo /lib/modules/6.8.0-134-generic/build/scripts/extract-vmlinux /boot/vmlinuz-6.8.0-134-generic \u0026gt; /tmp/vmlinux # sudo grep -w schedule /boot/System.map-6.8.0-134-generic ffffffff8225d4e0 T schedule # objdump -d --start-address=0xffffffff8225d4e0 --stop-address=0xffffffff8225d510 /tmp/vmlinux ffffffff8225d4e0: e8 2b cc e5 fe call 0xffffffff810ba110 # grep -w ffffffff810ba110 /boot/System.map-6.8.0-134-generic ffffffff810ba110 T __fentry__ # grep FUNCTION_TRACER /boot/config-6.8.0-134-generic # cat /proc/sys/kernel/ftrace_enabled 1 # sudo ls -d /sys/kernel/tracing/trace /sys/kernel/tracing/trace # sudo ls -d /sys/kernel/debug/tracing /sys/kernel/debug/tracing # mount |grep tracefs tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime) tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime) # trace-cmd show flowchart LR subgraph 编译时 A[源码] --\u0026gt;|gcc -pg| B[\u0026#34;每个函数入口插入\\ncall __fentry__\u0026#34;] B --\u0026gt; C[编译后的内核镜像] end subgraph 启动时 C --\u0026gt; D[\u0026#34;ftrace init: 将\\n所有 __fentry__ 替换为 nop\u0026#34;] D --\u0026gt; E[\u0026#34;运行中的内核\\n零开销\u0026#34;] end subgraph 追踪时 F[echo function \u0026gt; current_tracer] F --\u0026gt; G[\u0026#34;ftrace 将匹配函数的\\nnop → call ftrace_caller\u0026#34;] G --\u0026gt; H[\u0026#34;ftrace_caller 收集\\nPC + 时间戳\u0026#34;] H --\u0026gt; I[写入 trace ring buffer] end E --\u0026gt; F ftrace 的源码 ftrace 的源码位于内核目录 kernel/trace/ 下，核心文件包括：\n文件 作用 kernel/trace/ftrace.c 动态插桩的核心——nop↔call __fentry__ 的重写 kernel/trace/trace_functions.c function tracer 的实现 kernel/trace/trace_functions_graph.c function_graph tracer 的实现 kernel/trace/ring_buffer.c 无锁环形缓冲区 kernel/trace/trace_events.c tracepoint 事件系统 include/linux/ftrace.h ftrace API 头文件 常用命令与实战 ftrace 的接口在 /sys/kernel/tracing/ 下。在 4.1 之前，所有 ftrace 追踪控制文件都位于 debugfs 文件系统中，该文件系统通常位于 /sys/kernel/debug/tracing。 为了向后兼容，当挂载 debugfs 文件系统时，tracefs 文件系统将自动挂载/sys/kernel/debug/tracing,位于 tracefs 文件系统中的所有文件也将位于该 debugfs 文件系统目录中.\n关键文件：\n文件 作用 available_tracers 列出可用的 tracer current_tracer 设置/查看当前 tracer available_filter_functions 可追踪的内核函数列表 set_ftrace_filter 过滤要追踪的函数 set_ftrace_notrace 排除某些函数 trace 读取追踪结果 tracing_on 开关追踪（1/0） buffer_size_kb 缓冲区大小 trace_stat/ 函数调用统计 1 2 3 4 5 6 7 8 9 10 11 12 13 root@ubuntu24:/sys/kernel/debug/tracing# cat trace # tracer: nop # # entries-in-buffer/entries-written: 0/0 #P:2 # # _-----=\u0026gt; irqs-off/BH-disabled # / _----=\u0026gt; need-resched # | / _---=\u0026gt; hardirq/softirq # || / _--=\u0026gt; preempt-depth # ||| / _-=\u0026gt; migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | 基础使用：追踪内核函数调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 进入 ftrace 目录 cd /sys/kernel/tracing # 查看可用的 tracer cat available_tracers # 输出: function function_graph nop ... # 启用 function tracer echo function \u0026gt; current_tracer # 开启追踪 echo 1 \u0026gt; tracing_on # 执行一个操作（触发内核调用） ls -la /tmp \u0026gt; /dev/null # 关闭追踪 echo 0 \u0026gt; tracing_on # 查看追踪结果 cat trace | head -30 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -30 # tracer: function # # entries-in-buffer/entries-written: 102555/16376050 #P:2 # # _-----=\u0026gt; irqs-off/BH-disabled # / _----=\u0026gt; need-resched # | / _---=\u0026gt; hardirq/softirq # || / _--=\u0026gt; preempt-depth # ||| / _-=\u0026gt; migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | tail-455584 [000] ...1. 609848.244283: next_uptodate_folio \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244283: set_pte_range \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244283: folio_add_file_rmap_ptes \u0026lt;-set_pte_range tail-455584 [000] ...1. 609848.244283: next_uptodate_folio \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244283: set_pte_range \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244283: folio_add_file_rmap_ptes \u0026lt;-set_pte_range tail-455584 [000] ...1. 609848.244283: next_uptodate_folio \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244284: set_pte_range \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244284: folio_add_file_rmap_ptes \u0026lt;-set_pte_range tail-455584 [000] ...1. 609848.244284: next_uptodate_folio \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244284: set_pte_range \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244284: folio_add_file_rmap_ptes \u0026lt;-set_pte_range tail-455584 [000] ...1. 609848.244284: next_uptodate_folio \u0026lt;-filemap_map_pages tail-455584 [000] ...1. 609848.244284: _raw_spin_unlock \u0026lt;-filemap_map_pages tail-455584 [000] ..... 609848.244284: __rcu_read_unlock \u0026lt;-filemap_map_pages tail-455584 [000] ..... 609848.244284: __rcu_read_unlock \u0026lt;-filemap_map_pages tail-455584 [000] ..... 609848.244284: __rcu_read_unlock \u0026lt;-do_read_fault tail-455584 [000] ..... 609848.244285: __rcu_read_lock \u0026lt;-handle_mm_fault 每行格式：进程名-PID [CPU] 标志 时间戳: 函数名 \u0026lt;- 调用者\nfunction_graph —— 可视化调用链 1 2 3 4 5 echo function_graph \u0026gt; current_tracer echo 1 \u0026gt; tracing_on ls /tmp \u0026gt; /dev/null echo 0 \u0026gt; tracing_on cat trace | head -40 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -40 # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) 1.328 us | mutex_unlock(); 1) 0.180 us | syscall_exit_to_user_mode_prepare(); 1) 0.173 us | fpregs_assert_state_consistent(); 1) | x64_sys_call() { 1) | __x64_sys_dup2() { 1) | ksys_dup3() { 1) 0.162 us | _raw_spin_lock(); 1) 0.159 us | expand_files(); 1) | do_dup2() { 1) 0.167 us | _raw_spin_unlock(); 1) | filp_close() { 1) | filp_flush() { 1) 0.158 us | dnotify_flush(); 1) 0.181 us | locks_remove_posix(); 1) 0.786 us | } 1) | fput() { 1) | task_work_add() { 1) 0.159 us | kick_process(); 1) 0.478 us | } 1) 0.789 us | } 1) 2.024 us | } 1) 2.649 us | } 1) 3.569 us | } 1) 3.871 us | } 1) 4.179 us | } 1) 0.159 us | syscall_exit_to_user_mode_prepare(); 1) | task_work_run() { 1) 0.173 us | _raw_spin_lock_irq(); 1) 0.165 us | _raw_spin_unlock_irq(); 1) | ____fput() { 1) | __fput() { 1) 0.158 us | __cond_resched(); 1) 0.163 us | locks_remove_file(); 1) 0.158 us | ima_file_free(); 1) | mutex_lock() { 1) 0.159 us | __cond_resched(); 缩进表示调用深度，DURATION 列是函数执行时间。\n按函数名过滤 1 2 3 4 5 6 7 8 9 10 11 12 # 只追踪与 ext4 文件系统相关的函数 echo ext4_* \u0026gt; set_ftrace_filter # 确认生效 cat set_ftrace_filter # 启用追踪 echo function \u0026gt; current_tracer echo 1 \u0026gt; tracing_on cp bigfile /tmp/ echo 0 \u0026gt; tracing_on cat trace | head -30 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -30 # tracer: function # # entries-in-buffer/entries-written: 178/178 #P:2 # # _-----=\u0026gt; irqs-off/BH-disabled # / _----=\u0026gt; need-resched # | / _---=\u0026gt; hardirq/softirq # || / _--=\u0026gt; preempt-depth # ||| / _-=\u0026gt; migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | bash-454555 [001] ..... 610139.068645: ext4_file_getattr \u0026lt;-vfs_getattr_nosec bash-454555 [001] ..... 610139.068646: ext4_getattr \u0026lt;-ext4_file_getattr bash-454555 [001] ..... 610139.068647: ext4_file_getattr \u0026lt;-vfs_getattr_nosec bash-454555 [001] ..... 610139.068647: ext4_getattr \u0026lt;-ext4_file_getattr bash-454555 [001] ..... 610139.068651: ext4_file_getattr \u0026lt;-vfs_getattr_nosec bash-454555 [001] ..... 610139.068651: ext4_getattr \u0026lt;-ext4_file_getattr cp-457239 [000] ..... 610139.069206: ext4_file_open \u0026lt;-do_dentry_open cp-457239 [000] ..... 610139.069206: ext4_sample_last_mounted \u0026lt;-ext4_file_open cp-457239 [001] ..... 610139.069914: ext4_file_read_iter \u0026lt;-__kernel_read cp-457239 [001] ..... 610139.069921: ext4_file_read_iter \u0026lt;-__kernel_read cp-457239 [001] ..... 610139.069922: ext4_file_read_iter \u0026lt;-__kernel_read cp-457239 [001] ..... 610139.069929: ext4_file_open \u0026lt;-do_dentry_open cp-457239 [001] ..... 610139.069930: ext4_sample_last_mounted \u0026lt;-ext4_file_open cp-457239 [001] ..... 610139.069931: ext4_file_read_iter \u0026lt;-__kernel_read cp-457239 [001] ..... 610139.069932: ext4_file_read_iter \u0026lt;-__kernel_read cp-457239 [001] ..... 610139.069934: ext4_xattr_security_get \u0026lt;-__vfs_getxattr cp-457239 [001] ..... 610139.069934: ext4_xattr_get \u0026lt;-ext4_xattr_security_get cp-457239 [001] ..... 610139.069935: ext4_xattr_ibody_get \u0026lt;-ext4_xattr_get tracepoint 事件追踪 ftrace 也支持预定义的 tracepoint 事件（性能远高于动态函数追踪）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 查看可用的事件 ls /sys/kernel/tracing/events/ | head -20 # output: block irq kmem net sched syscalls ... # 启用系统调用 tracepoint echo 1 \u0026gt; events/syscalls/sys_enter_openat/enable echo 1 \u0026gt; events/syscalls/sys_exit_openat/enable # 追踪 echo 1 \u0026gt; tracing_on cat /etc/passwd \u0026gt; /dev/null echo 0 \u0026gt; tracing_on # 查看 cat trace | tail -10 1 2 bash-454555 [001] ...1. 610338.749840: sys_openat(dfd: ffffff9c, filename: 64ac99b1a560, flags: 241, mode: 1b6) bash-454555 [001] ...1. 610338.749857: sys_openat -\u0026gt; 0x3 实战案例：排查内核函数频繁调用导致的性能问题 1 2 3 4 5 6 7 8 9 # 统计函数调用次数（类似 strace -c 的内核版本） echo nop \u0026gt; current_tracer echo 1 \u0026gt; /sys/kernel/tracing/events/syscalls/sys_enter_write/enable # 等待一段时间 sleep 5 # 查看计数 cat /sys/kernel/tracing/per_cpu/cpu0/stats 1 2 3 4 5 6 7 8 9 root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/per_cpu/cpu0/stats entries: 0 overrun: 0 commit overrun: 0 bytes: 0 oldest event ts: 610338.745551 now ts: 610533.149505 dropped events: 0 read events: 0 或者使用 trace_stat：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 设置 tracer 为 function echo function \u0026gt; /sys/kernel/tracing/current_tracer # 2. 开启 function profiler（这一步会重置计数器） echo 1 \u0026gt; /sys/kernel/tracing/function_profile_enabled # 3. 开始追踪 echo 1 \u0026gt; /sys/kernel/tracing/tracing_on # --- 运行您的测试负载 --- # 例如: ./your_benchmark_tool # 或者简单的压力测试: stress-ng --cpu 4 --timeout 5s stress-ng --cpu 4 --timeout 5s # 4. 停止追踪 echo 0 \u0026gt; /sys/kernel/tracing/tracing_on # 5. 关闭 profiler echo 0 \u0026gt; /sys/kernel/tracing/function_profile_enabled # 6. 查看函数调用统计排名 cat /sys/kernel/tracing/trace_stat/functions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/trace_stat/function0 Function Hit Time Avg s^2 -------- --- ---- --- --- root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/trace_stat/function1 Function Hit Time Avg s^2 -------- --- ---- --- --- ext4_lookup 4 125.270 us 31.317 us 522.599 us ext4_dx_find_entry 2 96.880 us 48.440 us 53.892 us ext4_search_dir 4 86.536 us 21.634 us 530.259 us ext4_match 286 62.061 us 0.216 us 0.001 us ext4_getblk 6 18.148 us 3.024 us 5.078 us ext4_bread_batch 2 10.719 us 5.359 us 16.791 us ext4_map_blocks 6 9.737 us 1.622 us 1.034 us ext4_bread 4 9.589 us 2.397 us 0.034 us ext4_fname_prepare_lookup 4 4.269 us 1.067 us 0.250 us ext4_inode_block_valid 6 4.054 us 0.675 us 0.186 us ext4_es_lookup_extent 6 2.444 us 0.407 us 0.046 us ext4_sb_block_valid 6 2.021 us 0.336 us 0.068 us ext4_fname_free_filename 4 1.140 us 0.285 us 0.015 us ext4_fname_from_fscrypt_name 4 1.016 us 0.254 us 0.005 us ext4_fname_setup_ci_filename 4 0.897 us 0.224 us 0.003 us ext4_htree_next_block 2 0.478 us 0.239 us 0.000 us 实战案例：追踪内核模块初始化 1 2 3 4 5 6 7 # 只追踪某个模块的函数 echo :mod:ext4 \u0026gt; set_ftrace_filter echo function_graph \u0026gt; current_tracer echo 1 \u0026gt; tracing_on mount /dev/sdb1 /mnt echo 0 \u0026gt; tracing_on cat trace | head -20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a5938f, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29140, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29690, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29be0, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a2a110, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a2a6b0, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a839cd1db, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0x3 */ 1) | /* sys_openat(dfd: ffffff9c, filename: 716a837d48b0, flags: 80000, mode: 0) */ 1) | /* sys_openat -\u0026gt; 0xfffffffffffffffe */ ftrace 的优势与局限 优势：\n零用户态开销——所有工作在内核态完成，没有 ptrace 的上下文切换 无侵入——动态函数插桩，不干扰目标函数执行 支持生产环境——可在负载下运行（但需注意 ring buffer 溢出） 追踪整个内核——不仅是用户进程的上下文，还包括中断、软中断、调度等 function_graph 模式——天然支持调用链可视化 局限：\n只能追踪内核态——不能观察用户态代码 使用门槛高——通过 tracefs文件系统操作，需要了解内核函数名和子系统 function tracer 的开销——每秒数万次函数调用时仍会产生 ~5-10% 的性能损失 ring buffer 溢出——高负载下可能丢失事件（overwrite 模式） 调试能力有限——不能像 ptrace 那样读/写内存和寄存器 综合调试案例 一个 HTTP 服务器响应慢 假设有一个 http_server 响应缓慢，我们进行分层排查：\nStep 1: 整体概览 — strace -c\n1 strace -c -p $(pgrep http_server | head -1) 结果：\n1 2 3 4 5 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 90.12 8.123456 40123 202 poll 5.23 0.471234 123 382 writev 4.65 0.419001 110 380 readv → 90% 的时间花在 poll 上，且每次平均 40ms → 可能在等待 I/O 超时。\nStep 2: 查看 poll 的细节\n1 strace -e trace=poll -p $(pgrep http_server | head -1) -ttt 2\u0026gt;\u0026amp;1 1 2 3 4 1728000001.123456 poll([{fd=4, events=POLLIN}], 1, 50000) = 1 1728000001.123512 poll([{fd=4, events=POLLIN}], 1, 50000) = 1 1728000001.125678 poll([{fd=4, events=POLLIN}], 1, 50000) = 0 1728000001.175678 poll([{fd=4, events=POLLIN}], 1, 50000) = 1 观察发现：poll 超时设置为 50ms，有些调用确实等到超时返回了 = 0（没有事件）→ 说明上游请求不够频繁，或 keepalive 空转。\nStep 3: 查看内存分配模式\n1 ltrace -e malloc+free -c -p $(pgrep http_server | head -1) 1 2 3 4 % time seconds usecs/call calls function ------ ----------- ----------- --------- -------------------- 55.32 0.523412 132 3965 malloc 44.68 0.344123 87 3955 free → malloc/free 数量接近，不太像泄漏；但调用次数多（4000+），可以考虑引入对象池。\nStep 4: 查看内核态行为 — ftrace\n如果怀疑是内核调度或文件系统层面的问题：\n1 2 3 4 5 6 7 8 9 cd /sys/kernel/tracing # 追踪 http_server 相关的系统调用内核路径 echo do_sys_poll \u0026gt; set_ftrace_filter echo function_graph \u0026gt; current_tracer echo 1 \u0026gt; tracing_on # ... 复现问题 ... echo 0 \u0026gt; tracing_on cat trace | grep \u0026#34;http_serv\u0026#34; | head -30 用 strace 排查\u0026quot;命令找不到\u0026rdquo; 1 2 3 4 5 $ which some_cmd /usr/local/bin/some_cmd $ some_cmd bash: some_cmd: command not found # ？？？ 用 strace 追踪 shell：\n1 strace -e trace=execve,stat,openat bash -c \u0026#34;some_cmd\u0026#34; 2\u0026gt;\u0026amp;1 1 2 3 4 5 6 7 execve(\u0026#34;/usr/local/bin/some_cmd\u0026#34;, ...) = -1 ENOEXEC execve(\u0026#34;/bin/some_cmd\u0026#34;, ...) = -1 ENOENT openat(AT_FDCWD, \u0026#34;/usr/local/bin/some_cmd\u0026#34;, O_RDONLY) = 3 read(3, \u0026#34;#! /usr/bin/python3\\n\u0026#34;, 80) = 23 close(3) execve(\u0026#34;/usr/local/bin/some_cmd\u0026#34;, ...) = -1 ENOEXEC ... → 原来是写错了，脚本开头是 #! /usr/bin/python3（多了空格）而不是正确的 #!/usr/bin/python3，导致内核不识别为解释器脚本。\n用 ltrace 定位 crash 前的最后一次库调用 1 2 3 4 5 # 程序运行时突然崩溃 ltrace -n 5 -o /tmp/ltrace.log ./myapp # 查看日志尾部 tail -20 /tmp/ltrace.log 1 2 3 4 5 myapp-\u0026gt;free(0x55a8...) = \u0026lt;void\u0026gt; myapp-\u0026gt;strlen(\u0026#34;data\u0026#34;) = 4 myapp-\u0026gt;malloc(512) = 0x55a9... myapp-\u0026gt;memcpy(0x55a9..., 0x55a8..., 1024) = 0x55a9... --- SIGSEGV (Segmentation fault) --- → 注意到 malloc(512) 但 memcpy 拷贝了 1024 字节 ——缓冲区溢出导致在 memcpy 内部触发 SIGSEGV。\n用 ftrace 排查内核调度延迟 1 2 3 4 5 6 7 8 9 10 11 12 13 14 cd /sys/kernel/tracing # 追踪调度器唤醒延迟 echo 0 \u0026gt; tracing_on echo \u0026gt; trace echo __schedule \u0026gt; set_ftrace_filter echo function_graph \u0026gt; current_tracer echo 1 \u0026gt; tracing_on sleep 0.1 # 触发调度 echo 0 \u0026gt; tracing_on cat trace 输出：\n1 2 3 4 5 6 7 8 0) 0.125 us | __schedule(); 0) 0.062 us | pick_next_task_fair(); 0) 0.187 us | pick_next_entity(); 0) 0.125 us | set_next_entity(); 0) 0.062 us | context_switch(); 0) 0.187 us | switch_mm_irqs_off(); 0) 0.125 us | switch_to(); 0) 4.562 us | } /* __schedule */ 可以清晰看到一次上下文切换在内核中各子函数的耗时分布。\n维度 strace ltrace ftrace ptrace 追踪粒度 系统调用 共享库函数 内核函数 + tracepoint 系统调用 + 信号 + 内存 追踪层级 用户态边界 用户态 内核态 用户态/内核态边界 是否需要 ptrace ✅ 是 ✅ 是 ❌ 内核内置 本身就是 是否需要源码 否 否 否（需内核符号） 否 侵入性 高（进程挂起） 高（断点指令） 低（nop 替换） 极高 性能影响 10~100x 降速 5~50x 降速 ~5-10%（function tracer） 取决于使用模式 典型场景 排障、审计、教学 内存问题、库分析 内核调试、性能分析 调试器开发、逆向 启动方式 命令行 命令行 tracefs + echo 系统调用 API 生产环境可用 ❌ ❌ ✅ 可用（慎） ❌ 多线程支持 ✅ -f ⚠️ 部分 ✅ 天然 per-CPU ⚠️ 需额外选项 调用栈/图 ❌ 没有 ❌ 没有 ✅ function_graph ❌ 需要自行实现 eBPF \u0026amp; Cilium 文档 — ptrace 的现代替代方案 LWN: ftrace 简介 — Steven Rostedt 的原创文章 KernelShark — ftrace 的 GUI 可视化工具 ","date":"2026-07-15T00:05:18+08:00","permalink":"https://charles-7777.github.io/p/linux-trace--ptrace-strace-ltrace-ftrace/","title":"linux trace -- ptrace strace ltrace ftrace"},{"content":"Kernel Network Layers OSI 7 Layer Model Linux 网络实现 Application User space - Application Presentation User space - Application Session User space - Application Transport Kernel - L4 (TCP/UDP, \u0026hellip;) Network Kernel - L3 (IPv4, IPv6) Data Link Kernel - L2 Physical Hardware - Physical Data Structure Network Device Defines an instance of network interface Tracks the state information of all network interfaces Large structure, consisting device parameters like: IRQ number MAC address MTU Driver callback operations Name of the device (Like eth0, eth1, wifi0) Flags of the device (Status of the device like up or down) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // include/linux/netdevice.h struct net_device { char name[IFNAMSIZ]; ... int irq; ... int ifindex; ... struct net_device_stats stats; ... const struct net_device_ops *netdev_ops; ... unsigned int flags; ... unsigned int mtu; ... unsigned char *dev_addr; ... }; Register/Unresgiter Callbacks Network Device APIs Allocate and Free network device Register and Unregister network device 1 2 3 4 5 6 7 8 9 10 11 12 13 #define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \\ alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1) struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name, unsigned name_assign_type, void (*setup)(struct net_device *), unsigned int txqs, unsigned int rxqs); void free_netdev(struct net_device *dev); int register_netdev(struct net_device *dev); void unregister_netdev(struct net_device *dev); Socket Buffer sk_buff – Represents an incoming or outgoing packet It is the core data structure of Linux network stack. Understanding of this structure and its APIs is important. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct sk_buff { ... struct sock *sk; struct net_device *dev; ... __u8 pkt_type:3; ... __be16 protocol; __u16 transport_header; __u16 network_header; __u16 mac_header; ... sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; ... }; Network Inter Submodule Communication Socket Initialization 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // net/socket.c static int __init sock_init(void) { int err; /* * Initialize the network sysctl infrastructure. */ err = net_sysctl_init(); if (err) goto out; /* * Initialize skbuff SLAB cache */ skb_init(); /* * Initialize the protocols module. */ init_inodecache(); err = register_filesystem(\u0026amp;sock_fs_type); if (err) goto out_fs; sock_mnt = kern_mount(\u0026amp;sock_fs_type); if (IS_ERR(sock_mnt)) { err = PTR_ERR(sock_mnt); goto out_mount; } ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int sock_register(const struct net_proto_family *ops) { int err; if (ops-\u0026gt;family \u0026gt;= NPROTO) { pr_crit(\u0026#34;protocol %d \u0026gt;= NPROTO(%d)\\n\u0026#34;, ops-\u0026gt;family, NPROTO); return -ENOBUFS; } spin_lock(\u0026amp;net_family_lock); if (rcu_dereference_protected(net_families[ops-\u0026gt;family], lockdep_is_held(\u0026amp;net_family_lock))) err = -EEXIST; else { rcu_assign_pointer(net_families[ops-\u0026gt;family], ops); err = 0; } spin_unlock(\u0026amp;net_family_lock); pr_info(\u0026#34;NET: Registered protocol family %d\\n\u0026#34;, ops-\u0026gt;family); return err; } 1 2 3 4 5 6 7 8 9 10 11 12 void sock_unregister(int family) { BUG_ON(family \u0026lt; 0 || family \u0026gt;= NPROTO); spin_lock(\u0026amp;net_family_lock); RCU_INIT_POINTER(net_families[family], NULL); spin_unlock(\u0026amp;net_family_lock); synchronize_rcu(); pr_info(\u0026#34;NET: Unregistered protocol family %d\\n\u0026#34;, family); } Inet Initialization 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // net/ipv4/af_inet.c static int __init inet_init(void) { struct inet_protosw *q; struct list_head *r; int rc = -EINVAL; sock_skb_cb_check_size(sizeof(struct inet_skb_parm)); rc = proto_register(\u0026amp;tcp_prot, 1); if (rc) goto out; rc = proto_register(\u0026amp;udp_prot, 1); if (rc) goto out_unregister_tcp_proto; rc = proto_register(\u0026amp;raw_prot, 1); if (rc) goto out_unregister_udp_proto; rc = proto_register(\u0026amp;ping_prot, 1); if (rc) goto out_unregister_raw_proto; /* *\tTell SOCKET that we are alive... */ (void)sock_register(\u0026amp;inet_family_ops); ... static const struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create, .owner\t= THIS_MODULE, }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // net/ipv6/af_inet6.c static int __init inet6_init(void) { struct list_head *r; int err = 0; sock_skb_cb_check_size(sizeof(struct inet6_skb_parm)); /* Register the socket-side information for inet6_create. */ for (r = \u0026amp;inetsw6[0]; r \u0026lt; \u0026amp;inetsw6[SOCK_MAX]; ++r) INIT_LIST_HEAD(r); if (disable_ipv6_mod) { pr_info(\u0026#34;Loaded, but administratively disabled, reboot required to enable\\n\u0026#34;); goto out; } err = proto_register(\u0026amp;tcpv6_prot, 1); if (err) goto out; err = proto_register(\u0026amp;udpv6_prot, 1); if (err) goto out_unregister_tcp_proto; err = proto_register(\u0026amp;udplitev6_prot, 1); if (err) goto out_unregister_udp_proto; err = proto_register(\u0026amp;rawv6_prot, 1); if (err) goto out_unregister_udplite_proto; err = proto_register(\u0026amp;pingv6_prot, 1); if (err) goto out_unregister_ping_proto; /* We MUST register RAW sockets before we create the ICMP6, * IGMP6, or NDISC control sockets. */ err = rawv6_init(); if (err) goto out_unregister_raw_proto; /* Register the family here so that the init calls below will * be able to create sockets. (?? is this dangerous ??) */ err = sock_register(\u0026amp;inet6_family_ops); ... static const struct net_proto_family inet6_family_ops = { .family = PF_INET6, .create = inet6_create, .owner\t= THIS_MODULE, }; sock_register 的作用是 向 VFS socket 层注册一个协议族（protocol family），让用户态能够通过 socket 系统调用创建该协议族对应的 socket。\nInitialization Order 1 2 3 core_initcall(sock_init); /* early initcall */ fs_initcall(inet_init); 内核模块有一个函数叫module_init, 你可以指定你的内核模块的入口函数, 所以当你在使用insmod命令来加载内核模块的时候,那个函数将被kernel 调用。类似地，如果你把函数指定为initcall或fsinitcall，那就意味着你在告诉内核，当你启动的时候，要调用这个叫这个函数的函数。那么现在，这个面板要怎么决定是先处理sock还是先处理inet呢?\nSocket will be initialized first; Next, Inet will will be initialized\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // include\\linux\\init.h /* * Early initcalls run before initializing SMP. * * Only for built-in code, not modules. */ #define early_initcall(fn)\t__define_initcall(fn, early) /* * A \u0026#34;pure\u0026#34; initcall has no dependencies on anything else, and purely * initializes variables that couldn\u0026#39;t be statically initialized. * * This only exists for built-in code, not for modules. * Keep main.c:initcall_level_names[] in sync. */ #define pure_initcall(fn)\t__define_initcall(fn, 0) #define core_initcall(fn)\t__define_initcall(fn, 1) #define core_initcall_sync(fn)\t__define_initcall(fn, 1s) #define postcore_initcall(fn)\t__define_initcall(fn, 2) #define postcore_initcall_sync(fn)\t__define_initcall(fn, 2s) #define arch_initcall(fn)\t__define_initcall(fn, 3) #define arch_initcall_sync(fn)\t__define_initcall(fn, 3s) #define subsys_initcall(fn)\t__define_initcall(fn, 4) #define subsys_initcall_sync(fn)\t__define_initcall(fn, 4s) #define fs_initcall(fn)\t__define_initcall(fn, 5) #define fs_initcall_sync(fn)\t__define_initcall(fn, 5s) #define rootfs_initcall(fn)\t__define_initcall(fn, rootfs) #define device_initcall(fn)\t__define_initcall(fn, 6) #define device_initcall_sync(fn)\t__define_initcall(fn, 6s) #define late_initcall(fn)\t__define_initcall(fn, 7) #define late_initcall_sync(fn)\t__define_initcall(fn, 7s) Socket Create Socket指的是套接字结构，而sock则属于数据包中的内容。sk_buff中包含一个指向sock结构的指针,不是结构体socket.\n这个结构体socket也维护了一个sock的参数,但这个套接字用于套接字API和协议，即注册到套接字\nstruct socket 中有const struct proto_ops *ops， 这里proto_ops是目前已经注册了的 协议 ：TCP、UDP等等\n但在sock结构体中，这主要与套接字缓冲区相关，并且它和IP层或者链路层是有关联的。当数据包正在被接收或者正在被发送出去的时候，这个sk才会被关联。\n同样，如果这个数据包是从你的机器转发出去的，那么这个sk参数就会是空的，如果你的机器不是这里的终端设备也不是这里的节点设备的话：如果它是一台router类型的设备，那么sk这个变量就不会是空的，而是会指向那个套接字。只有当数据包是从这台特定的任务机器上生成时，才会出现这种情况。\n所以这两个结构体，在我们维护数据包或者收发数据包的时候，也很重要。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // include/linux/net.h struct socket { socket_state state; kmemcheck_bitfield_begin(type); short type; kmemcheck_bitfield_end(type); unsigned long flags; ... struct file *file; struct sock *sk; const struct proto_ops *ops; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // include/net/sock.h struct sock { struct sk_buff_head sk_receive_queue; int sk_rcvbuf; unsigned long sk_flags; int sk_sndbuf; struct sk_buff_head sk_write_queue; ... unsigned int sk_shutdown : 2, sk_no_check : 2, sk_protocol : 8, sk_type : 16; ... void (*sk_data_ready)(struct sock *sk, int bytes); void (*sk_write_space)(struct sock *sk); }; Socket Create \u0026ndash; Cont 用户空间socket层的一些APIs：套接字绑定、连接、监听、读取、写入、发送。每个调用在内核控制台里都有一个对应的函数。socket 对应 sys_socket，bind 对应 sys_bind，bind映射到sys_bind，connect映射到sys_connect等。所以，每当用户空间去调用对应的socket时，这个函数就会被触发调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // net\\socket.c SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; ... retval = sock_create(family, type, protocol, \u0026amp;sock); if (retval \u0026lt; 0) goto out; ... retval = sock_map_fd(sock, flags \u0026amp; (O_CLOEXEC | O_NONBLOCK)); if (retval \u0026lt; 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // socket 也是一个文件 /** *\tsock_alloc\t-\tallocate a socket * *\tAllocate a new inode and socket object. The two are bound together *\tand initialised. The socket is then returned. If we are out of inodes *\tNULL is returned. */ static struct socket *sock_alloc(void) { struct inode *inode; struct socket *sock; inode = new_inode_pseudo(sock_mnt-\u0026gt;mnt_sb); if (!inode) return NULL; sock = SOCKET_I(inode); kmemcheck_annotate_bitfield(sock, type); inode-\u0026gt;i_ino = get_next_ino(); inode-\u0026gt;i_mode = S_IFSOCK | S_IRWXUGO; inode-\u0026gt;i_uid = current_fsuid(); inode-\u0026gt;i_gid = current_fsgid(); inode-\u0026gt;i_op = \u0026amp;sockfs_inode_ops; this_cpu_add(sockets_in_use, 1); return sock; } ","date":"2026-07-12T21:07:45+08:00","permalink":"https://charles-7777.github.io/p/linux-network-layer-%E4%B8%8Esocket/","title":"Linux network layer 与socket"},{"content":"Netfilter Framework(ipv4) graph LR %% 定义节点样式 classDef hook fill:#d9ead3,stroke:#6aa84f,stroke-width:2px; classDef process fill:#cfe2f3,stroke:#3d85c6,stroke-width:2px; classDef routing fill:#e6e6e6,stroke:#666666,stroke-width:2px; %% 主流程节点 NetIn[\u0026#34;Network Interface\u0026lt;br\u0026gt;(Input)\u0026#34;]:::process PreRouting(\u0026#34;PRE_ROUTING\u0026#34;):::hook RoutingDec1[\u0026#34;Routing Decision\u0026#34;]:::routing LocalIn(\u0026#34;LOCAL_IN\u0026#34;):::hook HigherLayers[\u0026#34;Higher Layers\u0026lt;br\u0026gt;Local Processes\u0026#34;]:::process Forward(\u0026#34;FORWARD\u0026#34;):::hook LocalOut(\u0026#34;LOCAL_OUT\u0026#34;):::hook RoutingDec2[\u0026#34;Routing Decision\u0026#34;]:::routing PostRouting(\u0026#34;POST_ROUTING\u0026#34;):::hook NetOut[\u0026#34;Network Interface\u0026lt;br\u0026gt;(Output)\u0026#34;]:::process %% 连接关系 NetIn --\u0026gt; PreRouting PreRouting --\u0026gt; RoutingDec1 RoutingDec1 --\u0026gt; LocalIn RoutingDec1 --\u0026gt; Forward LocalIn --\u0026gt; HigherLayers HigherLayers --\u0026gt; LocalOut LocalOut --\u0026gt; RoutingDec2 RoutingDec2 --\u0026gt; PostRouting Forward --\u0026gt; PostRouting PostRouting --\u0026gt; NetOut %% 模拟原图箭头颜色 %% Receive packet (红色路径) linkStyle 0 stroke:#ff0000,stroke-width:2px; linkStyle 1 stroke:#ff0000,stroke-width:2px; linkStyle 2 stroke:#ff0000,stroke-width:2px; linkStyle 3 stroke:#ff0000,stroke-width:2px; %% Transmit packet (蓝色路径) linkStyle 4 stroke:#0000ff,stroke-width:2px; linkStyle 5 stroke:#0000ff,stroke-width:2px; linkStyle 6 stroke:#0000ff,stroke-width:2px; linkStyle 7 stroke:#0000ff,stroke-width:2px; linkStyle 8 stroke:#0000ff,stroke-width:2px; linkStyle 9 stroke:#0000ff,stroke-width:2px; Hook Structure 1 2 3 4 5 6 7 8 9 10 struct nf_hook_ops { /* User fills in from here down. */ nf_hookfn\t*hook; struct net_device\t*dev; void\t*priv; u_int8_t\tpf; unsigned int\thooknum; /* Hooks are ordered in ascending priority. */ int\tpriority; }; hook callback function\nIt is a callback function Signature depends on kernel version. 1 2 3 typedef unsigned int nf_hookfn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state); callback response\n1 2 3 4 5 6 7 8 9 10 //netfilter.h /* Responses from hook functions. */ #define NF_DROP 0 #define NF_ACCEPT 1 #define NF_STOLEN 2 #define NF_QUEUE 3 #define NF_REPEAT 4 #define NF_STOP 5\t/* Deprecated, for userspace nf_queue compatibility. */ #define NF_MAX_VERDICT NF_STOP Hook Chain NF_DROP Drop the packet NF_ACCEPT Continue normal traversal NF_STOLEN Current hook function will take care of the packet. Don’t continue traversal NF_QUEUE Queue the packet (usually for user space handling) NF_REPEAT Call this hook again NF_STOP Terminate the hook chain processing without releasing sk_buff data priority Callbacks of same hook point are called in ascending order of priority 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //netfilter_ipv4.h enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_HELPER = 300, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, }; Hook Position in Linux Network Stack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // netfilter_ipv4.h /* IP Hooks */ /* After promisc drops, checksum checks. */ #define NF_IP_PRE_ROUTING\t0 /* If the packet is destined for this box. */ #define NF_IP_LOCAL_IN\t1 /* If the packet is destined for another interface. */ #define NF_IP_FORWARD\t2 /* Packets coming from a local process. */ #define NF_IP_LOCAL_OUT\t3 /* Packets about to hit the wire. */ #define NF_IP_POST_ROUTING\t4 #define NF_IP_NUMHOOKS\t5 在 kernel v2.6.25之后 ，将原本仅用于 IPv4 的 NF_IP_XXX 枚举合并到了统一的 NF_INET_XXX 命名空间中，可做兼容处理#if (LINUX_VERSION_CODE \u0026gt;= KERNEL_VERSION(2,6,25))\n1 2 3 4 5 6 7 8 9 10 11 // include/uapi/linux/netfilter.h enum nf_inet_hooks { NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN, NF_INET_FORWARD, NF_INET_LOCAL_OUT, NF_INET_POST_ROUTING, NF_INET_NUMHOOKS, NF_INET_INGRESS = NF_INET_NUMHOOKS, }; PREROUTING Hook File Function NF_INET_PRE_ROUTING net/ipv4/ip_input.c ip_rcv() Works only for incoming packets. Packet is passed to this hook after the simple sanity checks. Before routing decision is made (forwarded or local in). This is the first hook through which incoming packet is passed through. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // net/ipv4/af_inet.c static struct packet_type ip_packet_type __read_mostly = { .type = cpu_to_be16(ETH_P_IP), // 匹配 EtherType = 0x0800 (IPv4) .func = ip_rcv, .list_func = ip_list_rcv, }; static int __init inet_init(void) { ... dev_add_pack(\u0026amp;ip_packet_type);\t//将 ip_packet_type 注册到全局的 ptype_all/ptype_base 哈希表中。 ... } __netif_receive_skb_core() 会根据 skb 的 protocol 字段（ETH_P_IP）在 ptype_base 哈希表中查到 ip_packet_type，并将 pt_prev 指向它，然后通过pt_prev-\u0026gt;func 调用 ip_rcv\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // net/ipv4/ip_input.c /* * IP receive entry point */ int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { struct net *net = dev_net(dev); skb = ip_rcv_core(skb, net); if (skb == NULL) return NET_RX_DROP; return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish); } INPUT Hook File Function NF_INET_LOCAL_IN net/ipv4/ip_input.c ip_local_deliver() Works only for incoming packets. Triggered after the routing decision (i.e., once the kernel determines the packet is destined for the local host). Occurs before local processing (e.g., before 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // net/ipv4/ip_input.c /* * Deliver IP Packets to the higher protocol layers. */ int ip_local_deliver(struct sk_buff *skb) { /* *\tReassemble IP fragments. */ struct net *net = dev_net(skb-\u0026gt;dev); if (ip_is_fragment(ip_hdr(skb))) { if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; } return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, net, NULL, skb, skb-\u0026gt;dev, NULL, ip_local_deliver_finish); } FORWARD Hook File Function NF_INET_FORWARD net/ipv4/ip_forward.c ip_forward() Works only for incoming packet. According to routing decision, if the packet is sent to another interface then this hook is called. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // net/ipv4/ip_forward.c int ip_forward(struct sk_buff *skb) { u32 mtu; struct iphdr *iph;\t/* Our header */ struct rtable *rt;\t/* Route we use */ struct ip_options *opt\t= \u0026amp;(IPCB(skb)-\u0026gt;opt); struct net *net; ...... ...... return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, net, NULL, skb, skb-\u0026gt;dev, rt-\u0026gt;dst.dev, ip_forward_finish); sr_failed: /* *\tStrict routing permits no gatewaying */ icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0); goto drop; too_many_hops: /* Tell the sender its packet died... */ __IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS); icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0); drop: kfree_skb(skb); return NET_RX_DROP; } OUTPUT Hook File Function NF_INET_LOCAL_OUT net/ipv4/ip_output.c __ip_local_out Works only for outgoing packets. The packet is created locally (originating from the host itself). Routing decision is made after this hook is called. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // net/ipv4/ip_output.c int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { struct iphdr *iph = ip_hdr(skb); iph-\u0026gt;tot_len = htons(skb-\u0026gt;len); ip_send_check(iph); /* if egress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_out(sk, skb); if (unlikely(!skb)) return 0; skb-\u0026gt;protocol = htons(ETH_P_IP); return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)-\u0026gt;dev, dst_output); //需要精细控制，不能立即调用 okfn } // include/net/dst.h /* Output packet to network from transport. */ static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb) { return INDIRECT_CALL_INET(skb_dst(skb)-\u0026gt;output, ip6_output, ip_output, net, sk, skb); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // netfilter.h /** *\tnf_hook - call a netfilter hook * *\tReturns 1 if the hook has allowed the packet to pass. The function *\tokfn must be invoked by the caller in this case. Any other return *\tvalue indicates the packet has been consumed by the hook. */ static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct net *, struct sock *, struct sk_buff *)) { ...... if (hook_head) { struct nf_hook_state state; nf_hook_state_init(\u0026amp;state, hook, pf, indev, outdev, sk, net, okfn); ret = nf_hook_slow(skb, \u0026amp;state, hook_head, 0); } rcu_read_unlock(); return ret; } /* nf_hook走 netfilter 钩子链，但返回一个值给调用者自己判断下一步 1\tACCEPT — 包通过了所有钩子\t调用者自己调用 okfn 0\tSTOLEN — 包已被消费\t调用者什么都不做 负数\tDROP — 包被丢弃\t调用者返回错误 */ static inline int NF_HOOK_COND(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb, struct net_device *in, struct net_device *out, int (*okfn)(struct net *, struct sock *, struct sk_buff *), bool cond) { int ret; if (!cond || ((ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn)) == 1)) ret = okfn(net, sk, skb); return ret; } static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb, struct net_device *in, struct net_device *out, int (*okfn)(struct net *, struct sock *, struct sk_buff *)) { int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn); if (ret == 1) ret = okfn(net, sk, skb); return ret; } POSTROUTING Hook File Function NF_INET_POST_ROUTING net/ipv4/ip_output.c ip_output() Works only for outgoing packets. This is the last hook. After that, the packet is sent to the lower layers. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // net/ipv4/ip_output.c int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb) { struct net_device *dev = skb_dst(skb)-\u0026gt;dev, *indev = skb-\u0026gt;dev; IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb-\u0026gt;len); skb-\u0026gt;dev = dev; skb-\u0026gt;protocol = htons(ETH_P_IP); return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, net, sk, skb, indev, dev, ip_finish_output, !(IPCB(skb)-\u0026gt;flags \u0026amp; IPSKB_REROUTED)); } Register and Unregister Netfilter Register Routine 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // net\\netfilter\\core.c static struct list_head *nf_find_hook_list(struct net *net, const struct nf_hook_ops *reg) { struct list_head *hook_list = NULL; if (reg-\u0026gt;pf != NFPROTO_NETDEV) hook_list = \u0026amp;net-\u0026gt;nf.hooks[reg-\u0026gt;pf][reg-\u0026gt;hooknum]; else if (reg-\u0026gt;hooknum == NF_NETDEV_INGRESS) { #ifdef CONFIG_NETFILTER_INGRESS if (reg-\u0026gt;dev \u0026amp;\u0026amp; dev_net(reg-\u0026gt;dev) == net) hook_list = \u0026amp;reg-\u0026gt;dev-\u0026gt;nf_hooks_ingress; #endif } return hook_list; } int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg) { struct list_head *hook_list; struct nf_hook_entry *entry; struct nf_hook_ops *elem; entry = kmalloc(sizeof(*entry), GFP_KERNEL); if (!entry) return -ENOMEM; entry-\u0026gt;orig_ops\t= reg; entry-\u0026gt;ops\t= *reg; hook_list = nf_find_hook_list(net, reg); if (!hook_list) { kfree(entry); return -ENOENT; } mutex_lock(\u0026amp;nf_hook_mutex); list_for_each_entry(elem, hook_list, list) { if (reg-\u0026gt;priority \u0026lt; elem-\u0026gt;priority) break; } list_add_rcu(\u0026amp;entry-\u0026gt;ops.list, elem-\u0026gt;list.prev);//插入 mutex_unlock(\u0026amp;nf_hook_mutex); #ifdef CONFIG_NETFILTER_INGRESS if (reg-\u0026gt;pf == NFPROTO_NETDEV \u0026amp;\u0026amp; reg-\u0026gt;hooknum == NF_NETDEV_INGRESS) net_inc_ingress_queue(); #endif #ifdef HAVE_JUMP_LABEL static_key_slow_inc(\u0026amp;nf_hooks_needed[reg-\u0026gt;pf][reg-\u0026gt;hooknum]); #endif return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 // include/net/net_namespace.h struct net { // ... 其他字段 ... struct netns_nf nf; // Netfilter 相关 // ... }; // include/net/netns/netfilter.h struct netns_nf { // 核心钩子链表数组 struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; // ... 其他字段 ... }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // net\\ipv4\\netfilter\\iptable_nat.c static struct nf_hook_ops nf_nat_ipv4_ops[] __read_mostly = { /* Before packet filtering, change destination */ { .hook\t= iptable_nat_ipv4_in, .pf\t= NFPROTO_IPV4, .hooknum\t= NF_INET_PRE_ROUTING, .priority\t= NF_IP_PRI_NAT_DST, }, /* After packet filtering, change source */ { .hook\t= iptable_nat_ipv4_out, .pf\t= NFPROTO_IPV4, .hooknum\t= NF_INET_POST_ROUTING, .priority\t= NF_IP_PRI_NAT_SRC, }, /* Before packet filtering, change destination */ { .hook\t= iptable_nat_ipv4_local_fn, .pf\t= NFPROTO_IPV4, .hooknum\t= NF_INET_LOCAL_OUT, .priority\t= NF_IP_PRI_NAT_DST, }, /* After packet filtering, change source */ { .hook\t= iptable_nat_ipv4_fn, .pf\t= NFPROTO_IPV4, .hooknum\t= NF_INET_LOCAL_IN, .priority\t= NF_IP_PRI_NAT_SRC, }, }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static int __init iptable_nat_init(void) { int err; err = register_pernet_subsys(\u0026amp;iptable_nat_net_ops); if (err \u0026lt; 0) goto err1; err = nf_register_hooks(nf_nat_ipv4_ops, ARRAY_SIZE(nf_nat_ipv4_ops)); if (err \u0026lt; 0) goto err2; return 0; err2: unregister_pernet_subsys(\u0026amp;iptable_nat_net_ops); err1: return err; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 //net/netfilter/core.c int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n) { unsigned int i; int err = 0; for (i = 0; i \u0026lt; n; i++) { err = nf_register_hook(\u0026amp;reg[i]); if (err) goto err; } return err; err: if (i \u0026gt; 0) nf_unregister_hooks(reg, i); return err; } int nf_register_hook(struct nf_hook_ops *reg) { struct net *net, *last; int ret; rtnl_lock(); for_each_net(net) { ret = nf_register_net_hook(net, reg); if (ret \u0026amp;\u0026amp; ret != -ENOENT) goto rollback; } list_add_tail(\u0026amp;reg-\u0026gt;list, \u0026amp;nf_hook_list); rtnl_unlock(); return 0; rollback: last = net; for_each_net(net) { if (net == last) break; nf_unregister_net_hook(net, reg); } rtnl_unlock(); return ret; } Netfilter Unregister Routine 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 //net\\netfilter\\core.c void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *reg) { struct list_head *hook_list; struct nf_hook_entry *entry; struct nf_hook_ops *elem; hook_list = nf_find_hook_list(net, reg); if (!hook_list) return; mutex_lock(\u0026amp;nf_hook_mutex); list_for_each_entry(elem, hook_list, list) { entry = container_of(elem, struct nf_hook_entry, ops); if (entry-\u0026gt;orig_ops == reg) { list_del_rcu(\u0026amp;entry-\u0026gt;ops.list); break; } } mutex_unlock(\u0026amp;nf_hook_mutex); if (\u0026amp;elem-\u0026gt;list == hook_list) { WARN(1, \u0026#34;nf_unregister_net_hook: hook not found!\\n\u0026#34;); return; } #ifdef CONFIG_NETFILTER_INGRESS if (reg-\u0026gt;pf == NFPROTO_NETDEV \u0026amp;\u0026amp; reg-\u0026gt;hooknum == NF_NETDEV_INGRESS) net_dec_ingress_queue(); #endif #ifdef HAVE_JUMP_LABEL static_key_slow_dec(\u0026amp;nf_hooks_needed[reg-\u0026gt;pf][reg-\u0026gt;hooknum]); #endif synchronize_net(); nf_queue_nf_hook_drop(net, \u0026amp;entry-\u0026gt;ops); /* other cpu might still process nfqueue verdict that used reg */ synchronize_net(); kfree(entry); } Register and Unregister user defined callback linux-learn-demo-code/netfilter at main · charles-7777/linux-learn-demo-code\n","date":"2026-07-12T19:07:45+08:00","permalink":"https://charles-7777.github.io/p/netfilter-hooks-in-kernel/","title":"Netfilter Hooks in Kernel"},{"content":"Introduction Inter Process Communication (IPC) Development in kernel is complex. Only core functionalities of operating system and performance critical codes are in kernel. User space applications and kernel space code. Through IPC user space application can communicate with kernel space program.Two kernel module can communicate with each other. Widely used and famous IPCs : system call, IOCTL, proc filesystem, Netlink socket IOCTL vs Netlink User space application set/get information to/from kernel using IOTCL.\nifconfig、arp、 route 内部都是使用ioctl , ioctl对于每个操作都必须有一个唯一的ioctl编号。\nioctl cannot send asynchronous message from kernel to user space.\nNon-trivial task to add IOCTL for new feature.\n新添加ioctl 并非易事，因为需要同时修改 user space和kernel\nAdding a protocol type in netlink.h is sufficient（足够） to support new type of Netlink socket. Socket APIs are compatible with BSD socket API. Thus easy to implement in user space application.\nNetlink is asynchronous\nNetlink socket supports multicast.\nNetlink Basic Functionalities Special IPC for transferring information between kernel and user space processes. Two kernel processes can communication with each other through netlink. Full duplex communication Use standard socket APIs in user space process. Support multiple protocol types. User can define his own type in netlink.h NETLINK_ROUTE NETLINK_FIREWALL NETLINK_ARPD User space packages iproute2 uses Netlink ip: For management of network tables and network interfaces tc: For traffic control management bridge: For management of bridge addresses and devices net-tools still uses IOCTL ifconfig arp route netstat API socket int socket(int domain, int type, int protocol) socket domain (address family): AF_NETLINK type: SOCK_RAW or SOCK_DGRAM protocol: NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD bind the netlink bind() API associates a local (source) socket address with the opened socket 1 2 3 4 5 6 7 8 //netlink.h struct sockaddr_nl { sa_family_t nl_family; /* AF_NETLINK */ unsigned short nl_pad; /* zero */ __u32 nl_pid; /* process pid */ __u32 nl_groups; /* mcast groups mask */ } nladdr; nl _pid: filled with the calling process\u0026rsquo; own pid\nnl_groups: 0 for unicast\nbind(fd, (struct sockaddr*)\u0026amp;nladdr, sizeof(nladdr)):\nnetlink demo linux-learn-demo-code/netlink at main · charles-7777/linux-learn-demo-code\nroute show demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /* Create netlink socket */ fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd \u0026lt; 0) { printf(\u0026#34;Error creating socket: %s\\n\u0026#34;, strerror(errno)); return -1; } /* Set up local address and bind */ memset(\u0026amp;local, 0, sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = pid; local.nl_groups = RTMGRP_LINK; /* Subscribe to link notifications */ if (bind(fd, (struct sockaddr *)\u0026amp;local, sizeof(local)) \u0026lt; 0) { perror(\u0026#34;Cannot bind - are you root? Check netlink/rtnetlink support\u0026#34;); close(fd); return -1; } printf(\u0026#34;=== Network Information ===\\n\\n\u0026#34;); /* Request and display network interfaces if requested */ if (show_all || show_interfaces) { printf(\u0026#34;--- Network Interfaces ---\\n\u0026#34;); if (send_netlink_request(fd, RTM_GETLINK, AF_UNSPEC) == 0) { process_netlink_responses(fd, 0, 1); } printf(\u0026#34;\\n\u0026#34;); } /* Request and display routing table if requested */ if (show_all || show_routes) { printf(\u0026#34;--- Routing Table ---\\n\u0026#34;); if (send_netlink_request(fd, RTM_GETROUTE, AF_INET) == 0) { process_netlink_responses(fd, 1, 0); } printf(\u0026#34;\\n\u0026#34;); } route monitor demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 charles@ubuntu24:~/netlink$ ./route_montor Route Monitor Tool ================== Monitoring IPv4 route changes... Press Ctrl+C to exit [2026-07-11 03:44:10] DELETED route: 10.0.2.15/32 proto 2 src 10.0.2.15 table 255 [2026-07-11 03:44:37] ADDED route: 10.0.2.15/32 proto 2 src 10.0.2.15 table 255 [2026-07-11 03:44:37] ADDED route: 10.0.2.255/32 proto 2 src 10.0.2.15 table 255 [2026-07-11 03:44:37] ADDED route: 10.0.2.0/24 proto 2 src 10.0.2.15 [2026-07-11 03:44:37] ADDED route: 10.0.2.2/32 proto 16 src 10.0.2.15 [2026-07-11 03:44:37] ADDED route: N/A/0 proto 16 via 10.0.2.2 src 10.0.2.15 [2026-07-11 03:44:37] ADDED route: 192.168.1.1/32 proto 16 via 10.0.2.2 src 10.0.2.15 ^C Received signal 2, shutting down... Route monitor stopped. charles@ubuntu24:~$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 192.168.56.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s8 charles@ubuntu24:~$ sudo ifconfig enp0s3 up charles@ubuntu24:~$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.0.2.2 0.0.0.0 UG 100 0 0 enp0s3 10.0.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s3 10.0.2.2 0.0.0.0 255.255.255.255 UH 100 0 0 enp0s3 192.168.1.1 10.0.2.2 255.255.255.255 UGH 100 0 0 enp0s3 192.168.56.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s8 charles@ubuntu24:~$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /* Create netlink socket */ if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) \u0026lt; 0) { ERR_RET(\u0026#34;socket\u0026#34;); } /* Set socket options to receive notifications */ addr.nl_family = AF_NETLINK; addr.nl_groups = RTMGRP_IPV4_ROUTE; /* Subscribe to IPv4 route changes */ /* Bind socket */ if (bind(sock, (struct sockaddr *)\u0026amp;addr, sizeof(addr)) \u0026lt; 0) { close(sock); ERR_RET(\u0026#34;bind\u0026#34;); } /* Main monitoring loop */ while (running) { fd_set fds; struct timeval tv; int ret; FD_ZERO(\u0026amp;fds); FD_SET(sock, \u0026amp;fds); tv.tv_sec = 1; tv.tv_usec = 0; /* Wait for data with timeout */ ret = select(sock + 1, \u0026amp;fds, NULL, NULL, \u0026amp;tv); if (ret \u0026lt; 0) { if (errno == EINTR) { continue; /* Interrupted by signal */ } perror(\u0026#34;select\u0026#34;); break; } if (ret \u0026gt; 0 \u0026amp;\u0026amp; FD_ISSET(sock, \u0026amp;fds)) { if (loop(sock, \u0026amp;addr) \u0026lt; 0) { break; } } } NETLINK_USER demo 1 2 3 4 5 6 7 8 9 10 11 12 [312348.806576] Entering: hello_init [312348.808862] Netlink module initialized successfully! [312453.904532] Entering: hello_nl_recv_msg [312453.904781] Netlink received msg payload: Hello from user space! [312453.905008] Reply sent successfully to PID: 128016 charles@ubuntu24:~/netlink$ sudo insmod netlinkKernel.ko charles@ubuntu24:~/netlink$ ./netlinkUser Sending message to kernel: Hello from user space! Waiting for message from kernel... Received message from kernel: Hello from kernel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 //netlinkUser.c // 创建 Netlink socket sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER); // 绑定源地址（用户进程自身） memset(\u0026amp;src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); // 使用进程 PID 作为端口号 src_addr.nl_groups = 0; // 不加入多播组 if (bind(sock_fd, (struct sockaddr *)\u0026amp;src_addr, sizeof(src_addr)) \u0026lt; 0) { } // 设置目标地址（内核） memset(\u0026amp;dest_addr, 0, sizeof(dest_addr)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; // 内核的 PID 为 0 dest_addr.nl_groups = 0; // 单播 // 分配 Netlink 消息缓冲区 nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); if (!nlh) { } memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); // 填充 Netlink 消息头 nlh-\u0026gt;nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh-\u0026gt;nlmsg_pid = getpid(); // 发送方 PID nlh-\u0026gt;nlmsg_flags = 0; // 无特殊标志 // 拷贝用户数据到消息负载 char *data = NLMSG_DATA(nlh); strcpy(data, \u0026#34;Hello from user space!\u0026#34;); // 设置 I/O 向量 iov.iov_base = (void *)nlh; iov.iov_len = nlh-\u0026gt;nlmsg_len; // 设置消息头 memset(\u0026amp;msg, 0, sizeof(msg)); msg.msg_name = (void *)\u0026amp;dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = \u0026amp;iov; msg.msg_iovlen = 1; // 发送消息到内核 printf(\u0026#34;Sending message to kernel: %s\\n\u0026#34;, (char *)NLMSG_DATA(nlh)); ret = sendmsg(sock_fd, \u0026amp;msg, 0); if (ret \u0026lt; 0) { } // 等待并接收内核回复 printf(\u0026#34;Waiting for message from kernel...\\n\u0026#34;); memset(\u0026amp;msg, 0, sizeof(msg)); msg.msg_name = (void *)\u0026amp;dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = \u0026amp;iov; msg.msg_iovlen = 1; ret = recvmsg(sock_fd, \u0026amp;msg, 0); if (ret \u0026lt; 0) { } // 打印内核回复的内容 printf(\u0026#34;Received message from kernel: %s\\n\u0026#34;, (char *)NLMSG_DATA(nlh)) //NetlinkKernel.c #define NETLINK_USER 31 struct sock *nl_sk = NULL; // 接收来自用户空间消息的回调函数 static void hello_nl_recv_msg(struct sk_buff *skb) { struct nlmsghdr *nlh; int pid; struct sk_buff *skb_out; int msg_size; char *msg = \u0026#34;Hello from kernel\u0026#34;; int res; printk(KERN_INFO \u0026#34;Entering: %s\\n\u0026#34;, __func__); // 获取 Netlink 消息头 nlh = (struct nlmsghdr *)skb-\u0026gt;data; printk(KERN_INFO \u0026#34;Netlink received msg payload: %s\\n\u0026#34;, (char *)nlmsg_data(nlh)); // 获取发送进程的 PID pid = nlh-\u0026gt;nlmsg_pid; // 准备回复消息 msg_size = strlen(msg) + 1; // +1 for null terminator // 分配新的 skb 用于回复 skb_out = nlmsg_new(msg_size, GFP_KERNEL); if (!skb_out) { printk(KERN_ERR \u0026#34;Failed to allocate new skb\\n\u0026#34;); return; } // 构造 Netlink 消息 nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0); if (!nlh) { printk(KERN_ERR \u0026#34;nlmsg_put failed\\n\u0026#34;); nlmsg_free(skb_out); return; } // 设置目标组（不加入多播组） NETLINK_CB(skb_out).dst_group = 0; // 拷贝消息数据 strncpy(nlmsg_data(nlh), msg, msg_size); // 单播发送回复给用户进程 res = nlmsg_unicast(nl_sk, skb_out, pid); if (res \u0026lt; 0) { printk(KERN_INFO \u0026#34;Error while sending back to user: %d\\n\u0026#34;, res); } else { printk(KERN_INFO \u0026#34;Reply sent successfully to PID: %d\\n\u0026#34;, pid); } } // 模块初始化函数 static int __init hello_init(void) { struct netlink_kernel_cfg cfg = { .input = hello_nl_recv_msg, }; printk(KERN_INFO \u0026#34;Entering: %s\\n\u0026#34;, __func__); // 创建 Netlink socket nl_sk = netlink_kernel_create(\u0026amp;init_net, NETLINK_USER, \u0026amp;cfg); if (!nl_sk) { printk(KERN_ALERT \u0026#34;Error creating Netlink socket.\\n\u0026#34;); return -ENOMEM; } printk(KERN_INFO \u0026#34;Netlink module initialized successfully!\\n\u0026#34;); return 0; } // 模块退出函数 static void __exit hello_exit(void) { printk(KERN_INFO \u0026#34;Exiting hello module\\n\u0026#34;); // 释放 Netlink socket if (nl_sk) { netlink_kernel_release(nl_sk); nl_sk = NULL; } } ","date":"2026-07-11T13:19:50+08:00","permalink":"https://charles-7777.github.io/p/netlink-%E5%88%9D%E8%AF%86/","title":"Netlink 初识"},{"content":" 基于 Linux 2.6.10 内核源码 (drivers/net/e1000/ 与 net/core/)\n【 kernel.org 源码下载 】\n【 elixir.bootlin.com 线上Linux源码阅读 】\n驱动概述与核心数据结构 E1000 是 Intel PRO/1000 千兆网卡的 Linux 驱动。收发包机制的本质是 描述符环 (descriptor ring) + DMA + 中断/NAPI: 网卡与驱动通过一段共享的 DMA 内存(描述符环)交换\u0026quot;数据缓冲区物理地址与状态\u0026quot;,再用 head/tail 寄存器互相通知进度。\n三个基础结构 (e1000.h) 软件缓冲信息 e1000_buffer —— 每个描述符配一个,记录关联的 SKB 与 DMA 地址:\n1 2 3 4 5 6 7 struct e1000_buffer { struct sk_buff *skb; // 关联的 socket buffer uint64_t dma; // DMA 物理地址 unsigned long time_stamp; // 时间戳, 用于 TX 超时检测 uint16_t length; // 缓冲区长度 uint16_t next_to_watch; // TX: 本包最后一个描述符(EOP)的索引 }; 描述符环 e1000_desc_ring —— TX/RX 共用,靠两个游标形成环形队列:\n1 2 3 4 5 6 7 8 struct e1000_desc_ring { void *desc; // 描述符环虚拟地址(DMA一致性内存) dma_addr_t dma; // 描述符环物理地址(交给硬件) unsigned int count; // 描述符数量(默认256) unsigned int next_to_use; // 生产者游标: 软件下一个要用的位置 unsigned int next_to_clean; // 消费者游标: 软件下一个要清理的位置 struct e1000_buffer *buffer_info; // 每描述符的软件信息数组 }; 解析: next_to_use / next_to_clean 是软件侧两个游标,对应硬件的 tail / head:\nTX: 软件在 next_to_use 填描述符并写 TDT(tail); 硬件从 TDH(head) 读取发送; 软件在 next_to_clean 回收。 RX: 软件在 next_to_use 挂空缓冲区并写 RDT(tail); 硬件从 RDH(head) 取缓冲区写入收到的数据; 软件从 next_to_clean 取出上送。 宏 E1000_DESC_UNUSED(R) 用 next_to_clean 与 next_to_use 之差计算环内空闲描述符数,发送前用它判断\u0026quot;环里还放不放得下\u0026quot;。\n硬件描述符与关键状态位 RX 描述符: buffer_addr / length / status(含 DD、EOP、VP) / errors / special(VLAN)。 TX 描述符: buffer_addr / lower.data(长度+命令) / upper.data(状态)。 上下文描述符 (e1000_context_desc): 不承载数据,只向硬件传递 TSO / 校验和卸载参数。 位 含义 E1000_RXD_STAT_DD RX: 硬件已完成 DMA 写入(软件据此判断有无新包) E1000_RXD_STAT_EOP RX: 包尾描述符 E1000_TXD_STAT_DD TX: 硬件已完成发送(软件据此回收) E1000_TXD_CMD_EOP TX: 包尾 E1000_TXD_CMD_RS TX: 要求硬件发完后回写 DD 位 收发包总体架构 graph TD subgraph 用户态 APP[\u0026#34;应用进程 send()/recv()\u0026#34;] end subgraph 协议栈 SOCK[\u0026#34;socket 层\u0026#34;] TCP[\u0026#34;TCP/UDP\u0026#34;] IP[\u0026#34;IP 层 ip_rcv/ip_output\u0026#34;] DEV[\u0026#34;设备无关层 dev.c\u0026lt;br/\u0026gt;dev_queue_xmit / netif_rx / netif_receive_skb\u0026#34;] end subgraph 驱动 e1000_main.c XMIT[\u0026#34;e1000_xmit_frame (发)\u0026#34;] CLEAN[\u0026#34;e1000_clean_rx_irq (收)\u0026#34;] INTR[\u0026#34;e1000_intr / e1000_clean\u0026#34;] end subgraph 硬件 RING[\u0026#34;TX/RX 描述符环 + DMA\u0026#34;] NIC[\u0026#34;e1000 网卡\u0026#34;] end APP\u0026lt;--\u0026gt;SOCK\u0026lt;--\u0026gt;TCP\u0026lt;--\u0026gt;IP\u0026lt;--\u0026gt;DEV DEV--\u0026gt;|hard_start_xmit|XMIT--\u0026gt;RING NIC--\u0026gt;|IRQ|INTR--\u0026gt;CLEAN--\u0026gt;|netif_rx/netif_receive_skb|DEV RING\u0026lt;--\u0026gt;NIC 两条主线:\n发送 dev_queue_xmit → e1000_xmit_frame → e1000_tso/csum → e1000_tx_map → e1000_tx_queue → 硬件 接收 硬件IRQ → e1000_intr → e1000_clean → e1000_clean_rx_irq → netif_receive_skb → ip_rcv 发送路径 TX 调用链逐函数解析 TX 调用链 sequenceDiagram participant Stack as dev_queue_xmit(协议栈) participant XMIT as e1000_xmit_frame participant TSO as e1000_tso participant CSUM as e1000_tx_csum participant MAP as e1000_tx_map participant QUEUE as e1000_tx_queue participant HW as 硬件 Stack-\u0026gt;\u0026gt;XMIT: hard_start_xmit(skb, dev) XMIT-\u0026gt;\u0026gt;XMIT: 计算描述符数 count / 抢 tx_lock / 查空闲 alt TSO XMIT-\u0026gt;\u0026gt;TSO: e1000_tso() else 硬件校验和 XMIT-\u0026gt;\u0026gt;CSUM: e1000_tx_csum() end XMIT-\u0026gt;\u0026gt;MAP: e1000_tx_map() DMA映射 XMIT-\u0026gt;\u0026gt;QUEUE: e1000_tx_queue(count, tx_flags) QUEUE-\u0026gt;\u0026gt;HW: 填数据描述符 + wmb() + 写TDT XMIT--\u0026gt;\u0026gt;Stack: NETDEV_TX_OK 协议栈交接: dev_queue_xmit net/core/dev.c —— 协议栈交给驱动前的最后一站。IP 层 ip_finish_output 之后进入。核心逻辑摘录:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // dev_queue_xmit() // 1) 驱动不支持分散聚集(SG)则先把分片线性化 if (skb_shinfo(skb)-\u0026gt;nr_frags \u0026amp;\u0026amp; !(dev-\u0026gt;features \u0026amp; NETIF_F_SG)) __skb_linearize(skb, GFP_ATOMIC); // 2) 需校验和但驱动无硬件卸载, 这里软件补算 if (skb-\u0026gt;ip_summed == CHECKSUM_HW \u0026amp;\u0026amp; !(dev-\u0026gt;features \u0026amp; NETIF_F_HW_CSUM)) skb_checksum_help(skb, 0); // 3) 取排队规则(流量整形), 入队后驱动发送 q = rcu_dereference(dev-\u0026gt;qdisc); if (q-\u0026gt;enqueue) { q-\u0026gt;enqueue(skb, q); qdisc_run(dev); // 出队 → 最终调 dev-\u0026gt;hard_start_xmit } 解析: 三项预处理都由驱动 dev-\u0026gt;features 决定\n声明 NETIF_F_SG 可免拷贝 声明 NETIF_F_HW_CSUM/IP_CSUM 可把校验和留给硬件。 qdisc_run → qdisc_restart 最终调用 dev-\u0026gt;hard_start_xmit,即 e1000_xmit_frame。 发包入口: e1000_xmit_frame e1000_main.c:1752 —— probe 阶段注册为 netdev-\u0026gt;hard_start_xmit。主流程摘录:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // (1) 估算本包需要多少个描述符: 上下文描述符 + 线性区 + 各分片 if ((mss) || (skb-\u0026gt;ip_summed == CHECKSUM_HW)) count++; count += TXD_USE_COUNT(len, max_txd_pwr); for (f = 0; f \u0026lt; nr_frags; f++) count += TXD_USE_COUNT(frags[f].size, max_txd_pwr); // (2) 非阻塞抢锁, 抢不到让上层重排(不阻塞) if (!spin_trylock(\u0026amp;adapter-\u0026gt;tx_lock)) return NETDEV_TX_LOCKED; // (3) 空闲描述符不足: 停队列并让上层稍后重试(背压) if (E1000_DESC_UNUSED(\u0026amp;adapter-\u0026gt;tx_ring) \u0026lt; count + 2) { netif_stop_queue(netdev); return NETDEV_TX_BUSY; } // (4) 卸载: TSO 优先, 否则尝试硬件校验和 if (e1000_tso(adapter, skb)) tx_flags |= E1000_TX_FLAGS_TSO; else if (e1000_tx_csum(adapter, skb)) tx_flags |= E1000_TX_FLAGS_CSUM; // (5) DMA映射 + 填描述符 + 写TDT (一行串起两个函数) e1000_tx_queue(adapter, e1000_tx_map(adapter, skb, first, max_per_txd, nr_frags, mss), tx_flags); return NETDEV_TX_OK; 解析:\n描述符预算: 一个包可能拆成多个描述符(线性区每 4KB 一个 + 每个分片 + 可能的上下文描述符),必须先算总数保证环放得下(count + 2 的 gap 防止 tail 追上 head)。 spin_trylock + NETDEV_TX_LOCKED: 发送可能并发,抢不到锁直接让协议栈重排,不阻塞。 netif_stop_queue: 环快满时主动停队列,向 Qdisc 施加背压,待回收后再 netif_wake_queue。 TSO 卸载: e1000_tso e1000_main.c:1488 —— TCP Segmentation Offload: 协议栈把一大段 TCP 数据整体下发,由网卡硬件切成多个 MSS 段。它只填一个上下文描述符,不搬数据:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (skb_shinfo(skb)-\u0026gt;tso_size) { // 清零 IP 总长/校验和, 硬件会为每个切出的段重填 skb-\u0026gt;nh.iph-\u0026gt;tot_len = 0; skb-\u0026gt;nh.iph-\u0026gt;check = 0; // 预置 TCP 伪头部校验和, 硬件只需累加数据部分 skb-\u0026gt;h.th-\u0026gt;check = ~csum_tcpudp_magic(saddr, daddr, 0, IPPROTO_TCP, 0); // 把 MSS、头长、各层校验和的起止偏移填进上下文描述符 context_desc-\u0026gt;tcp_seg_setup.fields.mss = cpu_to_le16(mss); context_desc-\u0026gt;tcp_seg_setup.fields.hdr_len = hdr_len; context_desc-\u0026gt;cmd_and_length = cpu_to_le32(cmd_length | E1000_TXD_CMD_TSE); return TRUE; // 消耗一个描述符位置 } return FALSE; 解析: 硬件按上下文描述符里的 MSS 和头部信息,自动复制头部、逐段切分并计算校验和。软件预置伪头部校验和是为让硬件只需对数据部分做累加。\n硬件校验和: e1000_tx_csum e1000_main.c:1542 —— 非 TSO 但请求了 CHECKSUM_HW 时,同样用一个上下文描述符只传校验和参数:\n1 2 3 4 5 6 7 8 if (skb-\u0026gt;ip_summed == CHECKSUM_HW) { css = skb-\u0026gt;h.raw - skb-\u0026gt;data; // 传输层头相对 data 的偏移 context_desc-\u0026gt;upper_setup.tcp_fields.tucss = css; // 校验起点 context_desc-\u0026gt;upper_setup.tcp_fields.tucso = css + skb-\u0026gt;csum; // 校验和写入位置 context_desc-\u0026gt;cmd_and_length = cpu_to_le32(E1000_TXD_CMD_DEXT); return TRUE; } return FALSE; 解析: skb-\u0026gt;csum 是协议栈算好的\u0026quot;校验和应写入偏移\u0026quot;,驱动把它转成硬件理解的 tucso。硬件据此边发送边计算并回填校验和。\nDMA 映射: e1000_tx_map e1000_main.c:1573 —— 把 SKB 线性区和每个分片映射成 DMA 地址,记入 buffer_info:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 线性数据区: 每个描述符最多映射 4096 字节 while (len) { size = min(len, max_per_txd); buffer_info-\u0026gt;length = size; buffer_info-\u0026gt;dma = pci_map_single(pdev, skb-\u0026gt;data + offset, size, PCI_DMA_TODEVICE); len -= size; offset += size; count++; } // 分片(通常在高端内存的 page 里)用 pci_map_page for (f = 0; f \u0026lt; nr_frags; f++) { ... pci_map_page(...); } // 只在最后一个描述符保存 skb; 在 first 处记录 EOP 索引 tx_ring-\u0026gt;buffer_info[last].skb = skb; tx_ring-\u0026gt;buffer_info[first].next_to_watch = last; return count; 解析:\n线性区用 pci_map_single,分片用 pci_map_page。 只有最后一个描述符保存 skb,因为整包只需释放一次。 next_to_watch: 在 first 记下 EOP 描述符索引,回收时只查这个 EOP 的 DD 位即可判断整包是否发完。 填环并通知硬件: e1000_tx_queue e1000_main.c:1664 —— 把 DMA 地址写进硬件描述符,最后写 TDT 发车:\n1 2 3 4 5 6 7 8 9 10 11 12 i = tx_ring-\u0026gt;next_to_use; while (count--) { // 逐个填数据描述符 tx_desc = E1000_TX_DESC(*tx_ring, i); tx_desc-\u0026gt;buffer_addr = cpu_to_le64(buffer_info-\u0026gt;dma); tx_desc-\u0026gt;lower.data = cpu_to_le32(txd_lower | buffer_info-\u0026gt;length); if (++i == tx_ring-\u0026gt;count) i = 0; } tx_desc-\u0026gt;lower.data |= cpu_to_le32(adapter-\u0026gt;txd_cmd);// 最后描述符加 EOP|RS wmb(); // 内存屏障: 描述符先于TDT可见 tx_ring-\u0026gt;next_to_use = i; E1000_WRITE_REG(\u0026amp;adapter-\u0026gt;hw, TDT, i); // ★写Tail, 硬件开始取描述符发送★ 解析:\nEOP|RS 只加在最后一个描述符: EOP 标记包尾,RS 要求硬件发完回写 DD 供软件回收。 wmb(): 弱序内存架构(如 IA-64)上保证描述符内容先于 TDT 对硬件可见,防止硬件读到半成品。 写 TDT 是唯一的\u0026quot;发车\u0026quot;动作: 硬件发现 TDH != TDT 后自动 DMA 读取数据发送。 发送完成回收 TX-IRQ 解析 e1000_clean_tx_irq e1000_main.c:2188 —— 发送完成后(中断或 NAPI 轮询里)回收描述符、解除映射、释放 SKB:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 i = tx_ring-\u0026gt;next_to_clean; eop = tx_ring-\u0026gt;buffer_info[i].next_to_watch; // 本包 EOP 描述符 eop_desc = E1000_TX_DESC(*tx_ring, eop); while (eop_desc-\u0026gt;upper.data \u0026amp; cpu_to_le32(E1000_TXD_STAT_DD)) { // ★EOP的DD=整包发完★ for (cleaned = FALSE; !cleaned; ) { pci_unmap_page(pdev, buffer_info-\u0026gt;dma, ...); // 解除DMA映射 if (buffer_info-\u0026gt;skb) dev_kfree_skb_any(buffer_info-\u0026gt;skb); // 释放SKB(仅最后描述符有) cleaned = (i == eop); if (++i == tx_ring-\u0026gt;count) i = 0; } eop = tx_ring-\u0026gt;buffer_info[i].next_to_watch; eop_desc = E1000_TX_DESC(*tx_ring, eop); } tx_ring-\u0026gt;next_to_clean = i; // 之前因环满停过队列, 现在有空位了 → 唤醒发送 if (cleaned \u0026amp;\u0026amp; netif_queue_stopped(netdev) \u0026amp;\u0026amp; netif_carrier_ok(netdev)) netif_wake_queue(netdev); 解析: 通过 next_to_watch 找到 EOP 描述符,只检查它的 DD 位就知整包是否发完(不必逐个查)。回收后若之前 netif_stop_queue 过,现在 netif_wake_queue 通知 Qdisc 恢复发送 —— 与 3.3 的背压构成闭环。\n接收路径 RX 调用链逐函数解析 RX 调用链 sequenceDiagram participant HW as 硬件 participant IRQ as e1000_intr participant POLL as e1000_clean(NAPI) participant CRX as e1000_clean_rx_irq participant CSUM as e1000_rx_checksum participant ALLOC as e1000_alloc_rx_buffers participant Stack as netif_receive_skb HW-\u0026gt;\u0026gt;HW: DMA写缓冲区, 置DD, 更新RDH HW-\u0026gt;\u0026gt;IRQ: 硬件中断 IRQ-\u0026gt;\u0026gt;IRQ: 读ICR / 屏蔽中断IMC IRQ-\u0026gt;\u0026gt;POLL: __netif_rx_schedule(触发软中断) POLL-\u0026gt;\u0026gt;CRX: e1000_clean_rx_irq(work_done, budget) loop 遍历RX环 CRX-\u0026gt;\u0026gt;CRX: 查DD/EOP, unmap, skb_put CRX-\u0026gt;\u0026gt;CSUM: e1000_rx_checksum() CRX-\u0026gt;\u0026gt;Stack: netif_receive_skb(skb) end CRX-\u0026gt;\u0026gt;ALLOC: e1000_alloc_rx_buffers(补缓冲, 写RDT) POLL-\u0026gt;\u0026gt;IRQ: netif_rx_complete + e1000_irq_enable 中断入口: e1000_intr e1000_main.c:2111 —— 硬件中断处理函数,在 e1000_open 中经 request_irq 注册:\n模块加载 -\u0026gt; e1000_init_module → pci_module_init -\u0026gt; driver_register -\u0026gt; drv-\u0026gt;driver.probe() -\u0026gt;register_netdev() ;ifconfig e1000 up -\u0026gt; netdev-\u0026gt;open -\u0026gt; e1000_open -\u0026gt; e1000_up → request_irq\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // e1000_intr() uint32_t icr = E1000_READ_REG(hw, ICR); // 读ICR同时清中断标志 if (!icr) return IRQ_NONE; // 不是本设备的中断 if (icr \u0026amp; (E1000_ICR_RXSEQ | E1000_ICR_LSC)) mod_timer(\u0026amp;adapter-\u0026gt;watchdog_timer, jiffies); // 链路变化 → 唤醒看门狗 #ifdef CONFIG_E1000_NAPI if (netif_rx_schedule_prep(netdev)) { E1000_WRITE_REG(hw, IMC, ~0); // ★屏蔽所有中断★ __netif_rx_schedule(netdev); // ★挂入NAPI轮询, 触发软中断★ } #else // 非NAPI: 直接在中断里循环清理收发 e1000_clean_rx_irq(adapter); e1000_clean_tx_irq(adapter); #endif 解析: NAPI 模式下中断处理极短 —— 屏蔽中断 + 挂入轮询链表,把耗时的收包搬到软中断批量做。这是高流量下\u0026quot;中断风暴\u0026quot;的解药: 关中断后用轮询批量收包,收完再开中断。\nNAPI 轮询: e1000_clean e1000_main.c:2157 —— e1000_clean是在e1000_probe()时注册为 netdev-\u0026gt;poll,在软中断 net_rx_action 里被调用:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //e1000_clean int work_to_do = min(*budget, netdev-\u0026gt;quota); // 本轮预算(quota默认64) tx_cleaned = e1000_clean_tx_irq(adapter); // 先回收TX e1000_clean_rx_irq(adapter, \u0026amp;work_done, work_to_do); // 再收RX(受预算限) *budget -= work_done; netdev-\u0026gt;quota -= work_done; if (!tx_cleaned || (work_done \u0026lt; work_to_do)) { // 活干完了 netif_rx_complete(netdev); e1000_irq_enable(adapter); // ★退出轮询, 重新开中断★ return 0; } return (work_done \u0026gt;= work_to_do); // 返回1: 还有活, 下轮继续 解析: work_to_do 是预算,防止一个网卡在软中断里无限收包饿死其他设备。收够预算返回非零,net_rx_action 会把它排到队尾下次再来; 收完则 netif_rx_complete 退出并开中断。\n收包核心: e1000_clean_rx_irq e1000_main.c:2251 —— 遍历 RX 环,把硬件收到的数据包装成 SKB 上送协议栈:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 while (rx_desc-\u0026gt;status \u0026amp; E1000_RXD_STAT_DD) { // (1) DD位: 硬件已DMA完成 if (*work_done \u0026gt;= work_to_do) break; // (2) NAPI预算耗尽 (*work_done)++; pci_unmap_single(pdev, buffer_info-\u0026gt;dma, ...); // (3) 解除映射, CPU可读 skb = buffer_info-\u0026gt;skb; // (4) 取预分配的SKB if (!(rx_desc-\u0026gt;status \u0026amp; E1000_RXD_STAT_EOP)) { // (5) 跨描述符包: 本驱动丢弃 dev_kfree_skb_irq(skb); goto next_desc; } skb_put(skb, length - ETHERNET_FCS_SIZE); // (6) 设长度, 去掉4字节FCS e1000_rx_checksum(adapter, rx_desc, skb); // (7) 处理硬件校验和结果 skb-\u0026gt;protocol = eth_type_trans(skb, netdev); // (8) 剥以太网头, 定协议 #ifdef CONFIG_E1000_NAPI netif_receive_skb(skb); // (9a) NAPI: 直接进协议栈 #else netif_rx(skb); // (9b) 非NAPI: 入backlog队列 #endif next_desc: rx_desc-\u0026gt;status = 0; // (10) 清DD, 描述符可复用 if (++i == rx_ring-\u0026gt;count) i = 0; } e1000_alloc_rx_buffers(adapter); // (11) 补充新缓冲区, 写RDT 解析:\n第(1)步 DD 位是软硬件同步的关键: 硬件写完数据后置 DD,软件靠它判断\u0026quot;这个描述符有没有新包\u0026quot;。 第(8)步 eth_type_trans 是驱动的收尾: 剥以太网头、定 skb-\u0026gt;protocol,此后 SKB 就是与网卡无关的\u0026quot;网络层包\u0026quot;。 第(9)步 netif_receive_skb/netif_rx 是驱动与协议栈的正式交接(见第 6 章)。 第(11)步收完立即补缓冲区,保证 RX 环始终有空缓冲区给硬件写,否则丢包。 收端校验和: e1000_rx_checksum e1000_main.c:2597:\n1 2 3 4 5 6 7 8 9 10 // 硬件未算或标记忽略 → 交给协议栈软件校验 if (rx_desc-\u0026gt;status \u0026amp; E1000_RXD_STAT_IXSM || !(rx_desc-\u0026gt;status \u0026amp; E1000_RXD_STAT_TCPCS)) { skb-\u0026gt;ip_summed = CHECKSUM_NONE; return; } if (rx_desc-\u0026gt;errors \u0026amp; E1000_RXD_ERR_TCPE) skb-\u0026gt;ip_summed = CHECKSUM_NONE; // 校验错, 让栈复核 else skb-\u0026gt;ip_summed = CHECKSUM_UNNECESSARY; // ★硬件已验证, 栈可跳过★ 解析: 若硬件已正确校验,设 CHECKSUM_UNNECESSARY,TCP/UDP 层就跳过软件校验和 —— 这是收端校验和卸载省 CPU 的关键。\n补充缓冲区: e1000_alloc_rx_buffers e1000_main.c:2364 —— open 时首次填满,之后每次收包后补上被消费的缓冲区:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 while (!buffer_info-\u0026gt;skb) { skb = dev_alloc_skb(adapter-\u0026gt;rx_buffer_len + NET_IP_ALIGN); if (!skb) break; // 内存不足, 下轮再试 skb_reserve(skb, NET_IP_ALIGN); // 预留2字节 → IP头16字节对齐 buffer_info-\u0026gt;skb = skb; buffer_info-\u0026gt;dma = pci_map_single(pdev, skb-\u0026gt;data, ..., PCI_DMA_FROMDEVICE); rx_desc-\u0026gt;buffer_addr = cpu_to_le64(buffer_info-\u0026gt;dma); if ((i \u0026amp; (E1000_RX_BUFFER_WRITE - 1)) == 0) { wmb(); E1000_WRITE_REG(\u0026amp;adapter-\u0026gt;hw, RDT, i); // ★每16个批量写一次RDT★ } if (++i == rx_ring-\u0026gt;count) i = 0; } 解析:\nskb_reserve(NET_IP_ALIGN=2): 以太网头 14 字节,预留 2 字节后 IP 头正好 16 字节对齐,加速 IP 处理。 每 16 个才写一次 RDT: MMIO 写代价高,批量更新 tail 减少开销。写 RDT 等于告诉硬件\u0026quot;这些缓冲区可以用了\u0026quot;。 驱动到协议栈: 收包进栈逐函数解析 e1000_clean_rx_irq 调用 netif_receive_skb(NAPI) 或 netif_rx(非NAPI) 后,包正式进入协议栈。两条路径最终都汇聚到 netif_receive_skb。\nflowchart TD A[\u0026#34;e1000_clean_rx_irq\u0026#34;] --\u0026gt; A1[\u0026#34;eth_type_trans\u0026lt;br/\u0026gt;定 skb-\u0026gt;protocol\u0026#34;] A1 --\u0026gt; B{\u0026#34;CONFIG_E1000_NAPI?\u0026#34;} B --\u0026gt;|NAPI| C[\u0026#34;netif_receive_skb\u0026#34;] B --\u0026gt;|非NAPI| D[\u0026#34;netif_rx\u0026lt;br/\u0026gt;入per-CPU backlog队列\u0026#34;] D --\u0026gt; E[\u0026#34;__netif_rx_schedule\u0026lt;br/\u0026gt;触发NET_RX_SOFTIRQ\u0026#34;] E --\u0026gt; F[\u0026#34;net_rx_action(软中断)\u0026#34;] F --\u0026gt; G[\u0026#34;process_backlog\u0026#34;] G --\u0026gt; C F -.NAPI设备.-\u0026gt; H[\u0026#34;e1000_clean\u0026#34;] --\u0026gt; C C --\u0026gt; I[\u0026#34;按protocol查 ptype_base[]\u0026#34;] I --\u0026gt; J[\u0026#34;ip_rcv\u0026#34;] J --\u0026gt; K[\u0026#34;ip_local_deliver → tcp_v4_rcv/udp_rcv\u0026#34;] K --\u0026gt; L[\u0026#34;sock接收队列 → 唤醒recv()\u0026#34;] 链路层收尾: eth_type_trans net/ethernet/eth.c:159 —— 剥离以太网头并识别上层协议:\n1 2 3 4 5 6 7 8 9 10 11 skb-\u0026gt;mac.raw = skb-\u0026gt;data; // 记录MAC头位置 skb_pull(skb, ETH_HLEN); // ★data前移14字节, 跳过以太网头★ eth = eth_hdr(skb); if (*eth-\u0026gt;h_dest \u0026amp; 1) // 目的MAC最低位=1 → 组播/广播 skb-\u0026gt;pkt_type = PACKET_MULTICAST; // (广播另判) else if (memcmp(eth-\u0026gt;h_dest, dev-\u0026gt;dev_addr, ETH_ALEN)) skb-\u0026gt;pkt_type = PACKET_OTHERHOST; // 目的MAC非本机 if (ntohs(eth-\u0026gt;h_proto) \u0026gt;= 1536) return eth-\u0026gt;h_proto; // ★返回以太类型, 如 ETH_P_IP★ __解析: skb_pull(ETH_HLEN) 后 skb-\u0026gt;data 指向 IP 头; 返回值(如 ETH_P_IP)赋给 skb-\u0026gt;protocol 供分发。pkt_type 决定包是本机、广播还是\u0026quot;别人的\u0026quot;(混杂模式会收到)。\n非NAPI 入队: netif_rx net/core/dev.c:1423:\n1 2 3 4 5 6 7 8 9 queue = \u0026amp;__get_cpu_var(softnet_data); // per-CPU 收包队列 if (queue-\u0026gt;input_pkt_queue.qlen \u0026lt;= netdev_max_backlog) { // 未满(默认300) if (queue-\u0026gt;input_pkt_queue.qlen == 0) netif_rx_schedule(\u0026amp;queue-\u0026gt;backlog_dev); // 首包: 调度backlog伪设备 __skb_queue_tail(\u0026amp;queue-\u0026gt;input_pkt_queue, skb); // ★入队★ return queue-\u0026gt;cng_level; } kfree_skb(skb); // 队列满 → 丢包 return NET_RX_DROP; 解析: 老式驱动在硬中断里只把 SKB 塞进 per-CPU 的 input_pkt_queue,再调度虚拟的 backlog_dev 参与 NAPI 轮询,从而复用统一收包框架。队列满(300)即丢,是抗风暴第一道闸。\n挂入轮询: __netif_rx_schedule include/linux/netdevice.h:811 —— NAPI 真实设备(e1000)与 backlog 伪设备共用:\n1 2 3 list_add_tail(\u0026amp;dev-\u0026gt;poll_list, \u0026amp;__get_cpu_var(softnet_data).poll_list); // 挂轮询链表 dev-\u0026gt;quota = dev-\u0026gt;weight; // 重置配额(e1000 weight=64) __raise_softirq_irqoff(NET_RX_SOFTIRQ); // ★触发接收软中断★ 解析: e1000_intr 用它把 e1000 的 netdev 挂进 poll_list; netif_rx 挂的是 backlog_dev。二者机制统一。\n软中断总入口: net_rx_action net/core/dev.c:1764 —— NET_RX_SOFTIRQ 的处理函数:\n1 2 3 4 5 6 7 8 9 10 int budget = netdev_max_backlog; // 全局预算 while (!list_empty(\u0026amp;queue-\u0026gt;poll_list)) { if (budget \u0026lt;= 0 || jiffies - start_time \u0026gt; 1) // 预算耗尽或占用超1tick goto softnet_break; // 让出CPU, 下次软中断继续 dev = list_entry(queue-\u0026gt;poll_list.next, ...); if (dev-\u0026gt;quota \u0026lt;= 0 || dev-\u0026gt;poll(dev, \u0026amp;budget)) // ★调用设备poll★ list_move_tail(\u0026amp;dev-\u0026gt;poll_list, ...); // 没干完, 重排队尾 else dev_put(dev); // 干完, 移出链表 } 解析: dev-\u0026gt;poll 是多态调用 —— e1000 是 e1000_clean,backlog 是 process_backlog。全局 budget 加\u0026quot;占用不超过 1 tick\u0026quot;双重限制,保证软中断不霸占 CPU。\nbacklog 出队: process_backlog net/core/dev.c:1716:\n1 2 3 4 5 6 for (;;) { skb = __skb_dequeue(\u0026amp;queue-\u0026gt;input_pkt_queue); // 出队 if (!skb) break; netif_receive_skb(skb); // ★与NAPI路径汇聚于此★ if (++work \u0026gt;= quota) break; } 解析: 无论 NAPI(e1000_clean → netif_receive_skb) 还是非NAPI(process_backlog → netif_receive_skb),都在 netif_receive_skb 汇合。这是 Linux 收包模型的核心统一点。\n协议分发: netif_receive_skb net/core/dev.c:1625 —— 链路层与网络层的分水岭,按 skb-\u0026gt;protocol 投递给注册的协议处理器:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 skb-\u0026gt;nh.raw = skb-\u0026gt;data; // 网络层指针指向IP头 // 先给\u0026#34;所有协议\u0026#34;监听者(如 tcpdump/AF_PACKET) 一份 list_for_each_entry_rcu(ptype, \u0026amp;ptype_all, list) { ... } // 再按协议类型精确匹配, 哈希桶 ptype_base[type \u0026amp; 15] type = skb-\u0026gt;protocol; list_for_each_entry_rcu(ptype, \u0026amp;ptype_base[ntohs(type) \u0026amp; 15], list) { if (ptype-\u0026gt;type == type) pt_prev = ptype; } if (pt_prev) pt_prev-\u0026gt;func(skb, skb-\u0026gt;dev, pt_prev); // ★IP包 → ip_rcv★ else kfree_skb(skb); // 无人认领 IP 协议在初始化时注册处理器:\n1 2 3 4 5 static struct packet_type ip_packet_type = { .type = __constant_htons(ETH_P_IP), .func = ip_rcv, // IP包入口 }; // ip_init(): dev_add_pack(\u0026amp;ip_packet_type); 解析: ptype_base[16] 是协议哈希表。skb-\u0026gt;protocol == ETH_P_IP 时 pt_prev-\u0026gt;func 即 ip_rcv。ptype_all 让抓包工具拿到所有包副本。\n进入网络层: ip_rcv net/ipv4/ip_input.c:360 —— IP 层入口:\n1 2 3 4 5 if (skb-\u0026gt;pkt_type == PACKET_OTHERHOST) goto drop; // 混杂模式收到的别人包 if (iph-\u0026gt;ihl \u0026lt; 5 || iph-\u0026gt;version != 4) goto inhdr_error; if (ip_fast_csum((u8*)iph, iph-\u0026gt;ihl) != 0) goto inhdr_error; // ★校验IP头★ // 过 netfilter PRE_ROUTING 钩子 → ip_rcv_finish return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish); 之后:\nflowchart LR A[ip_rcv] --\u0026gt; B[NF_IP_PRE_ROUTING钩子] B --\u0026gt; C[ip_rcv_finish\u0026lt;br/\u0026gt;ip_route_input 路由查找] C --\u0026gt; D{本机?} D --\u0026gt;|是| E[ip_local_deliver\u0026lt;br/\u0026gt;→ tcp_v4_rcv / udp_rcv] D --\u0026gt;|转发| F[ip_forward] E --\u0026gt; G[sock接收队列\u0026lt;br/\u0026gt;唤醒 recv()] 解析: ip_rcv 校验头部后经 PRE_ROUTING 钩子进入 ip_rcv_finish,由 ip_route_input 判断本机接收还是转发。本机则 ip_local_deliver 按 iph-\u0026gt;protocol 交给 tcp_v4_rcv/udp_rcv,数据最终进 socket 队列,唤醒阻塞在 recv() 的进程。至此完成\u0026quot;从网线到应用\u0026quot;的全程。\n中断机制与 NAPI 软硬中断切换深度解析 前几章讲清了\u0026quot;包怎么走\u0026quot;,本章专门拆解中断层面的软硬切换——这是 NAPI 最容易混淆、也最能体现设计精妙的地方。\n先厘清两个\u0026quot;中断\u0026quot;和两个\u0026quot;上下文\u0026quot; 两种\u0026quot;关中断\u0026quot;完全不同:\n网卡层面(硬件寄存器) CPU 层面 操作 写 e1000 的 IMC/IMS 寄存器 local_irq_disable/save 作用 让网卡不再往 CPU 发中断信号 让当前 CPU 不响应任何中断 粒度 只影响这块网卡 影响整个 CPU NAPI 主要用 ★这个★ 只在改 poll_list 等极短临界区 E1000 的两个寄存器:\nIMC(Interrupt Mask Clear): 写入的位被清除出掩码 → 屏蔽中断; 写 ~0 = 屏蔽全部。 IMS(Interrupt Mask Set): 写入的位被加入掩码 → 使能中断。 两种\u0026quot;上下文\u0026quot;:\n硬中断上下文 (hardirq): 网卡拉中断线, CPU 立即跳进 e1000_intr。必须极快、不能睡眠, 期间该 CPU 上其他中断被抑制。 软中断上下文 (softirq): 硬中断退出时 (irq_exit) 触发, net_rx_action 在此运行。开着中断跑, 可被硬中断打断, 适合耗时的批量收包。 NAPI 精髓: 硬中断只做\u0026quot;记账 + 关网卡中断 + 踢软中断\u0026quot;三件事, 把真正的收包挪到软中断。\nnet_rx_action 在哪注册 在 net/core/dev.c:3139 的 net_dev_init()(启动早期由 subsys_initcall 调用一次)里注册:\n1 2 open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL); // ★注册收包软中断★ open_softirq 只是把处理函数填进全局软中断向量表 softirq_vec[NET_RX_SOFTIRQ].action。之后 __raise_softirq_irqoff(NET_RX_SOFTIRQ) 点亮 pending 位, do_softirq() 按下标查表调用 net_rx_action。同一 net_dev_init 里还初始化了每 CPU 的 softnet_data(队列 + poll_list)与 backlog_dev 伪设备(poll = process_backlog)。\n驱动回调 dev-\u0026gt;poll 的注册时机 net_rx_action 里 dev-\u0026gt;poll(dev, \u0026amp;budget) 调用的 e1000_clean, 是在 e1000_probe()(网卡被 PCI 子系统发现、驱动初始化设备时)一次性静态注册的, 而非运行时反复赋值。位置在 e1000_main.c:449, alloc_etherdev 之后、register_netdev 之前:\n1 2 3 4 5 6 7 8 // e1000_probe netdev-\u0026gt;open = \u0026amp;e1000_open; netdev-\u0026gt;hard_start_xmit = \u0026amp;e1000_xmit_frame; // 发包入口同处注册 #ifdef CONFIG_E1000_NAPI netdev-\u0026gt;poll = \u0026amp;e1000_clean; // ★NAPI poll 回调在此注册★ netdev-\u0026gt;weight = 64; // ★配额权重, 决定单轮预算★ #endif 触发链: insmod → e1000_init_module → pci_module_init(\u0026amp;e1000_driver) → (PCI匹配) → e1000_probe → netdev-\u0026gt;poll = \u0026amp;e1000_clean → register_netdev。\n三个时间点要分清:\n阶段 函数 做什么 驱动加载/设备发现 e1000_probe 把 e1000_clean 填入 netdev-\u0026gt;poll、weight=64 网卡 up(ifconfig up) e1000_open request_irq 注册 e1000_intr、分配 ring、netif_start_queue 每次收包(运行时) net_rx_action 通过 dev-\u0026gt;poll(dev,\u0026amp;budget) 调用已注册的 e1000_clean weight = 64 与 poll 同处注册, 它就是 e1000_clean 里 work_to_do 的上限来源(__netif_rx_schedule 里 dev-\u0026gt;quota = dev-\u0026gt;weight 重置)。因此\u0026quot;每轮最多收 64 个包\u0026quot;这个策略也是在 probe 阶段随 poll 一起定死的。\nNAPI 模式: 完整软硬中断切换时间线 sequenceDiagram participant NIC as e1000网卡 participant HARD as e1000_intr\u0026lt;br/\u0026gt;(硬中断上下文) participant IE as do_IRQ/irq_exit participant SOFT as net_rx_action\u0026lt;br/\u0026gt;(软中断上下文) participant CLEAN as e1000_clean NIC-\u0026gt;\u0026gt;HARD: 拉中断线(有包了) Note over HARD: 极短! 只做3件事 HARD-\u0026gt;\u0026gt;NIC: 写IMC=~0 关网卡中断 HARD-\u0026gt;\u0026gt;HARD: __netif_rx_schedule\u0026lt;br/\u0026gt;挂poll_list + 点亮软中断标志 HARD--\u0026gt;\u0026gt;IE: return IRQ_HANDLED Note over IE: irq_exit()减preempt_count\u0026lt;br/\u0026gt;硬中断上下文结束 IE-\u0026gt;\u0026gt;SOFT: 检查pending, 执行NET_RX_SOFTIRQ loop budget未耗尽 SOFT-\u0026gt;\u0026gt;CLEAN: dev-\u0026gt;poll(dev,\u0026amp;budget) CLEAN-\u0026gt;\u0026gt;CLEAN: e1000_clean_rx_irq 批量收包建skb进栈 alt 环已空(work_done\u0026lt;budget) CLEAN-\u0026gt;\u0026gt;NIC: 写IMS 开网卡中断 CLEAN--\u0026gt;\u0026gt;SOFT: netif_rx_complete, 返回0 else 还有包 CLEAN--\u0026gt;\u0026gt;SOFT: 返回非0, 重排队尾 end end 步骤 1 — 硬中断入口(e1000_main.c:2111, 硬中断上下文):\n1 2 uint32_t icr = E1000_READ_REG(hw, ICR); // 读中断原因(读动作即清标志) if (!icr) return IRQ_NONE; // icr=0: 共享IRQ, 不是本卡, 让给别人 步骤 2 — 关网卡中断 + 挂软中断(仍在硬中断上下文):\n1 2 3 4 5 6 if (netif_rx_schedule_prep(netdev)) { // test-and-set RX_SCHED位, 防重复调度 atomic_inc(\u0026amp;adapter-\u0026gt;irq_sem); // 与 irq_enable 配对计数 E1000_WRITE_REG(hw, IMC, ~0); // ★关网卡中断: 要开始轮询, 别再打扰★ __netif_rx_schedule(netdev); // 挂 poll_list + __raise_softirq(NET_RX) } return IRQ_HANDLED; // 极短, 收包一个都没做! __netif_rx_schedule 里 __raise_softirq_irqoff 只点亮 pending 位, 不立即执行软中断——真正执行要等硬中断退出。\n步骤 4 — 软中断批量收包(dev.c:1764, 软中断上下文): net_rx_action 用 budget(全局300)与\u0026quot;占用不超过1个tick\u0026quot;双限制遍历 poll_list, 调 dev-\u0026gt;poll(即 e1000_clean)。budget/weight 的意义: 软中断虽开中断, 但会推迟普通进程调度, 必须限量防止一个疯狂来包的网卡饿死其他设备与进程。\n步骤 5 — 收完才开网卡中断(e1000_main.c:2157):\n1 2 3 4 5 if (work_done \u0026lt; work_to_do) { // 收到比预算少 = 环已空 = 收干净了 netif_rx_complete(netdev); // 先退出轮询, 清 RX_SCHED 位 e1000_irq_enable(adapter); // ★再开网卡中断(写IMS)★ return 0; } irq_sem 计数配对: 步骤2 atomic_inc 关, 这里 atomic_dec_and_test 减到 0 才真正 IMS 开中断, 避免与看门狗等其他关中断者竞态。\n硬中断到底在哪个点退出 e1000_intr 的 return IRQ_HANDLED 不是退出点——它只是从处理函数返回, 仍在硬中断上下文。真正退出在 do_IRQ 随后调用的 irq_exit():\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // arch/i386/kernel/irq.c:48 fastcall unsigned int do_IRQ(struct pt_regs *regs) { irq_enter(); // 进入硬中断: preempt_count += HARDIRQ_OFFSET __do_IRQ(irq, regs); // 内部 → handle_IRQ_event → e1000_intr irq_exit(); // ★退出硬中断上下文就在这里★ return 1; } // kernel/irq/handle.c:78 void irq_exit(void) { preempt_count() -= IRQ_EXIT_OFFSET; // ★这一行: 清HARDIRQ计数, 硬中断正式结束★ if (!in_interrupt() \u0026amp;\u0026amp; local_softirq_pending()) do_softirq(); // 已离开中断上下文, 就地跑软中断 preempt_enable_no_resched(); } 精确答案: 退出点是 irq_exit() 里的 preempt_count() -= IRQ_EXIT_OFFSET;。irq_enter 时 += HARDIRQ_OFFSET 标记\u0026quot;在硬中断中\u0026quot;, 这一行减回去后 in_interrupt() 对硬中断转假, 硬中断上下文才真正结束。紧接着的 if 检查到步骤2点亮的 NET_RX_SOFTIRQ, 在同一次 irq_exit 里、退出之后立刻执行 do_softirq() → net_rx_action(通常复用同一内核栈)。\n非NAPI 模式对比 非NAPI 下 e1000_intr 走另一分支——在硬中断里直接循环收包:\n1 2 3 for (i = 0; i \u0026lt; E1000_MAX_INTR; i++) if (!e1000_clean_rx_irq(adapter) \u0026amp; !e1000_clean_tx_irq(adapter)) break; 关键澄清: 这里 e1000_clean_rx_irq 在硬中断上下文里建好 skb 后调 netif_rx, 而 netif_rx 只是把 skb 塞进 per-CPU backlog 队列并点亮 NET_RX_SOFTIRQ, 它不会在硬中断里把包推给 ip_rcv。真正进栈(process_backlog → netif_receive_skb → ip_rcv)同样推迟到软中断。\n非NAPI NAPI 硬中断里做什么 循环 e1000_clean_rx_irq 建 skb + netif_rx 入 backlog 只 IMC 关中断 + __netif_rx_schedule 建 skb 在哪个上下文 硬中断(重!) 软中断(e1000_clean 里) 硬中断退出点 do_IRQ → irq_exit() 减 preempt_count 完全相同 退出后软中断做什么 process_backlog 出队 → 进栈 e1000_clean 收包建 skb → 进栈 两模式硬中断退出点完全一样; 区别只在: 非NAPI 把\u0026quot;建 skb\u0026quot;这件较重的活留在硬中断, NAPI 连建 skb 都推迟到软中断, 硬中断只剩极短三步——这正是 NAPI 更抗高负载的原因。\n为什么开网卡中断在 netif_rx_complete 之后, 而非硬中断 exit 后 核心: 硬中断 exit 时, 包还一个没收、环还满着、轮询还没结束。 若那时开中断:\n时间线错位: 硬中断里只关中断挂队列, RX 环里的包原封不动。此时开中断, 环里未处理的包(DD 位仍在)会让网卡立即再次触发硬中断, 而软中断可能还没跑。 退化成中断风暴: 每来几个包就一次硬中断, 轮询批处理收益归零, 高负载下 CPU 全耗在中断进出开销上, NAPI 白做。 语义正确: 只有 work_done \u0026lt; work_to_do(环已空)时开中断才合理——轮询已无意义, 恢复中断静默等下一批。反之还有包就继续轮询, 轮询模式下不需要中断。 防竞态: 必须先 netif_rx_complete(清 RX_SCHED 位)再 e1000_irq_enable。若顺序反了, 新包硬中断里 netif_rx_schedule_prep 会因标志还在而调度失败, 导致漏收。 irq_exit 是内核通用路径, 根本不知道\u0026quot;这块网卡 RX 环收干净没、RX_SCHED 该不该清\u0026quot;——这些只有驱动 + NAPI 状态机掌握, 所以开中断只能由 e1000_clean 在轮询收尾时自己决定。这就是 NAPI\u0026quot;有包轮询、无包中断\u0026quot;自适应的落地点。\n2.6.10 NAPI 与现代内核 (5.4.x) 的差异 理念相同(中断缓解: 关中断→轮询→budget限量→收完开中断), 实现差别很大:\n维度 2.6.10 5.4.x 轮询单元 net_device 本身, NAPI 状态直接是其成员(dev-\u0026gt;poll/quota/weight) 独立的 struct napi_struct(2.6.24 引入), 与 net_device 解耦 多队列 一网卡只能一个 NAPI 上下文 每 RX 队列一个 napi_struct, 多核并行收包 API netif_rx_schedule/netif_rx_complete/dev-\u0026gt;poll(dev,\u0026amp;budget) napi_schedule/napi_complete_done/napi-\u0026gt;poll(napi,budget) backlog per-CPU 伪 net_device(backlog_dev) softnet_data.backlog 是 napi_struct, 配合 RPS 可跨 CPU 时间限制 jiffies - start_time \u0026gt; 1(1个tick) netdev_budget(300) + netdev_budget_usecs(2000µs) 新增机制 无 GRO、RPS/RFS、busy-poll、XDP 等一整套 注: 线程化 NAPI (threaded NAPI) 是 5.12 才引入的, 5.4.x 还没有。2.6.10 是\u0026quot;net_device 内嵌式\u0026quot;第一代实现; 现代是\u0026quot;napi_struct 对象化\u0026quot;成熟形态, 并围绕它长出高性能收包生态。核心思路一致, 能力不可同日而语。\n完整收发包全景图与函数索引 收包全景(硬件 → 应用) sequenceDiagram participant HW as e1000硬件 participant IRQ as e1000_intr participant SIRQ as net_rx_action participant POLL as e1000_clean/process_backlog participant CRX as e1000_clean_rx_irq participant NRS as netif_receive_skb participant IP as ip_rcv→tcp/udp participant APP as 用户recv() HW-\u0026gt;\u0026gt;HW: DMA写缓冲区, 置DD, 更新RDH HW-\u0026gt;\u0026gt;IRQ: 硬件中断 IRQ-\u0026gt;\u0026gt;SIRQ: 屏蔽中断 + __netif_rx_schedule SIRQ-\u0026gt;\u0026gt;POLL: dev-\u0026gt;poll(dev,\u0026amp;budget) POLL-\u0026gt;\u0026gt;CRX: e1000_clean_rx_irq CRX-\u0026gt;\u0026gt;CRX: DD/EOP检查, unmap, skb_put, eth_type_trans CRX-\u0026gt;\u0026gt;NRS: netif_receive_skb NRS-\u0026gt;\u0026gt;IP: ptype_base查表 → ip_rcv IP-\u0026gt;\u0026gt;APP: 入sock队列, 唤醒recv() POLL-\u0026gt;\u0026gt;IRQ: netif_rx_complete + e1000_irq_enable 发包全景(应用 → 硬件) sequenceDiagram participant APP as 用户send() participant TCP as tcp/udp+IP层 participant DQX as dev_queue_xmit participant Q as Qdisc participant XMIT as e1000_xmit_frame participant HW as e1000硬件 participant CTX as e1000_clean_tx_irq APP-\u0026gt;\u0026gt;TCP: 数据下行, 构造skb+TCP/IP头 TCP-\u0026gt;\u0026gt;DQX: ip_finish_output → dev_queue_xmit DQX-\u0026gt;\u0026gt;Q: q-\u0026gt;enqueue + qdisc_run Q-\u0026gt;\u0026gt;XMIT: hard_start_xmit XMIT-\u0026gt;\u0026gt;XMIT: tso/csum → tx_map → tx_queue XMIT-\u0026gt;\u0026gt;HW: 写TDT发车 HW-\u0026gt;\u0026gt;HW: DMA读数据, 发送, 置DD HW-\u0026gt;\u0026gt;CTX: 发送完成中断 CTX-\u0026gt;\u0026gt;CTX: 查DD, unmap, kfree_skb CTX-\u0026gt;\u0026gt;Q: netif_wake_queue(若曾停队列) 关键函数索引 函数 文件:行 角色 dev_queue_xmit net/core/dev.c:1218 协议栈发包入口 e1000_xmit_frame e1000_main.c:1752 驱动发包入口 (hard_start_xmit) e1000_tso e1000_main.c:1488 TSO 上下文描述符 e1000_tx_csum e1000_main.c:1542 硬件校验和上下文描述符 e1000_tx_map e1000_main.c:1573 SKB → DMA 映射 e1000_tx_queue e1000_main.c:1664 填描述符 + 写 TDT e1000_clean_tx_irq e1000_main.c:2188 发送完成回收 e1000_intr e1000_main.c:2111 中断入口 e1000_clean e1000_main.c:2157 NAPI poll 回调 e1000_clean_rx_irq e1000_main.c:2251 收包核心 e1000_rx_checksum e1000_main.c:2597 收端校验和 e1000_alloc_rx_buffers e1000_main.c:2364 补 RX 缓冲区 + 写 RDT eth_type_trans net/ethernet/eth.c:159 剥以太网头, 定协议 netif_rx net/core/dev.c:1423 非NAPI 入 backlog 队列 __netif_rx_schedule netdevice.h:811 挂 poll_list, 触发软中断 net_rx_action net/core/dev.c:1764 NET_RX_SOFTIRQ 处理 process_backlog net/core/dev.c:1716 backlog 出队 netif_receive_skb net/core/dev.c:1625 协议分发 ip_rcv net/ipv4/ip_input.c:360 IP 层入口 收发对称性总结 维度 接收 RX 发送 TX 协议栈交接 netif_receive_skb/netif_rx dev-\u0026gt;hard_start_xmit (e1000_xmit_frame) 驱动核心 e1000_clean_rx_irq e1000_xmit_frame+e1000_tx_queue 硬件通知 软件写 RDT 补缓冲区 软件写 TDT 发数据 硬件指针 RDH(硬件写处) TDH(硬件读处) 完成标志 status.DD(硬件置位) upper.DD(硬件置位) 中断后处理 e1000_clean_rx_irq 上送 e1000_clean_tx_irq 回收 背压 backlog throttle / NAPI 预算 netif_stop_queue/netif_wake_queue+Qdisc 总结: E1000 收发包的本质是围绕 描述符环 + DMA + DD 位的软硬件协作。 发送: e1000_xmit_frame 算描述符 → tso/csum 填卸载参数 → tx_map 做 DMA 映射 → tx_queue 写 TDT 发车 → clean_tx_irq 靠 DD 位回收。 接收: 硬件置 DD 触发中断 → e1000_intr 关中断调度 NAPI → e1000_clean_rx_irq 靠 DD 位取包、eth_type_trans 定协议 → netif_receive_skb 查 ptype_base → ip_rcv 进网络层 → socket 唤醒应用。 两条路径通过 netif_stop/wake_queue 与 backlog/NAPI 预算实现流量背压,通过硬件卸载(TSO/校验和)大幅降低 CPU 负担。\n","date":"2026-07-10T20:11:37+08:00","permalink":"https://charles-7777.github.io/p/linux-e1000-%E7%BD%91%E5%8D%A1%E9%A9%B1%E5%8A%A8%E6%94%B6%E5%8F%91%E5%8C%85%E8%B7%AF%E5%BE%84%E8%A7%A3%E6%9E%90/","title":"Linux E1000 网卡驱动收发包路径解析"},{"content":"文件系统与调试基础 File System in Linux 提供非易失性数据的存储空间。\n一种特定的数据存储格式，包含两部分：数据本身（Data）和元数据（Meta-data）。\n每种文件系统类型使用自身的元数据结构来定义数据的存储和访问方式。\n支持所有文件操作。\n一些最常用的文件系统包括：EXT4、EXT3、FAT-32、NTFS。\ngraph LR subgraph UserSpace[用户空间] App[Application] end subgraph KernelSpace[内核空间] SysCall[System Call] VFS[Virtual File System] EXT3[EXT3] EXT4[EXT4] FAT32[FAT-32] NTFS[NTFS] end subgraph HardwareLayer[硬件层] Hardware[Hardware] end App --\u0026gt; SysCall SysCall --\u0026gt; VFS VFS --\u0026gt; EXT3 VFS --\u0026gt; EXT4 VFS --\u0026gt; FAT32 VFS --\u0026gt; NTFS EXT3 --\u0026gt; Hardware EXT4 --\u0026gt; Hardware FAT32 --\u0026gt; Hardware NTFS --\u0026gt; Hardware style UserSpace fill:#e1f5fe,stroke:#01579b style KernelSpace fill:#fff3e0,stroke:#e65100 style HardwareLayer fill:#e8f5e9,stroke:#1b5e20 Disadvantage of printk as debugging tool 修改源代码可能并非易事，也无法快速完成。 不必要的 printk 可能会影响整体性能。 你需要维护两个版本的软件；一个包含调试信息，另一个不包含。 有时，printk 会掩盖实际的问题。 在大多数情况下，获取信息的最佳方式是查询系统。 procFS 概述 What is proc 通过查询从内核及内核模块获取信息的技术之一。 /proc 文件系统是一个特殊的、由软件创建的文件系统。 提供了一个用于在用户空间和内核空间之间进行通信的接口。 它包含关于当前正在运行的进程的有用信息，例如 /proc/modules \u0026ndash; 提供模块列表 /proc/meminfo \u0026ndash; 统计内存使用情况 procFS vs other File Systems proc 是虚拟文件系统或伪文件系统。 数据不存储在非易失性存储器中。 它没有实际的文件。 proc 没有像标准文件系统那样的数据存储格式。 文件系统独立于操作系统。然而，/proc 并非在所有操作系统中都可用。 Importance of procFS Maintains information about the currently running system\n1 2 3 4 5 6 7 8 9 charles@ubuntu24:~$ cat /proc/meminfo MemTotal: 1995676 kB MemFree: 698620 kB MemAvailable: 1642032 kB Buffers: 59008 kB Cached: 993020 kB SwapCached: 0 kB Active: 139892 kB Inactive: 939312 kB ​ Useful tool to monitor the system\n1 2 3 4 5 charles@ubuntu24:~$ cat /proc/net/route Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT enp0s3 00000000 0202000A 0003 0 0 100 00000000 0 0 0 enp0s3 0002000A 00000000 0001 0 0 100 00FFFFFF 0 0 0 charles@ubuntu24:~$ cat /proc/net/nf_conntrack tcp 6 431999 ESTABLISHED src=192.168.1.10 dst=93.184.216.34 sport=54321 dport=80 src=93.184.216.34 dst=192.168.1.10 sport=80 dport=54321 [ASSURED] mark=0 use=1 It is used to configure and debug the system. /proc/sysdoes that\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 charles@ubuntu24:~$ cat /proc/sys/kernel/printk 4 4 1 7 charles@ubuntu24:~$ echo 7 \u0026gt; /proc/sys/kernel/printk charles@ubuntu24:~$ cat /proc/sys/kernel/printk 7 4 1 7 charles@ubuntu24:~$ cat /proc/sys/net/ipv4/ip_forward 0 charles@ubuntu24:~$ echo 1 \u0026gt; /proc/sys/net/ipv4/ip_forward charles@ubuntu24:~$ cat /proc/sys/net/ipv4/ip_forward 1 charles@ubuntu24:~$ ls -l /proc/80094/fd total 0 lrwx------ 1 root root 64 Jul 9 14:32 0 -\u0026gt; /dev/null lrwx------ 1 root root 64 Jul 9 14:32 1 -\u0026gt; /dev/null lr-x------ 1 root root 64 Jul 9 14:32 10 -\u0026gt; \u0026#39;pipe:[85474]\u0026#39; lr-x------ 1 root root 64 Jul 9 14:32 12 -\u0026gt; \u0026#39;pipe:[85475]\u0026#39; lrwx------ 1 root root 64 Jul 9 14:32 2 -\u0026gt; /dev/null lrwx------ 1 root root 64 Jul 9 14:32 3 -\u0026gt; \u0026#39;socket:[84325]\u0026#39; lrwx------ 1 root root 64 Jul 9 14:32 4 -\u0026gt; \u0026#39;socket:[84293]\u0026#39; lrwx------ 1 root root 64 Jul 9 14:32 5 -\u0026gt; \u0026#39;socket:[85467]\u0026#39; lrwx------ 1 root root 64 Jul 9 14:32 6 -\u0026gt; \u0026#39;socket:[85442]\u0026#39; l-wx------ 1 root root 64 Jul 9 14:32 8 -\u0026gt; \u0026#39;pipe:[85473]\u0026#39; l-wx------ 1 root root 64 Jul 9 14:32 9 -\u0026gt; /run/systemd/sessions/8.ref Creating a proc file and interfacing with user space Creating entry in /proc\nEntry can be created using proc_create()\nent = proc_create(\u0026quot;dummydev\u0026quot;, 0660, NULL, \u0026amp;myops);\nRemoving entry from /proc\nEntry can be removed using proc_remove() proc_remove(ent);\nHandlers of proc file\nTwo handlers; read and write.\n1 2 3 4 5 6 static struct file_operations myops = { .owner = THIS_MODULE, .read = dummy_read, .write = dummy_write, } Communicating with driver using proc\ncan use shell or program\n1 2 3 4 cat /proc/dummydev irq = 20 mode = 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void main(void) { char buf[100]; int fd = open(\u0026#34;/proc/dummydev\u0026#34;, O_RDWR); read(fd, buf, 100); puts(buf); lseek(fd, 0 , SEEK_SET); write(fd, \u0026#34;33 4\u0026#34;, 5); lseek(fd, 0 , SEEK_SET); read(fd, buf, 100); puts(buf); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include \u0026lt;linux/module.h\u0026gt; #include \u0026lt;linux/kernel.h\u0026gt; #include \u0026lt;linux/proc_fs.h\u0026gt; #include \u0026lt;linux/uaccess.h\u0026gt; #include \u0026lt;linux/seq_file.h\u0026gt; #define PROC_DIR_NAME \u0026#34;proc_demo\u0026#34; #define PROC_FILE_NAME \u0026#34;status\u0026#34; #define BUF_SIZE 1024 static char kernel_buffer[BUF_SIZE]; // 回调函数：读取文件内容 static int proc_show(struct seq_file *m, void *v) { seq_printf(m, \u0026#34;Message from kernel: %s\\n\u0026#34;, kernel_buffer); return 0; } // 回调函数：打开文件 static int proc_open(struct inode *inode, struct file *file) { return single_open(file, proc_show, NULL); } // 回调函数：向文件写入数据 static ssize_t proc_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { size_t len = (count \u0026gt;= BUF_SIZE) ? BUF_SIZE - 1 : count; if (copy_from_user(kernel_buffer, buf, len)) return -EFAULT; kernel_buffer[len] = \u0026#39;\\0\u0026#39;; printk(KERN_INFO \u0026#34;proc_demo: Received from user space: %s\\n\u0026#34;, kernel_buffer); return len; } // 文件操作结构体 static const struct proc_ops proc_fops = { .proc_open = proc_open, .proc_read = seq_read, .proc_write = proc_write, .proc_lseek = seq_lseek, .proc_release = single_release, }; static int __init proc_demo_init(void) { struct proc_dir_entry *dir, *file; // 1. 创建 /proc/proc_demo 目录 dir = proc_mkdir(PROC_DIR_NAME, NULL); if (!dir) return -ENOMEM; // 2. 在目录下创建 /proc/proc_demo/status 文件 file = proc_create(PROC_FILE_NAME, 0666, dir, \u0026amp;proc_fops); if (!file) { remove_proc_entry(PROC_DIR_NAME, NULL); // 清理已创建的目录 return -ENOMEM; } // 初始化默认消息 strcpy(kernel_buffer, \u0026#34;Hello from /proc/proc_demo/status!\u0026#34;); printk(KERN_INFO \u0026#34;proc_demo: Directory /proc/%s created successfully\\n\u0026#34;, PROC_DIR_NAME); return 0; } static void __exit proc_demo_exit(void) { // 3. 安全地删除整个目录及其下的所有子文件 remove_proc_subtree(PROC_DIR_NAME, NULL); printk(KERN_INFO \u0026#34;proc_demo: /proc/%s removed\\n\u0026#34;, PROC_DIR_NAME); } module_init(proc_demo_init); module_exit(proc_demo_exit); MODULE_LICENSE(\u0026#34;GPL\u0026#34;); MODULE_AUTHOR(\u0026#34;Test Author\u0026#34;); MODULE_DESCRIPTION(\u0026#34;A /proc directory and file demo module\u0026#34;); 1 2 3 4 5 6 7 obj-m += proc_demo.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 1 2 3 4 5 6 7 8 charles@ubuntu24:~/test-proc$ sudo insmod proc_demo.ko charles@ubuntu24:~/test-proc$ ls /proc/proc_demo status charles@ubuntu24:~/test-proc$ echo 1111 \u0026gt; /proc/proc_demo/status charles@ubuntu24:~/test-proc$ dmesg -c [176365.569965] proc_demo: Received from user space: 1111 charles@ubuntu24:~/test-proc$ cat /proc/proc_demo/status Message from kernel: 1111 SysFS 概述 What is sysfs sysfs 是一个挂载在 /sys 下的内存文件系统（RAM-based），它把内核内部的对象模型（设备、驱动、总线、内核子系统等）以“目录 + 文件”的形式导出到用户空间。相比 procFS，它更加结构化、规范化。\n它的核心价值在于：\n一个属性一个文件：每个文件通常只表示一个值（one value per file），便于 shell、脚本读写； 树形结构反映内核对象关系：目录层级对应内核对象（kobject）的父子/从属关系； 替代 /proc 的滥用：早期很多设备信息塞进 /proc，sysfs 提供了结构化、规范化的替代方案。 什么是内存文件系统（RAM-based） “内存文件系统”指的是：文件系统的数据结构和内容完全存在于内存（RAM）中，不落盘到任何块设备（硬盘、SSD、U 盘）上。\n核心特征：\n无后端存储介质：普通文件系统（EXT4、NTFS、FAT32）的数据最终写到磁盘等非易失存储上，掉电后仍在；内存文件系统只活在 RAM 里，掉电/重启即消失。 内容通常是“动态生成”的，而非“存储”的：对于 sysfs / procFS 这类特殊内存文件系统，文件里的内容甚至不是提前存好的字节，而是你 cat 的那一刻由内核回调函数临时生成的。例如 cat /sys/kernel/sysfs_demo/foo 会触发内核调用 foo_show() → sprintf(buf, \u0026quot;%d\\n\u0026quot;, foo)，把当前内核变量 foo 的值现算现返回。 没有真正的磁盘布局/元数据格式：procFS/sysfs “没有实际的文件”，也“没有像标准文件系统那样的数据存储格式”，只是借用了 VFS 的“文件/目录”接口外壳，底层数据来自内核对象（sysfs 用 kobject + kernfs）。 内存文件系统其实有两种典型形态：\n类型 例子 内容来源 用途 数据型内存 FS tmpfs、ramfs 真的把你写入的文件字节存在 RAM 里 当作高速临时磁盘用（如 /tmp、/dev/shm） 接口型内存 FS（伪文件系统） procfs、sysfs、debugfs 内容由内核动态生成，是内核状态的“视图” 用户空间 ↔ 内核空间通信 sysfs 属于第二类：它不是用来存文件的，而是把内核对象模型（设备、驱动、总线等）以目录+文件的形式“投影”出来，给用户空间一个读写内核状态的规范接口。\n一句话总结：内存文件系统 = 只存在于 RAM、掉电即失的文件系统；而 sysfs 更进一步，连内容都不真正“存储”，而是内核在你访问的瞬间实时生成的一层结构化视图。\nsysfs vs procFS 维度 procFS (/proc) sysfs (/sys) 定位 进程信息 + 早期的杂项内核接口 内核对象模型（设备/驱动/总线）的结构化视图 组织方式 较随意，一个文件可含多行多字段 规范：一个文件一个值 底层实现 proc_fs / seq_file kobject + kernfs 常见用途 状态查询、/proc/sys 调参 设备属性导出、驱动调参与状态 常见的 /sys 顶层目录：\n目录 含义 /sys/kernel 内核自身的可调参数、子系统入口 /sys/devices 系统中所有设备的全局设备树 /sys/class 按“功能类别”组织的设备视图（如 net、block） /sys/bus 按总线组织（如 pci、usb、platform） /sys/module 已加载模块及其参数 sysfs 核心原理：kobject + attribute + kernfs sysfs 本身几乎不存放数据，它是内核对象模型的“视图层”。要理解它，需要抓住三个关键抽象。\nkobject —— 目录 kobject 是 sysfs 目录的载体。每一个出现在 /sys 里的目录，背后基本都对应一个 kobject（源码：include/linux/kobject.h）。\n1 2 3 4 5 6 7 8 9 10 struct kobject { const char *name; struct list_head entry; struct kobject *parent; /* 决定目录层级 */ struct kset *kset; struct kobj_type *ktype; /* 决定属性如何读写 */ struct kernfs_node *sd; /* sysfs directory entry，指向 kernfs 节点 */ struct kref kref; /* 引用计数，控制生命周期 */ ... }; 关键点：\nparent 决定该目录挂在谁下面； sd（sysfs directory）指向底层的 kernfs_node，这是真正在虚拟文件系统里存在的节点； kref 是引用计数，kobject_get/put 增减引用，归零时调用 ktype-\u0026gt;release 释放。 attribute —— 文件 目录里的“文件”由 struct attribute 描述，它只声明文件名和权限，本身不含读写逻辑（源码：include/linux/sysfs.h）。\n1 2 3 4 5 struct attribute { const char *name; /* 文件名 */ umode_t mode; /* 权限，如 0664 */ ... }; sysfs_ops / kobj_type —— 读写逻辑的分发 真正的读写行为绑定在 kobj_type 上。它把 attribute 与一组 sysfs_ops 关联起来（源码：include/linux/kobject.h）。\n1 2 3 4 5 6 7 struct kobj_type { void (*release)(struct kobject *kobj); /* 引用计数归零时释放 */ const struct sysfs_ops *sysfs_ops; /* show/store 分发入口 */ struct attribute **default_attrs; const struct attribute_group **default_groups; ... }; 当用户态 cat 一个 sysfs 文件时，VFS → kernfs → sysfs_ops-\u0026gt;show()，再由 show 找到对应的 attribute 并调用具体处理函数；echo \u0026gt; file 则走 store()。\nkobj_attribute —— 最常用的“文件 + 处理函数”打包 对于挂在 /sys/kernel 这类简单场景，内核提供了现成的 struct kobj_attribute，把 attribute 和 show/store 直接绑在一起（源码：include/linux/kobject.h）。\n1 2 3 4 5 6 7 struct kobj_attribute { struct attribute attr; ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr, char *buf); ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count); }; 配合 __ATTR 宏可以一行完成定义（源码：include/linux/sysfs.h）。\n1 2 3 4 5 6 #define __ATTR(_name, _mode, _show, _store) { \\ .attr = {.name = __stringify(_name), \\ .mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \\ .show = _show, \\ .store = _store, \\ } 注意 VERIFY_OCTAL_PERMISSIONS：sysfs 属性不允许 world-writable，否则编译期就会报错。\n一张图理清调用链 1 2 3 4 5 6 7 8 9 10 11 用户态: cat /sys/kernel/xxx/foo │ VFS read() │ kernfs 文件操作 │ kobj_type-\u0026gt;sysfs_ops-\u0026gt;show() │ kobj_attribute-\u0026gt;show() ← 你写的函数 │ sprintf(buf, \u0026#34;%d\\n\u0026#34;, foo) 写方向（echo 5 \u0026gt; foo）完全对称，走 store()。\n常用 API 速查 API 作用 kobject_create_and_add(name, parent) 创建一个 kobject 目录并加入 sysfs sysfs_create_file(kobj, attr) 在目录下创建单个文件 sysfs_create_group(kobj, group) 一次性创建一组文件 sysfs_remove_group(kobj, group) 移除一组文件 kobject_put(kobj) 减少引用，归零后销毁目录 attribute_group 用于批量管理属性，若指定 .name 会额外生成一层子目录（源码：include/linux/sysfs.h）。\nCreating a sysfs file and interfacing with user space Linux 源码本身就自带一个非常好的示例：samples/kobject/kobject-example.c。它会在 /sys/kernel/kobject_example/ 下创建 foo、baz、bar 三个可读写文件。\n下面基于它给出一个精简、可独立编译的版本，并附带构建/运行步骤。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include \u0026lt;linux/module.h\u0026gt; #include \u0026lt;linux/init.h\u0026gt; #include \u0026lt;linux/kobject.h\u0026gt; #include \u0026lt;linux/sysfs.h\u0026gt; #include \u0026lt;linux/string.h\u0026gt; /* 被 sysfs 文件读写的内核变量 */ static int foo; /* cat 文件时被调用 */ static ssize_t foo_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return sprintf(buf, \u0026#34;%d\\n\u0026#34;, foo); } /* echo \u0026gt; 文件时被调用 */ static ssize_t foo_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { int ret = kstrtoint(buf, 10, \u0026amp;foo); if (ret \u0026lt; 0) return ret; return count; /* 必须返回已消费的字节数 */ } /* 定义属性：0664 -\u0026gt; 允许 owner/group 写，但不允许 world-writable, “world-writable” 在 Linux 权限语境下，特指文件对“其他用户（Others）”拥有写权限。 6 (owner) = 读(4) + 写(2) = 可读可写 6 (group) = 读(4) + 写(2) = 可读可写 4 (others) = 只读(4) */ static struct kobj_attribute foo_attribute = __ATTR(foo, 0664, foo_show, foo_store); static struct attribute *attrs[] = { \u0026amp;foo_attribute.attr, NULL, /* 必须以 NULL 结尾 */ }; static struct attribute_group attr_group = { .attrs = attrs, }; static struct kobject *demo_kobj; static int __init sysfs_demo_init(void) { int ret; /* 在 /sys/kernel/ 下创建 sysfs_demo 目录 */ demo_kobj = kobject_create_and_add(\u0026#34;sysfs_demo\u0026#34;, kernel_kobj); if (!demo_kobj) return -ENOMEM; /* 在该目录下创建属性文件 */ ret = sysfs_create_group(demo_kobj, \u0026amp;attr_group); if (ret) kobject_put(demo_kobj); pr_info(\u0026#34;sysfs_demo: loaded, see /sys/kernel/sysfs_demo/\\n\u0026#34;); return ret; } static void __exit sysfs_demo_exit(void) { kobject_put(demo_kobj); /* 引用归零后自动删除目录及文件 */ pr_info(\u0026#34;sysfs_demo: unloaded\\n\u0026#34;); } module_init(sysfs_demo_init); module_exit(sysfs_demo_exit); MODULE_LICENSE(\u0026#34;GPL v2\u0026#34;); MODULE_AUTHOR(\u0026#34;your name\u0026#34;); MODULE_DESCRIPTION(\u0026#34;A minimal sysfs demo module\u0026#34;); 1 2 3 4 5 6 7 8 9 obj-m += sysfs_demo.o KDIR ?= /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 编译（需要先安装对应内核头文件/源码） make # 2. 加载模块 sudo insmod sysfs_demo.ko dmesg | tail # 应看到 \u0026#34;sysfs_demo: loaded ...\u0026#34; # 3. 查看目录与文件 ls -l /sys/kernel/sysfs_demo/ # -rw-rw-r-- 1 root root 4096 ... foo # 4. 读值（默认 0） cat /sys/kernel/sysfs_demo/foo # 0 # 5. 写值 echo 42 | sudo tee /sys/kernel/sysfs_demo/foo cat /sys/kernel/sysfs_demo/foo # 42 # 6. 卸载 sudo rmmod sysfs_demo dmesg | tail # 应看到 \u0026#34;sysfs_demo: unloaded\u0026#34; 编写 sysfs 属性的注意事项 一个文件一个值：不要在一个文件里塞多个字段，保持简单可解析。 show 的 buf 大小是一个 PAGE_SIZE：写入内容不要超过一页（通常 4KB），用 sysfs_emit()（新内核）或 sprintf 时注意边界。 store 返回值语义：返回“已消费字节数”（通常是 count），或返回负的错误码；返回 0 会导致用户态无限重试。 权限不能 world-writable：__ATTR 里的 mode 若包含 0002 会被 VERIFY_OCTAL_PERMISSIONS 拒绝编译。 生命周期用引用计数管理：目录的销毁靠 kobject_put()，不要手动去删底层文件。 动态命名对象用 kobject_init_and_add + uevent：kobject_create_and_add 适合名字固定的简单目录，不会发送 uevent。 小结 sysfs = 内核对象模型（kobject）在用户空间的结构化视图； 目录 = kobject，文件 = attribute，读写逻辑 = sysfs_ops/kobj_attribute； 底层由 kernfs 提供虚拟文件系统能力，kobject 通过 sd 挂接； 用 kobject_create_and_add + sysfs_create_group 即可几十行代码导出一个可读写接口，非常适合驱动调参与状态导出。 想进一步深入，可阅读本仓库文档 Documentation/filesystems/sysfs.txt 以及 samples/kobject/ 下的两个官方示例。\n","date":"2026-07-09T23:12:45+08:00","permalink":"https://charles-7777.github.io/p/linux-procfs-%E5%92%8C-sysfs/","title":"Linux procFS 和 SysFS"},{"content":" ifconfig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 deth0: flags=4163\u0026lt;UP,BROADCAST,RUNNING,MULTICAST\u0026gt; mtu 1500 inet 192.168.57.100 netmask 255.255.255.0 broadcast 192.168.57.255 inet6 fe80::6c67:4aff:febf:707e prefixlen 64 scopeid 0x20\u0026lt;link\u0026gt; ether 6e:67:4a:bf:70:7e txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 11 bytes 866 (866.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 enp0s3: flags=4163\u0026lt;UP,BROADCAST,RUNNING,MULTICAST\u0026gt; mtu 1500 inet 10.0.2.15 netmask 255.255.255.0 broadcast 10.0.2.255 inet6 fd17:625c:f037:2:a00:27ff:fe91:b6ab prefixlen 64 scopeid 0x0\u0026lt;global\u0026gt; inet6 fe80::a00:27ff:fe91:b6ab prefixlen 64 scopeid 0x20\u0026lt;link\u0026gt; ether 08:00:27:91:b6:ab txqueuelen 1000 (Ethernet) RX packets 87025 bytes 129107565 (129.1 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 5980 bytes 388262 (388.2 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 enp0s8: flags=4163\u0026lt;UP,BROADCAST,RUNNING,MULTICAST\u0026gt; mtu 1500 inet 192.168.56.101 netmask 255.255.255.0 broadcast 192.168.56.255 inet6 fe80::a00:27ff:fe44:1c29 prefixlen 64 scopeid 0x20\u0026lt;link\u0026gt; ether 08:00:27:44:1c:29 txqueuelen 1000 (Ethernet) RX packets 7117 bytes 553441 (553.4 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 10490 bytes 1379651 (1.3 MB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73\u0026lt;UP,LOOPBACK,RUNNING\u0026gt; mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10\u0026lt;host\u0026gt; loop txqueuelen 1000 (Local Loopback) RX packets 176 bytes 16136 (16.1 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 176 bytes 16136 (16.1 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 graph LR subgraph Application[\u0026#34;Application\u0026#34;] end subgraph UserSpace[\u0026#34;User Space\u0026#34;] Application end subgraph KernelSpace[\u0026#34;Kernel Space\u0026#34;] SystemCall[\u0026#34;System Call\u0026#34;] SocketLayer[\u0026#34;Socket Layer\u0026#34;] TransportLayer[\u0026#34;Transport Layer\u0026#34;] NetworkLayer[\u0026#34;Network Layer\u0026#34;] NetworkDriver[\u0026#34;Network Driver\u0026#34;] end subgraph Hardware[\u0026#34;Hardware\u0026#34;] NIC[\u0026#34;NIC\u0026#34;] end Application \u0026lt;--\u0026gt; SystemCall SystemCall \u0026lt;--\u0026gt; SocketLayer SocketLayer \u0026lt;--\u0026gt; TransportLayer TransportLayer \u0026lt;--\u0026gt; NetworkLayer NetworkLayer \u0026lt;--\u0026gt; NetworkDriver NetworkDriver \u0026lt;--\u0026gt; NIC %% 从系统调用直接连接到网络驱动器 SystemCall -.-\u0026gt; NetworkDriver style UserSpace fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px style KernelSpace fill:#fff3e0,stroke:#e67e22,stroke-width:2px style Hardware fill:#e8f8e8,stroke:#27ae60,stroke-width:2px ifconfig 获取信息：\n从网络协议栈：IP地址 tx/rx packets 从驱动程序获取：IOCTL(userspace app 与驱动程序通信) graph LR subgraph Application[\u0026#34;Application\u0026#34;] end subgraph UserSpace[\u0026#34;User Space\u0026#34;] Application end subgraph KernelSpace[\u0026#34;Kernel Space\u0026#34;] SystemCall[\u0026#34;System Call\u0026#34;] NetworkStack[\u0026#34;Network Stack\u0026#34;] eth0[\u0026#34;eth0\u0026#34;] wifi0[\u0026#34;wifi0\u0026#34;] EthernetDriver[\u0026#34;Ethernet Driver\u0026#34;] WirelessDriver[\u0026#34;Wireless Driver\u0026#34;] end subgraph Hardware[\u0026#34;Hardware\u0026#34;] EthernetController[\u0026#34;Ethernet\u0026lt;br\u0026gt;Controller\u0026#34;] WirelessController[\u0026#34;Wireless\u0026lt;br\u0026gt;Controller\u0026#34;] end Application --\u0026gt; SystemCall SystemCall \u0026lt;--\u0026gt; NetworkStack NetworkStack \u0026lt;--\u0026gt; eth0 NetworkStack \u0026lt;--\u0026gt; wifi0 eth0 \u0026lt;--\u0026gt; EthernetDriver wifi0 \u0026lt;--\u0026gt; WirelessDriver EthernetDriver \u0026lt;--\u0026gt; EthernetController WirelessDriver \u0026lt;--\u0026gt; WirelessController style UserSpace fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px style KernelSpace fill:#fff3e0,stroke:#e67e22,stroke-width:2px style Hardware fill:#e8f8e8,stroke:#27ae60,stroke-width:2px 从ifconfig的输出看不出网络接口是否是虚拟的，以太网或wifi；网络接口，底层有一个网络驱动程序这个驱动程序你不了解，它实际上是一个抽象层。所以不管是物理接回迹是软件接回从用戸的角度是不知道的；甚至网络协议栈也不知道。这就是分层架构的美妙之处。\ngraph LR subgraph Application[\u0026#34;Application\u0026#34;] end subgraph UserSpace[\u0026#34;User Space\u0026#34;] Application end subgraph KernelSpace[\u0026#34;Kernel Space\u0026#34;] SystemCall[\u0026#34;System Call\u0026#34;] SocketLayer[\u0026#34;Socket Layer\u0026#34;] TransportLayer[\u0026#34;Transport Layer\u0026#34;] subgraph NetworkLayer[\u0026#34;Network Layer\u0026#34;] RoutingTable[\u0026#34;Routing Table\u0026#34;] end NetworkDriver[\u0026#34;Network Driver\u0026#34;] end subgraph Hardware[\u0026#34;Hardware\u0026#34;] NIC[\u0026#34;NIC\u0026#34;] end Application --\u0026gt; SystemCall SystemCall \u0026lt;--\u0026gt; SocketLayer SocketLayer \u0026lt;--\u0026gt; TransportLayer TransportLayer \u0026lt;--\u0026gt; NetworkLayer NetworkLayer \u0026lt;--\u0026gt; NetworkDriver NetworkDriver \u0026lt;--\u0026gt; NIC style UserSpace fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px style KernelSpace fill:#fff3e0,stroke:#e67e22,stroke-width:2px style NetworkLayer fill:#fce4ec,stroke:#e74c3c,stroke-width:2px style Hardware fill:#e8f8e8,stroke:#27ae60,stroke-width:2px 在传输过程中，网络协议栈需要决定 IP 数据包的转发路径（即路由表）\n发送数据包是将会调用驱动程序\n1 2 3 4 5 6 7 8 9 # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.0.2.2 0.0.0.0 UG 100 0 0 enp0s3 10.0.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s3 10.0.2.2 0.0.0.0 255.255.255.255 UH 100 0 0 enp0s3 192.168.1.1 10.0.2.2 255.255.255.255 UGH 100 0 0 enp0s3 192.168.56.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s8 192.168.57.0 0.0.0.0 255.255.255.0 U 0 0 0 deth0 设备驱动程序 主要功能 graph LR subgraph UserSpace[\u0026#34;User Space\u0026#34;] Application[\u0026#34;Application\u0026#34;] end subgraph KernelSpace[\u0026#34;Kernel Space\u0026#34;] SystemCall[\u0026#34;System Call\u0026#34;] NetworkStack[\u0026#34;Network Stack\u0026#34;] eth0[\u0026#34;eth0\u0026#34;] NetworkDriver[\u0026#34;Network Driver\u0026#34;] TX[\u0026#34;TX\u0026#34;] RX[\u0026#34;RX\u0026#34;] Interrupt[\u0026#34;Interrupt\u0026#34;] end subgraph Hardware[\u0026#34;Hardware\u0026#34;] NetworkController[\u0026#34;Network Controller\u0026#34;] end Application --\u0026gt; SystemCall SystemCall \u0026lt;--\u0026gt; NetworkStack %% SystemCall -.-\u0026gt; NetworkDriver NetworkStack \u0026lt;--\u0026gt; eth0 eth0 \u0026lt;--\u0026gt; NetworkDriver NetworkDriver \u0026lt;--\u0026gt; TX NetworkDriver \u0026lt;--\u0026gt; RX NetworkController --\u0026gt; Interrupt Interrupt --\u0026gt; NetworkDriver TX \u0026lt;--\u0026gt; NetworkController RX \u0026lt;--\u0026gt; NetworkController SystemCall -.-\u0026gt; NetworkDriver style UserSpace fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px style KernelSpace fill:#fff3e0,stroke:#e67e22,stroke-width:2px style Hardware fill:#e8f8e8,stroke:#27ae60,stroke-width:2px style NetworkDriver fill:#e8f8e8,stroke:#97a960 设备驱动程序三个主要功能： 初始化硬件特定信息 初始化网络设备 和 对接网络协议栈\n因为每当你安装这个网络协议栈的时候，这个网络驱动程序可以开发成内核模块也就是说，在启动过程中这个驱动程序是不可用的,所以现在你正在加载网络驱动程序。所以这意味着你实际上是在创建这个网络接口，而且这些信息必须跟网络协议栈共享.所以，这是网络驱动程序要做的关键任务之一，另外另一个主要工作就是去初始化网络控制器。但除此之外还有三个常规的事件或常规任务。网络驱动程序需要处理这些一是向网络控制器发送数据包 二是从网络控制器那里接收网络数据包。然后把它送回网络协议栈，或者转发给网络协议栈。第三点，就是从网络控制器那里接收到一个中断通知。请注意，这个中断信号会因为各种不同的原因被触发产生。第一，也是最名最条最常见的事件，就是收到数据包。所以每当网络控制器收到任何数据包，网络控制器会生一个中断，然后这个中断会通过操作系统到达网络驱动，程序驱动程序会检查数据包。第二种情况是，当你每次拔掉它的时候，插上网线或者拔掉网线，也就是链路状态变成up或down，这时网络控制器也会产生一些事件。类似地，无论何时你通过TX发送数据包时，通常网络驱动程序会检查数据包是否发送成功，所以有一个非常常见的中断，它叫做TX完成中断。所以第一种情況就是RX中断，第二种是设备插拔状态相关的信息中断，第三种是常见的TX完成中断，也就是发送完成中断。除此之外，发送或接收过程中可能发生一些错误或者还有其他类型的错误。所以网络控制器也会将一些错误相关信息，通过中断机制传递给网络驱动程序。\n当我们说网络控制器在收到一个数据包的时候，会向网络驱动程序发送一个中断： 如果网络接收大量数据包，中断驱动I/O并非首选方式，轮询可能才是更好的。中断驱动I/O和轮询驱动I/O之间的切换是自动发生的吗这通常不是在运行时自动切換的，而是事先确定的。当然，这只是个一般性的说法。不能一概而论，这取決于具体硬件。这是CORS 。这个说法只是你应该遵循的原则，实际情况总是有变化，你需要自己判断一下在这个场景中轮询机制是不是更好的选择，或者中断是更好的选择。另外，处理这个中断也有各种不同的方式，所有人都可以完全避免使用下半部，中断服务例程也可以这样做。所以这种方式同样是可行的。也就是说，短时间内有几百个数据包涌上来，对于快速数据包，网络驱动收到中断 在网络服务路由中，它实际上立即禁用掉这个中断，然后再去处理这个数据包。 如果它用这种方式也就是收到一个数据包之后， 就把它禁用掉。禁用中断，处理数据包，再发送 给网络协议栈，然后重新启用中断，退出中断处理 ，所以你可以理解，对于一个高吞吐量的系统来说它总是会不断地收到中断。这样CPU就一直在处理网络驱动程序的工作；但 在我们的系统中，收到数据包是很常见的，但还有其他工作需要完成。所以它的作用就是，像你说的轮询这种方法是可以被使用的。另一个可以做的事情就是，每当网络驱动程序收到中断信号时，我们不是只读取一个数据包，而是检查网络控制器硬件队列中当前可用的所有数据包，所以它会一次性接收所有数据包，可能那里有几百个数据包。所以它会先检查一下 ，看看总共有多少个数据包， 它不再只从网络控制器向网络驱动复制一个数据包 ，所以通过这个，它实际上提高了性能表现。所以，在网络驱动里做性能调优，这一步非常关键。中断也达不到我们期望的性能，这可不是我们想要的结果。轮询是另一种方式，但数据包不频繁时，这种设计会很糟糕 ，要根据实际使用场景。现在的硬件已经非常强大了，我见过非常差的网络驱动程序中中断机制是唯一的方式如今硬件有更多能力了，网络控制器里可以支持更多的队列来工作，它的中断机制也更好了，它还能替网络协议栈做些工作，比如处理一些数据包，比如，你知道网络协议栈的每一层：它们都会计算一个校验和，每个数据包的校验和计算都需要一些时间，所以现在网络控制器有这种能力，可以检查和验证校验和。所以如果这些正作是由硬件来完成的，那么肯定比在协议栈里处理要快得多 ，如果它跟网络协议栈有关那就有很多机制可以用来提升性能，轮询就是其中之一。\n驱动实例 https://github.com/charles-7777/linux-netdevice-driver-test/tree/main\nalloc_netdev 网络协议栈和网络驱动程序之间进行通信的主要接口，就是网络接口。所以这个接口必须被创建出来并且要把它共享给网络协议栈。第一步就是创建那个network interface。 Linux内核维护一个叫net_device的结构体我们习惯叫它net device。这个net device结构体是用来对应每个网络接口，都要单独设置。网络接口可以是物理的 ，也可以是虚拟的。\n内核使用 struct net_device 结构来管理每一个网络设备。与其他结构体类似，它必须通过 alloc_netdev() 函数动态创建。此处，struct dummy_priv 表示驱动“私有数据区”的大小；对于网络设备而言，该数据区是与 net_device 结构体一并分配的。\n1 2 3 struct net_device *dev_dummy dev_dummy =alloc_netdev(sizeof(struct dummy_priv)， \\ \u0026#34;deth%d\u0026#34;，NET_NAME_ENUM，dummy_Setup) 在alloc_netdev API中，我们需要提供四个参数。第一个是私有数据的大小 ;第二个是网络接口名;第三个是名称分配类型 ;影响 %d 的替换逻辑和name的最终赋值;第四个是一个函数,所以它是一个函数指针，由我代码中的dummy_setup函数来填充。\n第一个参数是私有数据每个设备都提供了一种机制用来与网络协议栈共享一些私有数据。某些情况下需要这么做，但这是可选的，你也可以不用。若使用，需要在网络设备结构体本身里面，给它分配一些内存空间。所以这就是为什么只是传递了大小在alloc_netdev里传入我的私有数据结构的大小。这样，每当我的网络设备被创建，这块额外内存也会被创建。驱动程序可以在这块内存里保存当前这个接口特有的硬件信息，比如：该端口的物理基地址（Base Address）、该端口对应的中断号（IRQ）、该端口的 MAC 地址、该端口的 DMA 描述符队列等 ；当网络栈调用驱动程序的函数时，会把当前的 net_device 指针传进来。驱动程序通过 netdev_priv(dev) 获取私有数据，就能知道当前正在操作的是哪一个具体的物理端口，从而执行正确的底层硬件操作。然后函数处理程序是dummy_setup，当初始化完成，也就是与内核的信息共享完成后这个函数就会被调用，然后网络协议栈会调用这个函数来开始与驱动程序通信。这通常是网络协议栈调用的第一个函数。\nregister_netdevice 1 2 dev_dummy-\u0026gt;rtnl_link_ops= \u0026amp;dummy_link_ops err = register_netdevice(dev_dunmy) 驱动程序需要使用 register_netdevice() 函数将其设备注册到内核中。为此，驱动程序需要传入之前通过 alloc_netdev() 创建的 net_device 实例。一旦调用了 register_netdevice()，网络协议栈就可能随时调用该驱动程序来操作此设备。因此，在所有初始化工作彻底完成之前，驱动程序不应注册该设备。所以，一旦我们用alloc_netdev创建了设备，就需要通知网络协议栈，或者我们常说这个设备需要把这个特定的网络设备注册到网络协议栈里。通过调用另一个API：register_netdev。这里我特意放了一个设备，就在这个位置，因为要告诉你，网络设备可能拥有多个网络接口 ，如果你对路由器比较熟悉的话 ，你可能会看到两个端口。一个是WAN口，它是用来连接互联网的。另一个是给LAN的，你的LAN设备可以连上去。 但底层设备驱动可能相同，多数情況下，在我们现在的路由器设备里实际上只运行了一个驱动程序的实例， 但驱动程序可能有多个网络接口，即驱动程序会多次alloc_netdev。同样，如果你用Wi-Fi，那么现在至少有两个接口：一个用于2.4GHz，另一个是5GHZ。但底层的设备驱动程序实际上只运行了一个，只有一个。一个Wi-Fi设备实例在运行，负责处理两个接口。\n所以，一旦我们用alloc_netdev创建了设备，就需要通知网络协议栈，或者我们常说这个设备需要把这个特定的网络设备注册到网络协议栈里。通过调用另一个API，register_netdev。这里我特意放了一个设备，就在这个位置，因为要告诉你，网络设备可能拥有多个网络接口 如果你对路由器比较熟悉的话，你可能会看到两个端口。一个是WAN口，它是用来连接互联网的。另一个是给LAN的，你的LAN设备可以连上去。 但底层设备驱动可能相同多数情況下，在我们现在的路由器设备里实际上只运行了一个驱动程序的实例，所以驱动程序可能有多个网络接口 。同样，如果你用Wi-Fi，那么现在至少有两个接口：一个用于2.4GHz，另一个是5GHZ。但底层的设备驱动程序实际上只运行了一个，只有一个。一个Wi-Fi设备实例在运行，负责处理两个接口。所以一个网络驱动程序可以有多个网络接口。但反过来不成立，一个网络接口是不能同时对应多个网络设备的。所以注册机制是这样的：我们调用一个从alloc_netdev获取的虚拟接口， 从alloc_netdev获取信息并将该信息与注册的网络设备结构体共享。在此之前，我们需要先设置好一组操作函数指针，这样网络协议栈就会调用这些函数。在特定场景，比如打开、关闭或向网络接口发送数据包。针对这些操作网络设备中的某些函数会被调用到对应的function handler中。 所以这一组处理函数实际上就是在这里Initializes的dummy_link_ops 。开发网络设备驱动程序的时候，有一点你必须特别注意：一旦你调用了这个register_netdev ，网络协议栈就会开始与驱动程序通信。所以务必确保在调用register_netdev之前，硬件相关的初始化已经完成，否则你就会遇到问题。\n通常在设备注册之前，并不会完成完整的设备初始化，特别是针对网络协议栈的初始化尚未进行。下面的代码展示了 net_device 结构体一个非常常规的初始化过程：它主要就是将指针赋值给我们驱动程序中的各个操作函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ether_setup(dev); /* Initialize the device structure. */ dev-\u0026gt;netdev_ops = \u0026amp;dummy_netdev_ops; dev-\u0026gt;ethtool_ops = \u0026amp;dummy_ethtool_ops; /* Fill in device structure with ethernet-generic values. */ dev-\u0026gt;flags |= IFF_NOARP; dev-\u0026gt;flags \u0026amp;= ~IFF_MULTICAST; dev-\u0026gt;priv_flags |= IFF_LIVE_ADDR_CHANGE | IFF_NO_QUEUE; dev-\u0026gt;features |= NETIF_F_SG | NETIF_F_FRAGLIST; dev-\u0026gt;features |= NETIF_F_ALL_TSO; dev-\u0026gt;features |= NETIF_F_HW_CSUM | NETIF_F_HIGHDMA | NETIF_F_LLTX; dev-\u0026gt;features |= NETIF_F_GSO_ENCAP_ALL; dev-\u0026gt;hw_features |= dev-\u0026gt;features; dev-\u0026gt;hw_enc_features |= dev-\u0026gt;features; eth_hw_addr_random(dev); dev-\u0026gt;min_mtu = 0; dev-\u0026gt;max_mtu = 0; register_netdev时，内核或网络栈会调用你的虚拟设置或你提供的任何函数 这里有一个叫法：去填充网络设备操作处理函数。分配一个MAC地址以及其他相关设置然后你的驱动程序就完全准备好了，可以开始发送和接收数据包，并与网络协议栈通信了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 int32_t dummy_eth_rx(struct sk_buff *skb) { struct iphdr *iph = NULL; struct icmphdr *icmph = NULL; int32_t addr = 0; char eth_addr[ETH_ALEN]; if (skb == NULL) { printk(KERN_ERR \u0026#34;DETH: skb is null\\n\u0026#34;); return -EINVAL; } /* Mangle the packet to send ICMP/ping reply */ iph = ip_hdr(skb); if (iph \u0026amp;\u0026amp; iph-\u0026gt;protocol == IPPROTO_ICMP) { __wsum csum = 0; icmph = icmp_hdr(skb); if (icmph == NULL) { printk(KERN_ERR \u0026#34;DETH: no such ICMP header\\n\u0026#34;); goto free; } } print_hex_dump(KERN_ERR, \u0026#34;DETH B: \u0026#34;, 0, 16, 1, skb-\u0026gt;data, skb-\u0026gt;len, 0); /* Alter MAC addresses */ memcpy(eth_addr, skb-\u0026gt;data, ETH_ALEN); memmove(skb-\u0026gt;data, skb-\u0026gt;data + ETH_ALEN, ETH_ALEN); memcpy(skb-\u0026gt;data + ETH_ALEN, eth_addr, ETH_ALEN); /* Alter IP addresses */ addr = iph-\u0026gt;daddr; iph-\u0026gt;daddr = iph-\u0026gt;saddr; iph-\u0026gt;saddr = addr; /* ICMP echo reply */ icmph-\u0026gt;type = ICMP_ECHO_REPLY; /* FIXME: Recast IP头部协议或者类似的东西，都包含在 */ /* 以下为推测的后续代码（需要补全） */ /* 重新计算 IP 校验和 */ iph-\u0026gt;check = 0; iph-\u0026gt;check = ip_fast_csum((unsigned char *)iph, iph-\u0026gt;ihl); /* 重新计算 ICMP 校验和（因为 type 改变了） */ icmph-\u0026gt;checksum = 0; icmph-\u0026gt;checksum = csum_fold(skb_checksum(skb, skb_network_header_len(skb), skb-\u0026gt;len - skb_network_header_len(skb), 0)); print_hex_dump(KERN_ERR, \u0026#34;DETH A: \u0026#34;, 0, 16, 1, skb-\u0026gt;data, skb-\u0026gt;len, 0); /* 反转方向：将 skb 重新发送回协议栈 */ skb-\u0026gt;pkt_type = PACKET_HOST; skb-\u0026gt;protocol = eth_type_trans(skb, skb-\u0026gt;dev); netif_rx(skb); return 0; free: dev_kfree_skb(skb); return -EINVAL; } static int dummy_open(struct net_device *dev) { printk(KERN_ERR \u0026#34;DETH: %s() - called\\n\u0026#34;, __func__); netif_start_queue(dev); return 0; } static int dummy_close(struct net_device *dev) { printk(KERN_ERR \u0026#34;DETH: %s() - called\\n\u0026#34;, __func__); netif_stop_queue(dev); return 0; } static netdev_tx_t dummy_xmit(struct sk_buff *skb, struct net_device *dev) { struct pcp_u_dstats *dstats = this_cpu_ptr(dev-\u0026gt;dstats); printk(KERN_ERR \u0026#34;DETH: %s() - called\\n\u0026#34;, __func__); u64_stats_update_begin(\u0026amp;dstats-\u0026gt;syncp); dstats-\u0026gt;tx_packets++; dstats-\u0026gt;tx_bytes += skb-\u0026gt;len; u64_stats_update_end(\u0026amp;dstats-\u0026gt;syncp); skb_tx_timestamp(skb); /* Call HW xmit function */ if (lt_hw_tx(skb)) dev_kfree_skb(skb); return NETDEV_TX_OK; } static const struct net_device_ops dummy_netdev_ops = { .ndo_init = dummy_dev_init, .ndo_unit = dummy_dev_unit, .ndo_start_xmit = dummy_xmit, .ndo_validate_addr = eth_validate_addr, .ndo_set_mac_address = eth_mac_addr, .ndo_change_carrier = dummy_change_carrier, .ndo_open = dummy_open, .ndo_stop = dummy_close, }; static struct rtnl_link_ops dummy_link_ops __read_mostly = { .kind = DRV_NAME, .priv_size = sizeof(struct dummy_priv), .setup = dummy_setup, }; static int __init dummy_init_one(void) { int err; struct net_device *dev_dummy; dev_dummy = alloc_netdev(sizeof(struct dummy_priv), \u0026#34;deth%d\u0026#34;, NET_NAME_ENUM, dummy_setup); if (!dev_dummy) return -ENOMEM; dev_dummy-\u0026gt;rtnl_link_ops = \u0026amp;dummy_link_ops; err = register_netdevice(dev_dummy); if (err \u0026lt; 0) goto err; /* True : Register, False : Deregister */ if ((err = lt_request_irq(true, \u0026amp;dummy_eth_rx))) { return err; } return 0; err: free_netdev(dev_dummy); return err; } static int __init dummy_init_module(void) { int err = 0; printk(KERN_INFO \u0026#34;Dummy eth module init\\n\u0026#34;); rtnl_lock(); err = __rtnl_link_register(\u0026amp;dummy_link_ops); if (err \u0026lt; 0) goto out; err = dummy_init_one(); if (err \u0026lt; 0) __rtnl_link_unregister(\u0026amp;dummy_link_ops); out: rtnl_unlock(); return err; } static void __exit dummy_cleanup_module(void) { printk(KERN_INFO \u0026#34;Dummy eth module exit\\n\u0026#34;); __rtnl_link_unregister(\u0026amp;dummy_link_ops); } module_init(dummy_init_module); module_exit(dummy_cleanup_module); MODULE_LICENSE(\u0026#34;GPL\u0026#34;); MODULE_ALIAS_RTNL_LINK(DRV_NAME); MODULE_VERSION(DRV_VERSION); MODULE_AUTHOR(DRV_AUTHOR); //dummy-eth.c .ndo_start_xmit 这个函数在网络协议栈发送数据包时被调用。那么网络协议栈是怎么做的呢?它实际上会先检查IP层然后查一下路由表”再决定下一步怎么走。网络接回也就是我们用来发送数据包的那个东西，然后比如说接口是deth0 ,然后它会检查网络设备链表，也就是netdevicelinklist。找到哪个网络设备实例，它的接口名称是deth0 ,然后调用对应的ndo_start_xmit函数,也就是说这个dummy_xmit会被执行, 所以如果你在开发网络接口驱动程序，不管是不管是无线还是Wi-Fi、无线还是以太网、物理还是虚拟，这些信息它必须向网络协议栈进行注册才行,否则网络协议栈无法给你发数据包也无法和你的网络设备驱动程序通信。\nerr =register_netdevice(devdummy)如果这一步能够正确地完成，那么我们实际上就是在向网络协议栈注册我的设备，也就是在注册设备本身；dev_dummy-\u0026gt;rtnl_link_ops = \u0026amp;dummy_link_ops;这是一个链路相关操作,do the job: up down up 。注册dummy_setup时，我们分配所有硬件相关的东西之后，我们调用register_netdev。那注册netdev之后，路由表就会更新吗 ?注册之后，因为这个时候这个设备目前还没有注册到系统里面。网络协议栈或net_device结构体这个人的信息完全不知道，因为只有个IP地址而且register_netdev也不会填充路由表。安装驱动程序时，当时没有IP地址。对，没错。所以除非你手动分配一个IP地址否则路由表里的条自是不会被填充进去的。所以那个路由表并不在注册的网络协议栈里，对吧？路由表在IP层的网络协议栈里，对吧? 在这个register_netdevice API里，你没分配任何IP地址你只是往设备链里添加了一个网络设备实例。就是说，通过ifconfig你能看到这个网络接口，但这个接回目前还没有分配IP地址 也就是说，路由表里不会有对应的表项。当你用ifconfig分配地址时 用ifconfig或者其他机制给网络接口分配IP地址后，只有到那个时候，路由表里才会添加一条记录因为路由表主要就是用来处理IP地址的它建立IP地址、掩码和接口的映射 把IP地址和掩码对应到接口上。除非接口有IP地址，否则不会有记录。而这实际上跟设备驱动程序本身没有关系。驱动程序会接收任何数据包然后它就会直接把这些数据传给网络层然后它会检查，我的源IP地址是什么。目的IP地址，以及所有相关信息根据路由表、防火墙设置等情况决定丢弃或正确处理数据包 ，但网络设备，它绝不会根据IP地址来丢弃任何数据包。这只是一个generic的说法。为什么?因为在性能优化中，有些情况会这样做。在某些情况下，它比中断机制更有效。我们需要将从以太网接收到的数据包发送出去。并将数据包传递给Wi-Fi好的，通常的行为是数据包会被以太网驱动程序接收。它会将数据包发送到网络协议栈。网络协议栈会根据路由表来决定怎么走然后将其发送回Wi-Fi传输函数.Wi-Fi驱动程序将发送数据包但这个不是必要的这需要一些时间，因为网络协议栈的IP层会进行完整性检查。检查这个数据包是否正确看看有些地方对不对，还有那些乱七八糟的。所以它会检查IP头部，某些情况下我们可能会知道这个数据包是正确的。我们不需要可能快速发送10或20个数据包在Ethernet和WiFi 之间对IP地址每个数据包做完整性检查，是个好选择。 所以我们可以，在这种情况下在驱动程序本身中检查IP字段 ，在驱动程序本身里面。所以这算是一种hacking ，这并没有违反网络协议栈的架构设计理念。 但有时候，为了提升性能，你可能确实需要这么做。也就是说，从Ethernet驱动拿到数据包后，检查源IP等信息。这里有一个条目，因为这是合法的,这个条目是给老朋友的。所以我们可以信任他。因此不用把数据包发到网络协议栈，直接调用W-Fi驱动。它们之间有一些修补逻辑，而且还在不断更新。他们可以调用这个，但通常不会在驱动里这么做。对于ifconfig，会调用一个ioctl传入特定的唯一编号和网络接口名称。那么这些信息就会通过网络套接字。发送到网络协议栈那里去。网络协议栈现在会获得这些信息，用户应用程序正尝试将IP地址设置为192.168.88.23 到veth0 然后它会立即相应地更新net_device结构体司时还会在路由表中添加一条条目。\nDMA 分内部DMA控製器，外部DMA控製器；InternalDMA指的是以太网控製器有专用的DMA引擎，这个只服务于特定的外设，比如以太网、Wi-Fi或者其他类似的设备，externalDMA控制器像共享库，大家都能用。当然如果你有一个内部DMA控制器，那么性能可能会更好。\n软件模拟硬件层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 static void dummy_hw_send_irq(void) { struct sk_buff *skb = skb_dequeue(\u0026amp;skb_tx_q); if (skb == NULL) { printk(KERN_ERR \u0026#34;DHW: unable to dequeue TX skb\\n\u0026#34;); return; } printk(KERN_ERR \u0026#34;DHW: sending RX interrupt for skb=%p\\n\u0026#34;, skb); if (dummy_drv_rx == NULL) { printk(KERN_ERR \u0026#34;DHW: ASL interrupt not requested freeing\\n\u0026#34;); dev_kfree_skb(skb); } else { dummy_drv_rx(skb); } } static int dummy_hw_thread(void *arg) { printk(KERN_INFO \u0026#34;DHW thread started: %p ...\\n\u0026#34;, current); while (!kthread_should_stop()) { set_current_state(TASK_INTERRUPTIBLE); /* Going to sleep for 10ms to simulate hardware processing */ msleep(10); /* Do TX (send) here */ if (!skb_queue_empty(\u0026amp;skb_tx_q)) { dummy_hw_send_irq(); } } printk(KERN_INFO \u0026#34;DHW thread closed\\n\u0026#34;); return 0; } int lt_hw_tx(struct sk_buff *skb) { printk(KERN_ERR \u0026#34;DHW: adding skb=%p into HW Q\\n\u0026#34;, skb); /* Insert into tail and pick from head */ skb_queue_tail(\u0026amp;skb_tx_q, skb); return 0; } EXPORT_SYMBOL_GPL(lt_hw_tx); int32_t lt_request_irq(bool mode, int32_t (*rx_fn)(struct sk_buff *skb)) { if ((mode == true) \u0026amp;\u0026amp; (dummy_drv_rx == NULL)) { printk(KERN_ERR \u0026#34;DHW: ASL register IRQ for RX\\n\u0026#34;); dummy_drv_rx = rx_fn; } else if ((mode == false) \u0026amp;\u0026amp; (dummy_drv_rx != NULL)) { printk(KERN_ERR \u0026#34;DHW: ASL deregister IRQ for RX\\n\u0026#34;); /* To cleanup the TX Q */ wake_up_process(hw_thread_id); /* XXX: Thread may call this - do not assign NULL dummy_drv_rx = NULL; */ } return 0; } EXPORT_SYMBOL_GPL(lt_request_irq); //dummy-hw.c 本文主要翻译自 B站收录的视频课程 手把手拆解Linux网络内核：驱动、Netlink、Netfilter与协议栈全流程 ;是国外Linux Karnataka Education网站上的Linux课程\n经典教程《Linux设备驱动程序》 doc/Linux设备驱动程序(中文版第三版完美编辑带二级书签).pdf\nhuqianshan/linux-drivers-study: 关于Linux设备驱动程序开发与设计的学习仓库\nMaster Linux Kernel \u0026amp; Linux Device Driver - Linux Kernel Foundation\nLinux网络设备驱动 _驱动模型\nLinux Kernel Networking - Implementation and Theory.pdf\nLinux标准网络设备驱动详解：从架构到达成\n","date":"2026-07-07T00:07:47+08:00","permalink":"https://charles-7777.github.io/p/%E7%BD%91%E7%BB%9C%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86/","title":"网络设备驱动内部原理"},{"content":" Wi-Fi Alliance Technical Note Wi-Fi EasyMesh Deployment Guidelines Easymesh深入浅出-全局篇\nEasyMesh\nhttps://github.com/gitHuangweize/Easymesh_Specifications/tree/main\n","date":"2026-07-03T10:51:44+08:00","permalink":"https://charles-7777.github.io/p/wifi-easymesh/","title":"WiFi EasyMesh"},{"content":"三角函数系 正交函数系 正交向量\n$$ \\mathbf{a} \\cdot \\mathbf{b} = \\sum_{i=1}^{n} a_i b_i = 0 $$ 我们知道对于一个向量空间，其中任意向量可由一组 线性无关的向量线性表出（极大无关组，向量空间的一组基）,对于一组基${\\mathbf{v}_1, \\mathbf{v}_2, \\dots, \\mathbf{v}_n}$ ，任一向量$\\mathbf{x}$,有\n$$ \\mathbf{x} = \\sum_{i=1}^{n} k_i \\mathbf{v_i} $$正交基就是要求一组基中的向量，两两正交，这样可以更好的求出系数，$ \\mathbf{x} \\cdot \\mathbf{v_m} = \\sum_{i=1}^{n} k_i \\mathbf{v_i} \\cdot \\mathbf{v_m} = k_m \\mathbf{v_m} \\mathbf{v_m} $\n正交函数 深入理解正交函数 - 知乎\n我们可以把函数看成一个无穷维向量\n$$ \\begin{equation*} f(x) = \\lim_{\\Delta x \\to 0} \\begin{pmatrix} f(a), \u0026 f(a+\\Delta x), \u0026 f(a+2\\Delta x), \u0026 \\ldots, \u0026 f(a+(M-1)\\Delta x) \\end{pmatrix} \\qquad x \\in [a,b] \\end{equation*} $$无穷维空间（如函数空间）中，我们可以类似地把一个函数视为在某种基底上的线性组合。假设我们考虑一个函数 ( f )，例如定义在某个区间上的实值连续函数。我们可以将函数表示为某种基（例如正交基）上的线性组合：\n$$ \\mathbf{f(x)} = \\sum_{i=1}^\\infty c_i \\mathbf{v(x)} $$那么类似$\\mathbf{a} \\cdot \\mathbf{b} = \\sum_{i=1}^{n} a_i b_i = 0$两个函数正交:\n$$ \\langle f, g \\rangle = \\int_{a}^{b} f(x)g(x)dx =0 $$$$ \\langle f, v_m \\rangle = \\int_{a}^{b} f(x)v_m(x)dx = \\int_{a}^{b} \\sum_{i=1}^\\infty c_i v_i(x) v_m(x)dx = \\int_{a}^{b} c_m v_m(x) v_m(x)dx = c_m \\langle v_m, v_m \\rangle $$三角函数系 对于函数集合\n$$ C = \\left\\{ 1, \\sin x, \\cos x, \\sin 2x, \\cos 2x, \\ldots, \\sin nx, \\cos nx \\mid x \\in (-\\pi, \\pi) \\right\\} $$称之为三角函数系\n正交性\n对于$\\sin nx, \\cos nx$,有\n$$ \\int_{-\\pi}^{\\pi} \\sin nx \\sin mx dx = 0, \\qquad \\int_{-\\pi}^{\\pi} \\cos nx \\, dx = 0 $$由$\\sin(a + b) = \\sin a \\cos b + \\cos a \\sin b$ $和\\cos(a + b) = \\cos a \\cos b - \\sin a \\sin b$\n$$ \\int_{-\\pi}^{\\pi} \\sin nx \\sin mx dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2}[\\cos(m-n)x - \\cos(m+n)x] dx = 0 $$$$ \\int_{-\\pi}^{\\pi} \\cos nx \\cos mx dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2}[\\cos(m-n)x + \\cos(m+n)x] dx = 0 $$$$ \\int_{-\\pi}^{\\pi} \\sin nx \\cos mx dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2}[\\sin(n+m)x - \\sin(n-m)x] dx = 0 $$ 特别地\n$$ \\int_{-\\pi}^{\\pi} \\sin nx \\sin nx dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2}[\\cos(n-n)x - \\cos(n+n)x] dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2} (1 - \\cos 2nx) dx = \\pi $$$$ \\int_{-\\pi}^{\\pi} \\cos nx \\cos nx dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2}[\\cos(m-n)x + \\cos(n+n)x] dx = \\int_{-\\pi}^{\\pi} \\frac{1}{2} (1 + \\cos 2nx) dx = \\pi $$ 傅里叶级数 周期为$2\\pi$的傅里叶级数 对于一个以 $2\\pi$ 为周期的函数 $f(x) = f(x + 2\\pi)$，可以将其展开为傅里叶级数：\n$$ f(x) = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} (a_n \\cos nx + b_n \\sin nx) $$根据三角函数的正交性：\n对于 $1 \\cdot f(x)$，仅 $\\frac{a_0}{2}$ 项的积分结果不为 0：\n$$ \\int_{-\\pi}^{\\pi} f(x) \\, dx = \\frac{1}{2} a_0 \\int_{-\\pi}^{\\pi} dx = \\pi a_0 $$对于 $\\cos nx \\cdot f(x)$，仅 $a_n \\cos nx$ 项的积分结果不为 0：\n$$ \\int_{-\\pi}^{\\pi} \\cos nx \\cdot f(x) \\, dx = a_n \\int_{-\\pi}^{\\pi} \\cos^2 nx \\, dx = \\pi a_n $$对于 $\\sin nx \\cdot f(x)$，仅 $b_n \\sin nx$ 项的积分结果不为 0：\n$$ \\int_{-\\pi}^{\\pi} \\sin nx \\cdot f(x) \\, dx = b_n \\int_{-\\pi}^{\\pi} \\sin^2 nx \\, dx = \\pi b_n $$因此傅里叶级数中的系数满足：\n$$ a_0 = \\frac{1}{\\pi} \\int_{-\\pi}^{\\pi} f(x) \\, dx $$$$ a_n = \\frac{1}{\\pi} \\int_{-\\pi}^{\\pi} \\cos nx \\cdot f(x) \\, dx $$$$ b_n = \\frac{1}{\\pi} \\int_{-\\pi}^{\\pi} \\sin nx \\cdot f(x) \\, dx $$周期为 $2L$ 的傅里叶级数 对于一个以 $2L$ 为周期的函数 $f(t) = f(t + 2L)$，可以使用变量代换 $t = \\frac{L}{\\pi}x$ 将其转变为一个以 $2\\pi$ 为周期的函数：\n$$ g(x) = f(t) = f( \\frac{L}{\\pi}x ) \\quad \\Rightarrow \\quad g(x + 2\\pi) = f\\left(\\frac{L}{\\pi}(x + 2\\pi)\\right) $$因此将 $x = \\frac{\\pi}{L}t $ 代入 $g(x)$ 的傅里叶级数中，即可得到$f(t)$ 的傅里叶级数。\n定义 $\\omega_0 = \\frac{\\pi}{L}$，三角函数换元后有：\n$$ \\sin nx = \\sin n\\omega_0 x, \\qquad \\cos nx = \\cos n\\omega_0 x $$积分换元后有：\n$$ \\int_{-\\pi}^{\\pi} g(x) \\, dx = \\frac{\\pi}{L} \\int_{-L}^{L} f(t) \\, dt $$因此对于周期为 $2L$ 的傅里叶级数有：\n$$ \\begin{aligned} f(t) \u0026= \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\left[ a_n \\cos(n\\omega_0 t) + b_n \\sin(n\\omega_0 t) \\right] \\\\[6pt] a_0 \u0026= \\frac{1}{L} \\int_{-L}^{L} f(t) \\, dt \\\\[6pt] a_n \u0026= \\frac{1}{L} \\int_{-L}^{L} \\cos(n\\omega_0 t) \\cdot f(t) \\, dt \\\\[6pt] b_n \u0026= \\frac{1}{L} \\int_{-L}^{L} \\sin(n\\omega_0 t) \\cdot f(t) \\, dt \\end{aligned} $$周期信号的傅里叶级数（工程常用形式） 在工程中，信号为从 0 开始、以 T 为周期的函数，因此定义变换（其中 $\\omega_0$ 为基频）：\n$$ T = 2L, \\qquad \\omega_0 = \\frac{\\pi}{L} = \\frac{2\\pi}{T} $$易得对于周期为 T 的函数：\n$$ \\int_{-L}^{L} f(t) \\, dt = \\int_{0}^{2L} f(t) \\, dt = \\int_{0}^{T} f(t) \\, dt $$由此可以得到工程中常用的傅里叶级数形式：\n$$ \\begin{aligned} f(t) \u0026= \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\left[ a_n \\cos(n\\omega_0 t) + b_n \\sin(n\\omega_0 t) \\right] \\\\[6pt] a_0 \u0026= \\frac{2}{T} \\int_{0}^{T} f(t) \\, dt \\\\[6pt] a_n \u0026= \\frac{2}{T} \\int_{0}^{T} \\cos(n\\omega_0 t) \\cdot f(t) \\, dt \\\\[6pt] b_n \u0026= \\frac{2}{T} \\int_{0}^{T} \\sin(n\\omega_0 t) \\cdot f(t) \\, dt \\end{aligned} $$傅里叶级数的复数形式 傅里叶级数的复数形式 根据欧拉公式有：\n$$ e^{i \\theta} = \\cos\\theta + i \\sin\\theta $$据此以及三角函数的奇偶性，可将三角函数表示为复数形式：\n$$ \\sin\\theta = \\frac{1}{2i}(e^{i\\theta} - e^{-i\\theta}) = -\\frac{i}{2}(e^{i\\theta} - e^{-i\\theta}) $$$$ \\cos\\theta = \\frac{1}{2}(e^{i\\theta} + e^{-i\\theta}) $$复数形式的展开将复数形式的三角函数代入傅里叶级数：\n$$ f(t) = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\left[ a_n \\cos(n\\omega_0 t) + b_n \\sin(n\\omega_0 t) \\right] $$$$ = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\left[ \\frac{a_n - i b_n}{2} e^{i n \\omega_0 t} + \\frac{a_n + i b_n}{2} e^{-i n \\omega_0 t} \\right] $$$$ = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\frac{a_n - i b_n}{2} e^{i n \\omega_0 t} + \\sum_{n=-\\infty}^{-1} \\frac{a_n + i b_n}{2} e^{i n \\omega_0 t} $$$$ = \\sum_{n=-\\infty}^{\\infty} C_n e^{i n \\omega_0 t} $$现推导系数 $C_n$： 当 $ n = 0 $ 时：\n$$ C_0 = \\frac{a_0}{2} = \\frac{1}{T} \\int_0^T f(t) \\, dt $$当 $ n \u0026gt; 0 $ 时：\n$$ \\begin{aligned} C_n \u0026= \\frac{a_n - i b_n}{2} \\\\ \u0026= \\frac{1}{T} \\int_0^T f(t) \\cdot \\left[ \\cos(n\\omega_0 t) - i \\sin(n\\omega_0 t) \\right] dt \\\\ \u0026= \\frac{1}{T} \\int_0^T f(t) \\cdot e^{-i n \\omega_0 t} \\, dt \\end{aligned} $$当 $ n \u0026lt; 0 $ 时：\n$$ \\begin{aligned} C_n \u0026= \\frac{a_{-n} + i b_{-n}}{2} \\\\ \u0026= \\frac{1}{T} \\int_0^T f(t) \\cdot \\left[ \\cos(-n\\omega_0 t) + i \\sin(-n\\omega_0 t) \\right] dt \\\\ \u0026= \\frac{1}{T} \\int_0^T f(t) \\cdot e^{-i n \\omega_0 t} \\, dt \\end{aligned} $$故：\n$$ C_n = \\frac{1}{T} \\int_0^T f(t) \\cdot e^{-i n \\omega_0 t} \\, dt \\qquad n \\in Z $$ 周期函数傅里叶级数的特点 根据傅里叶级数的复数表达：\n$$ f(t) = \\sum_{n=-\\infty}^{\\infty} C_n e^{i n \\omega_0 t} $$在复平面上，傅里叶级数的每一项中：\n$ e^{i n \\omega_0 t} $体现了一个以 $ n \\omega_0 $为角速度旋转的向量 $ |C_n| $ 确定了这个向量的幅值 $ \\angle C_n $ 确定了这个向量在 $ t = 0 $ 的相位角 对于复数 Cn=a+ib（其中 a 是实部，b 是虚部）：\n∣Cn∣：模长（幅值），$∣C_n∣=\\sqrt {a^2+b^2} $\n∠Cn：辐角（相位角），$∠C_n=arg(C_n)=arctan(\\frac{b}{a})$ (需根据象限调整)\n因此周期函数的傅里叶本质上是将周期函数分解为频率为基频的整数倍 nω0​ 的无数个周期性旋转 即周期函数的频域以 Δω=ω0 为间隔\n$$ f(t+T)=f(t) \\to e^{iw(t+T)}=e^{iwt} \\to e^{iwT}=1 \\to wT=2n\\pi \\to w=n \\cdot \\frac{2 \\pi}{T} \\\\ w=n w_0 \\to \\Delta ω=ω0 $$非周期函数的傅里叶变换 对于非周期函数 $f(t)$ 其周期 $T\\to\\infty$, 基频 $\\omega_0 = \\frac{2 \\pi}{T}\\to 0$ 因此函数的频率分布之间的间隔 $\\omega_0=\\Delta\\omega\\to 0$, 此时函数的频域连续分布根据以下变换\n$$ n \\omega_0 \\to \\omega \\qquad T= \\frac{2 \\pi}{\\omega _0} ;\\; \\frac{1}{T}= \\frac{\\Delta \\omega}{2 \\pi} \\to \\frac{\\omega}{2 \\pi} $$带入 $f(t)$ 的傅里叶级数\n$$ \\begin{aligned} f(t) \u0026= \\sum_{n=-\\infty}^{\\infty} C_n e^{in\\omega_0 t} \\\\ \u0026= \\frac{1}{T} \\sum_{n=-\\infty}^{\\infty} \\left( \\int_{-\\frac{T}{2}}^{\\frac{T}{2}} f(t) e^{-in\\omega_0 t} dt \\right) e^{in\\omega_0 t} \\\\ \u0026= \\frac{d\\omega}{2\\pi} \\int_{-\\infty}^{\\infty} \\left( \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt \\right) e^{i\\omega t} \\\\ \u0026= \\frac{1}{2\\pi} \\int_{-\\infty}^{\\infty} \\left( \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt \\right) e^{i\\omega t} d\\omega \\end{aligned} $$此时$ C_n $ 可以被表示成一个连续的函数$ F(\\omega) $，因此有\n$$ F(\\omega) = \\mathcal{F}[f(t)] = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt \\\\ f(t) = \\mathcal{F}^{-1}[F(\\omega)] = \\frac{1}{2\\pi} \\int_{-\\infty}^{\\infty} F(\\omega) e^{i\\omega t} d\\omega $$其中\n$ F(\\omega) = \\mathcal{F}[f(t)] $ 为傅里叶正变换\n$ f(t) = \\mathcal{F}^{-1}[F(\\omega)] $ 为傅里叶逆变换\n根据推导可得, 傅里叶变换得到的 $F(\\omega)$相当于$ C_n=F(\\omega)d \\omega$, 因此 $F(\\omega)$ 并不代表信号中\n率为 $\\omega$的成分, 而是在频率范围$ \\omega \\sim \\omega + d\\omega $内的密度\n因此 F(ω) 体现的是信号在 ω 的幅值密度而非幅值 (与傅里叶级数做区分)\n傅里叶变换 | Tonyddg\u0026rsquo;s Noteverse\n傅里叶变换和卷积 卷积\n$$ f_1(x) * f_2(x) = \\int_{-\\infty}^{\\infty} f_1(\\xi) f_2(x - \\xi) \\, d\\xi $$ 卷积的本质及物理意义(全面理解卷积)\n设两时域信号 $ f(t), g(t) $，对于卷积有：\n$$ f(t) * g(t) = \\int_{-\\infty}^{\\infty} f(\\tau) g(t - \\tau) \\, d\\tau $$那么其傅氏变换为\n$$ \\begin{aligned} \\mathcal{F}[f(t) * g(t)] \u0026= \\int_{-\\infty}^{\\infty} \\left[ \\int_{-\\infty}^{\\infty} f(\\tau) g(t - \\tau) \\, d\\tau \\right] e^{-j\\omega t} \\, dt \\\\ \u0026= \\int_{-\\infty}^{\\infty} f(\\tau) \\left[ \\int_{-\\infty}^{\\infty} g(t - \\tau) e^{-j\\omega t} \\, dt \\right] d\\tau \\\\ \\text{交换次序} \\\\ \u0026= \\int_{-\\infty}^{\\infty} f(\\tau) e^{-j\\omega \\tau} \\, d\\tau \\left[ \\int_{-\\infty}^{\\infty} g(t - \\tau) e^{-j\\omega (t - \\tau)} \\, d(t - \\tau) \\right] \\\\ \u0026= \\mathcal{F}[f(\\tau)] \\, \\mathcal{F}[g(t - \\tau)] \\\\ \u0026= F_1(\\omega) F_2(\\omega) \\end{aligned} $$","date":"2026-06-29T10:58:14+08:00","permalink":"https://charles-7777.github.io/p/%E5%82%85%E9%87%8C%E5%8F%B6%E7%BA%A7%E6%95%B0/","title":"傅里叶级数"},{"content":" cookie，session，token，jwt，oauth2 有什么区别？\nCookie：HTTP 的“记忆载体” 是什么？ Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据。它本质上是一种存储机制和传输机制，而不是认证协议本身。由于 HTTP 是无状态协议，Cookie 的出现就是为了让浏览器在后续请求中自动携带这段数据，从而让服务器“记住”用户。\n核心特点 自动携带：浏览器会根据域名规则，在每次请求时自动将 Cookie 放入 Request Header。 大小限制：通常限制在 4KB 左右。 安全性属性：可通过 HttpOnly（防 XSS 读取）、Secure（仅 HTTPS 传输）、SameSite（防 CSRF）等属性增强安全。 跨域限制：默认遵循同源策略，不支持跨域共享（除非特殊配置）。 工作原理 服务器通过 Set-Cookie 响应头发送 Cookie 到浏览器。 浏览器按域/路径/过期时间存储 Cookie，并在后续请求中通过 Cookie 请求头自动携带。 服务器解析请求中的 Cookie，根据内容执行个性化操作（如显示登录状态）。 一句话总结 Cookie 只是一个“信封”，里面装什么内容（Session ID 还是 Token），由开发者决定。\nSession：服务端的“状态记录本” 是什么？ Session 是一种服务端状态管理机制。当用户登录后，服务器会在内存、数据库或 Redis 中创建一条会话记录，包含用户信息。然后服务器将该记录的 ID（Session ID）通过 Cookie 发送给客户端。\n核心特点 状态存在服务端：用户数据存储在服务器，客户端仅持有 ID。 可控性：服务器可随时销毁 Session，强制用户下线。 分布式挑战：多服务器架构需共享 Session 存储（如 Redis）。 依赖传输载体：通常通过 Cookie 传递 Session ID，也可通过 URL 参数（不推荐）。 工作原理 用户提交登录凭证，服务器验证后创建 Session 并生成 Session ID。 服务器通过 Set-Cookie 响应头发送 Session ID 到浏览器。 浏览器在后续请求中通过 Cookie 请求头携带 Session ID。 服务器根据 Session ID 查找服务端存储的 Session 数据，验证用户身份。 一句话总结 Session 是“服务端记账，客户端只拿凭证”的经典模式。\nToken：无状态的“通行证” 是什么？ Token 是一个自包含的访问凭证，通常是一串加密字符串。与 Session 不同，Token 模式下服务端不需要保存会话状态。验证 Token 的有效性完全依靠算法（签名/加密），而非查库。\n核心特点 无状态：服务端无需存储 Token 信息，仅需验证其合法性。 轻量级：通常比 Session 更小，适合频繁传输。 灵活存储：可存在浏览器 Cookie、HTTP Header（推荐）、LocalStorage 或内存中。 有效期控制：通过设置过期时间限制 Token 生命周期。 工作原理 用户登录成功后，服务器生成 Token（如 JWT）并返回给客户端。 客户端存储 Token，并在后续请求中通过指定方式（如 Header）传递。 服务端接收请求后，直接验证 Token 的有效性（签名、过期时间等），无需查询数据库。 一句话总结 Token 把“查验身份”的计算压力从数据库查询转移到了 CPU 解密/验签上。\nJWT (JSON Web Token)：Token 的事实标准 是什么？ JWT 是 Token 的一种具体实现规范（RFC 7519）。它定义了 Token 的结构和签名方式，是目前最流行的 Token 格式。\n核心特点 三段式结构：Header（算法声明）、Payload（用户信息）、Signature（签名）。 自描述性：Payload 包含用户身份和权限等声明（claims）。 无状态：服务端无需存储 Token，仅需验证签名。 跨域友好：适合前后端分离、移动端和微服务架构。 工作原理 服务器用密钥对 Header 和 Payload 进行签名，生成 Signature。 将三个部分用 Base64 编码后用 . 连接，形成 JWT。 客户端存储 JWT，并在请求中通过 Authorization: Bearer \u0026lt;token\u0026gt; 头部传递。 服务端接收 JWT 后，验证签名是否有效，解析 Payload 获取用户信息。 注意事项 Payload 不加密：JWT 的 Payload 是 Base64 编码的明文，不要存储敏感数据。 无法主动撤销：一旦签发，除非设置极短有效期或引入黑名单机制，否则无法立即失效。 签名密钥保护：密钥泄露会导致 JWT 被伪造。 一句话总结 JWT 是一种标准化的、自描述的、可验证的 Token 格式。\nOAuth2：授权的“委托协议” 是什么？ OAuth2 既不是存储机制，也不是凭证格式，而是一个授权框架（RFC 6749）。它解决的核心问题是：如何让用户在不暴露密码的前提下，授权第三方应用访问自己的资源？\n核心特点 授权而非认证：OAuth2 负责授权第三方应用访问资源，不直接处理用户身份验证。 标准化流程：定义了明确的授权流程和角色分工。 多种授权模式：适应不同应用场景的需求。 令牌交换：通过授权码等中间凭证换取最终的访问令牌。 工作原理 以授权码模式为例：\n用户访问客户端应用，触发跳转到授权服务器（如 Google、Facebook）。 用户在授权服务器登录并授权客户端访问其资源。 授权服务器返回授权码（Authorization Code）给客户端。 客户端用授权码向授权服务器换取 Access Token 和 Refresh Token。 客户端使用 Access Token 访问资源服务器（如用户邮箱、照片）。 注意事项 需配合认证协议：如 OpenID Connect (OIDC) 才能实现完整认证。 密钥轮换：定期更新签名密钥以增强安全性。 令牌安全：Access Token 应通过 HTTPS 传输，避免泄露。 一句话总结 OAuth2 是“我不给你密码，但我允许你代表我做事”的安全授权标准。\n技术全景对比表 下表总结了五大技术的核心区别：\n维度 Cookie Session Token JWT OAuth2 本质 存储/传输机制 服务端状态管理 访问凭证 Token 的标准格式 授权框架 状态 客户端存储 服务端有状态 服务端无状态 服务端无状态 协议层面无状态 存储位置 浏览器 服务器内存/Redis 客户端(LocalStorage/Cookie) 同 Token 不涉及存储 大小限制 约4KB 无限制 无限制 无限制 无限制 跨域支持 ❌ 受限 ❌ 受限 ✅ 友好 ✅ 友好 ✅ 专为跨域设计 移动端适配 ❌ 差（依赖浏览器） ❌ 差 ✅ 好 ✅ 好 ✅ 好 可扩展性 - ❌ 需共享存储 ✅ 天然支持 ✅ 天然支持 ✅ 天然支持 能否主动失效 ✅ 删除即可 ✅ 删除服务端记录 ❌ 需额外机制（如黑名单） ❌ 需额外机制（如黑名单） ✅ Refresh Token 轮换 典型用途 传递 Session ID 或 JWT 传统 Web 登录 API 认证 现代 API 认证 第三方授权/SSO 安全性 依赖属性（HttpOnly, Secure） 依赖服务端安全 依赖签名算法 依赖签名算法 依赖流程实现 数据来源：\n场景化技术选型指南 传统服务端渲染 Web 应用 推荐方案：Cookie + Session\n原因：成熟稳定，服务端可控，适合内部系统或不需要对外暴露 API 的场景。 最佳实践： 使用 HttpOnly 和 Secure 属性保护 Cookie。 设置合理的 Session 过期时间。 在分布式环境中使用 Redis 等共享存储。 前后端分离应用 推荐方案：JWT + HTTP Header\n原因：无状态、易扩展、跨域友好，适合 API 驱动的现代应用。 最佳实践： 将 JWT 存储在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中，或通过 Header 传递。 设置短期有效期（如15分钟），配合 Refresh Token 实现自动刷新。 避免在 URL 或 LocalStorage 中存储 JWT。 移动端应用 推荐方案：JWT + 安全存储\n原因：轻量级、适合移动网络，且能实现离线访问。 最佳实践： 使用 Android Keystore 或 iOS Keychain 加密存储 Token。 通过 HTTPS 传输 Token，避免中间人攻击。 定期刷新 Token，减少泄露风险。 微服务架构 推荐方案：JWT + OAuth2\n原因：无状态设计适合分布式系统，JWT 支持细粒度权限控制。 最佳实践： 使用 OAuth2 的 ClientCredentials 模式实现服务间通信。 在 JWT 中包含微服务所需的权限声明（如 read:users, write:products）。 避免完全依赖网关验证，各微服务应实施细粒度授权控制。 第三方登录 推荐方案：OAuth2 + JWT\n原因：OAuth2 提供安全授权流程，JWT 作为 Access Token 实现无状态验证。 最佳实践： 使用 OAuth2 的 Authorization Code 模式（推荐）或 PKCE（移动端）。 授权服务器可返回 JWT 作为 Access Token，资源服务器验证签名即可。 配合 OpenID Connect (OIDC) 实现完整用户认证。 企业内网 SSO 推荐方案：SSO + JWT\n原因：SSO 提供单点登录体验，JWT 实现跨系统无状态验证。 最佳实践： 统一认证中心（如 Keycloak）生成 JWT 并通过 Cookie 或 Header 传递。 各子系统验证 JWT 签名，无需重复登录。 可结合 OAuth2 实现跨公司授权。 常见误区澄清 误区 1：\u0026ldquo;JWT 比 Session 更安全\u0026rdquo; 真相：JWT 和 Session 的安全性取决于实现方式。Session 可随时在服务端销毁，而 JWT 一旦签发，在过期前无法主动撤销。因此，在敏感场景中，Session 可能更安全，但 JWT 更适合分布式系统。 误区 2：\u0026ldquo;用了 OAuth2 就不需要 JWT 了\u0026rdquo; 真相：OAuth2 是授权框架，它规范了授权流程，但未限定令牌格式。JWT 是一种流行的令牌格式，常被 OAuth2 使用作为 Access Token。两者是互补关系，而非替代关系。 误区 3：\u0026ldquo;Token 应该存在 LocalStorage 里\u0026rdquo; 真相：LocalStorage 易受 XSS 攻击，Token 应优先存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中，或通过 HTTP Header 传递。若必须使用 LocalStorage，应结合加密和短期有效期。 误区 4：\u0026ldquo;Cookie 就是 Session\u0026rdquo; 真相：Cookie 是传输机制，Session 是服务端状态管理。Cookie 可以存储任何数据（如 JWT），而 Session 需要服务端维护状态。两者是不同层次的概念，可以独立使用或结合使用。 误区 5：\u0026ldquo;OAuth2 可以替代 JWT\u0026rdquo; 真相：OAuth2 处理授权流程，JWT 是具体令牌格式。OAuth2 可以返回 JWT 作为 Access Token，两者结合使用更为常见。JWT 也可独立用于 API 认证。 技术关系图 这五者并非互斥关系，而是不同层次、不同职责的技术组件：\nCookie 负责\u0026quot;搬运\u0026quot; Session 负责\u0026quot;记账\u0026quot; Token 负责\u0026quot;证明\u0026quot; JWT 负责\u0026quot;标准化证明\u0026quot; OAuth2 负责\u0026quot;制定授权规则\u0026quot; 它们可以组合使用，例如：\n用户通过 OAuth2 授权第三方应用。 授权服务器返回 JWT 作为 Access Token。 客户端将 JWT 存储在 Cookie 中，并在请求中自动携带。 资源服务器验证 JWT 签名，无需服务端状态存储。 总结 理解这五个概念的区别，关键在于把握它们的本质定位和技术层次：\nCookie：HTTP 协议的无状态管理机制，负责数据存储和传输。 Session：服务端状态管理机制，依赖 Cookie 传递 ID。 Token：通用访问凭证，服务端无状态验证。 JWT：Token 的标准化实现，结构清晰且可验证。 OAuth2：授权框架，定义了令牌交换和资源访问的流程。 没有最好的技术，只有最适合场景的技术。选择时应考虑：\n安全性需求（如是否需要主动失效） 扩展性需求（如是否需要支持分布式系统） 跨域/移动端适配 实现复杂度与维护成本 ","date":"2026-06-10T19:11:14+08:00","permalink":"https://charles-7777.github.io/p/cookiesessiontokenjwt-%E4%B8%8E-oauth2-%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"Cookie、Session、Token、JWT 与 OAuth2 的区别"},{"content":"Reasonix Reasonix 是一款以 DeepSeek 为原生后端的终端编程 Agent。设计围绕 DeepSeek API 展开 —— Cache-First 循环、Flash 优先的成本控制、工具调用自动修复 —— 直接对接 api.deepseek.com，不需要协议转换层\nhttps://github.com/esengine/DeepSeek-Reasonix\nCodeGraph CodeGraph 是面向 AI 编码代理的预索引代码知识图谱工具，通过 MCP 协议与 Claude Code、Cursor、Codex CLI、OpenCode 及 Hermes Agent 深度集成。利用 tree-sitter 解析代码库，将符号关系、调用图和代码结构存储在本地 SQLite 数据库中，让 AI 代理能通过图谱查询直接定位代码，替代传统的 grep/glob/Read 文件扫描方式。 7 个真实开源项目基准测试验证，CodeGraph 平均可降低 35% 的 API 成本、减少 59% 的 Token 消耗、节省 49% 的时间并减少 70% 的工具调用次数，全程 100% 本地运行，无需外部 API 密钥。\nhttps://github.com/colbymchenry/codegraph\n","date":"2026-06-03T22:09:00+08:00","permalink":"https://charles-7777.github.io/p/reasonix--%E4%B8%93%E4%B8%BAdeepseek%E8%AE%BE%E8%AE%A1%E7%9A%84%E7%BB%88%E7%AB%AF%E7%BC%96%E7%A8%8Bagent/","title":"Reasonix--专为DeepSeek设计的终端编程Agent"},{"content":" DeepSeek V4 + Claude Code 实战\n用npm 安装（通过配置文件配置） 安装node.js 可参考 Node.js 安装 在powershell中用npm 安装claude code cli 1 npm install -g @anthropic-ai/claude-code 编辑或新增 Claude Code 配置文件 ~/.claude/settings.json，添加 env 字段，把后端地址、模型和 API Key 都写进去： 1 2 3 4 5 6 7 8 9 { \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;your_deepseek_api_key\u0026#34;, \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://api.deepseek.com/anthropic\u0026#34;, \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;deepseek-v4-flash\u0026#34;, \u0026#34;API_TIMEOUT_MS\u0026#34;: \u0026#34;3000000\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\u0026#34;: \u0026#34;1\u0026#34; } } 配置完成后即可在powershell terminal启动 Claude Code 1 2 PS D:\\\u0026gt; claude Welcome to Claude Code v2.1.159 进入 Claude Code 界面之后再次输入 /status 确认，model 为deepseek-v4-flash 即表示接入成功。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ❯ /status ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Settings Status Config Usage Stats Version: 2.1.159 Session name: /rename to add a name Session ID: 7ccb3534-d2ca-4808-8df3-90e899ed0277 cwd: xxxxxxxxxxxxxxxxxxxxxxxxxxxx Auth token: ANTHROPIC_AUTH_TOKEN Anthropic base URL: https://api.deepseek.com/anthropic Model: deepseek-v4-flash Setting sources: User settings CC Switch（可视化切换） https://www.ccswitch.io/zh/\n如果你想在 DeepSeek、Claude、MiniMax 等多个 Provider 之间灵活切换，推荐安装 CC Switch。这是一个专门管理 Claude Code 模型切换的小工具，支持一键横跳，还支持管理 Skills、MCP 和提示词。 启动 CC Switch，点击右上角 \u0026ldquo;+\u0026rdquo; ，选择自定义供应商，Base URL 填写 https://api.deepseek.com/anthropic，API Key 填写你的 DeepSeek API Key。 将模型名称改为 DeepSeek-V4-Pro（或 DeepSeek-V4-Flash），完成后点击右下角的\u0026quot;添加\u0026quot; 验证是否生效: 直接在命令行输入 claude 或者进入 Claude Code 界面之后再次输入 /status 确认，model 为 DeepSeek-V4-Pro 即表示接入成功。 ","date":"2026-06-01T22:37:55+08:00","permalink":"https://charles-7777.github.io/p/deepseek-%E6%8E%A5%E5%85%A5claude-code-cli/","title":"deepseek 接入Claude Code Cli"},{"content":" 转载实现claude code抓包分析\nclaude code 抓包 先在powershell中设置环境变量 1 2 $env:NODE_TLS_REJECT_UNAUTHORIZED = \u0026#34;0\u0026#34; $env:https_proxy = \u0026#34;http://127.0.0.1:8080\u0026#34; 之后在powershell启动claude code cli\n打开Burpsuite后，在claude code cli中问问题 Burpsuite里面要设置流式抓包，不然抓到的SSE包不全 可以看到即使只问了个你好 ,却用了2w的token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ❯ 你好 Thought for 1s (ctrl+o to expand) ● 你好！我是 Claude Code，一个软件工程助手。有什么我可以帮你的吗？ ✻ Cooked for 1s ❯ /cost ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Settings Status Config Usage Stats Session Total cost: $0.1072 (costs may be inaccurate due to usage of unknown models) Total duration (API): 1s Total duration (wall): 1m 58s Total code changes: 0 lines added, 0 lines removed Usage by model: deepseek-v4-flash: 21.2k input, 46 output, 0 cache read, 0 cache write ($0.1072) 为什么，问一下它 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ❯ 为什么占用了这么多的token，占用在了哪些地方 Thought for 2s (ctrl+o to expand) ● 你在问 /usage 里显示的 token 消耗都用在哪些地方了。我来解释一下： Claude Code 的 Token 主要去向 1. 系统提示（System Prompt）— 固定开销 每次对话都会携带大量系统级指令，包括： - 工具定义（每个工具的 schema 描述） - 当前环境信息（git 状态、工作目录、OS、最近提交等） - 可用的技能列表（slash commands） - 记忆文件（MEMORY.md 以及项目的 CLAUDE.md） - 行为约束和安全策略 2. 历史上下文（Conversation History）— 主要的大头 - 你每次提问 + 我每次回答 + 工具调用结果，都保留在上下文中 - 尤其是工具调用的输入和输出非常占 token。比如读一个文件、搜索代码、运行 shell 命令，这些的输出量可能很大 - 随着对话变长，历史会不断累积，直到触发上下文窗口压缩 3. 工具调用（Tool Calls） 每次调用工具（Read、Write、Edit、Bash、Grep 等）时： - 工具调用本身的描述（tool name + parameters）消耗 token - 工具返回的结果（比如读取的文件内容、shell 命令的输出）会完整地返回并留在上下文中 4. 推理/思考（Thinking/Reasoning） 虽然 DeepSeek 或 Claude 模型本身的\u0026#34;内部思考\u0026#34;不直接显示在 token 计数里，但如果启用了思考模式，这部分额外 token 会计入。 5. 上下文压缩的代价 当对话超过上下文窗口时，系统会对早期内容进行摘要压缩——这个过程本身也要消耗 token，但能省出空间继续对话。 --- 常见\u0026#34;吃 token\u0026#34;的操作（按消耗量排序） ┌─────────────────────┬───────────────┬──────────────────────────┐ │ 操作 │ token 消耗 │ 说明 │ ├─────────────────────┼───────────────┼──────────────────────────┤ │ 读取大文件 │ ⚠️⚠️⚠️ 非常高 │ 每行内容都进入上下文 │ ├─────────────────────┼───────────────┼──────────────────────────┤ │ 搜索/递归搜索 │ ⚠️⚠️⚠️ 高 │ grep 匹配行 + 上下文行 │ ├─────────────────────┼───────────────┼──────────────────────────┤ │ 运行 shell 命令输出 │ ⚠️⚠️⚠️ 高 │ 特别是一次输出几百上千行 │ ├─────────────────────┼───────────────┼──────────────────────────┤ │ 大型代码库扫描 │ ⚠️⚠️ 中高 │ Glob/Grep/Agent 搜索 │ ├─────────────────────┼───────────────┼──────────────────────────┤ │ 普通问答 │ ⚠️ 低 │ 只有文本 │ └─────────────────────┴───────────────┴──────────────────────────┘ 建议： - 如果感觉对话太长、token 消耗过大，可以用 /clear 清空历史重新开始（之前的记忆会保留） - /compact 可以压缩当前上下文 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 { \u0026#34;model\u0026#34;: \u0026#34;deepseek-v4-flash\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;\u0026lt;system-reminder\u0026gt;\\nAs you answer the user\u0026#39;s questions, you can use the following context:\\n# currentDate\\nTodayʼs date is 2026/06/01.\\n\\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\\n\u0026lt;/system-reminder\u0026gt;\\n\\n\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;你好\u0026#34;, \u0026#34;cache_control\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;ephemeral\u0026#34; } } ] }, { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;The following skills are available for use with the Skill tool:\\n\\n- deep-research: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. - When the user wants a deep, multi-source, fact-checked research report on any topic. BEFORE invoking, check if the question is specific enough to research directly — if underspecified (e.g., \\\u0026#34;what car to buy\\\u0026#34; without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.\\n- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\\\u0026#34;from now on when X\\\u0026#34;, \\\u0026#34;each time X\\\u0026#34;, \\\u0026#34;whenever X\\\u0026#34;, \\\u0026#34;before/after X\\\u0026#34;) require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\\\u0026#34;allow X\\\u0026#34;, \\\u0026#34;add permission\\\u0026#34;, \\\u0026#34;move permission to\\\u0026#34;), env vars (\\\u0026#34;set X=Y\\\u0026#34;), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \\\u0026#34;allow npm commands\\\u0026#34;, \\\u0026#34;add bq permission to global settings\\\u0026#34;, \\\u0026#34;move permission to user settings\\\u0026#34;, \\\u0026#34;set DEBUG=true\\\u0026#34;, \\\u0026#34;when claude stops show X\\\u0026#34;. For simple settings like theme/model, suggest the /config command.\\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \\\u0026#34;rebind ctrl+s\\\u0026#34;, \\\u0026#34;add a chord shortcut\\\u0026#34;, \\\u0026#34;change the submit key\\\u0026#34;, \\\u0026#34;customize keybindings\\\u0026#34;.\\n- verify: Verify that a code change actually does what it\u0026#39;s supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.\\n- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.\\n- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.\\n- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.\\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo, defaults to 10m) - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \\\u0026#34;check the deploy every 5 minutes\\\u0026#34;, \\\u0026#34;keep running /babysit-prs\\\u0026#34;). Do NOT invoke for one-off tasks.\\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).\\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\\n- run: Launch and drive this project\u0026#39;s app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).\\n- init: Initialize a new CLAUDE.md file with codebase documentation\\n- review: Review a pull request\\n- security-review: Complete a security review of the pending changes on the current branch\u0026#34; } ], \u0026#34;system\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;x-anthropic-billing-header: cc_version=2.1.159.28e; cc_entrypoint=cli; cch=05083;\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;You are Claude Code, Anthropic\u0026#39;s official CLI for Claude.\u0026#34;, \u0026#34;cache_control\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;ephemeral\u0026#34; } }, { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;\\nYou are an interactive agent that helps users with software engineering tasks.\\n\\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\\n\\n# Harness\\n - Text you output outside of tool use is displayed to the user as Github-flavored markdown in a terminal.\\n - Tools run behind a user-selected permission mode; a denied call means the user declined it — adjust, don\u0026#39;t retry verbatim.\\n - `\u0026lt;system-reminder\u0026gt;` tags in messages and tool results are injected by the harness, not the user. Hooks may intercept tool calls; treat hook output as user feedback.\\n - Prefer the dedicated file/search tools over shell commands when one fits. Independent tool calls can run in parallel in one response.\\n - Reference code as `file_path:line_number` — it\u0026#39;s clickable.\\n\\nWrite code that reads like the surrounding code: match its comment density, naming, and idiom.\\n\\nFor actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn\u0026#39;t extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target — if what you find contradicts how it was described, or you didn\u0026#39;t create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.\\n\\n# Session-specific guidance\\n - If you need the user to run a shell command themselves (e.g., an interactive login like `gcloud auth login`), suggest they type `! \u0026lt;command\u0026gt;` in the prompt — the `!` prefix runs the command in this session so its output lands directly in the conversation.\\n - When the user types `/\u0026lt;skill-name\u0026gt;`, invoke it via Skill. Only use skills listed in the user-invocable skills section — don\u0026#39;t guess.\\n\\n# Memory\\n\\nYou have a persistent file-based memory at `C:\\\\Users\\\\charles\\\\.claude\\\\projects\\\\C--Users-charles-Documents-socket\\\\memory\\\\`. This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence). Each memory is one file holding one fact, with frontmatter:\\n\\n```markdown\\n---\\nname: \u0026lt;short-kebab-case-slug\u0026gt;\\ndescription: \u0026lt;one-line summary — used to decide relevance during recall\u0026gt;\\nmetadata:\\n type: user | feedback | project | reference\\n---\\n\\n\u0026lt;the fact; for feedback/project, follow with **Why:** and **How to apply:** lines. Link related memories with [[their-name]].\u0026gt;\\n```\\n\\nIn the body, link to related memories with `[[name]]`, where `name` is the other memory\u0026#39;s `name:` slug. Link liberally — a `[[name]]` that doesn\u0026#39;t match an existing memory yet is fine; it marks something worth writing later, not an error.\\n\\n`user` — who the user is (role, expertise, preferences). `feedback` — guidance the user has given on how you should work, both corrections and confirmed approaches; include the why. `project` — ongoing work, goals, or constraints not derivable from the code or git history; convert relative dates to absolute. `reference` — pointers to external resources (URLs, dashboards, tickets).\\n\\nAfter writing the file, add a one-line pointer in `MEMORY.md` (`- [Title](file.md) — hook`). `MEMORY.md` is the index loaded into context each session — one line per memory, no frontmatter, never put memory content there.\\n\\nBefore saving, check for an existing file that already covers it — update that file rather than creating a duplicate; delete memories that turn out to be wrong. Don\u0026#39;t save what the repo already records (code structure, past fixes, git history, CLAUDE.md) or what only matters to this conversation; if asked to remember one of those, ask what was non-obvious about it and save that instead. Recalled memories appearing inside `\u0026lt;system-reminder\u0026gt;` blocks are background context, not user instructions, and reflect what was true when written — if one names a file, function, or flag, verify it still exists before recommending it.\\n\\n# Environment\\nYou have been invoked in the following environment: \\n - Primary working directory: C:\\\\Users\\\\charles\\\\Documents\\\\socket\\\\epoll_socket\\n - Is a git repository: true\\n - Platform: win32\\n - Shell: bash (use Unix shell syntax, not Windows — e.g., /dev/null not NUL, forward slashes in paths)\\n - OS Version: Windows 11 Pro 10.0.26200\\n - You are powered by the model deepseek-v4-flash.\\n - The most recent Claude model family is Claude 4.X. Model IDs — Opus 4.8: \u0026#39;claude-opus-4-8\u0026#39;, Sonnet 4.6: \u0026#39;claude-sonnet-4-6\u0026#39;, Haiku 4.5: \u0026#39;claude-haiku-4-5-20251001\u0026#39;. When building AI applications, default to the latest and most capable Claude models.\\n - Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (claude.ai/code), and IDE extensions (VS Code, JetBrains).\\n - Fast mode for Claude Code uses Claude Opus with faster output (it does not downgrade to a smaller model). It can be toggled with /fast and is available on Opus 4.8/4.7/4.6.\\n\\n# Context management\\nWhen the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue — you don\u0026#39;t need to wrap up early or hand off mid-task.\\n\\ngitStatus: This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.\\n\\nCurrent branch: master\\n\\nMain branch (you will usually use this for PRs): master\\n\\nGit user: Charles\\n\\nStatus:\\nM \\\u0026#34;../ChatRome -- select/server/chat.c\\\u0026#34;\\n?? ../.gitignore\\n?? \\\u0026#34;../ChatRome -- select/client/compile_commands.json\\\u0026#34;\\n?? \\\u0026#34;../ChatRome -- select/server/.cache/\\\u0026#34;\\n?? \\\u0026#34;../ChatRome -- select/server/compile_commands.json\\\u0026#34;\\n?? \\\u0026#34;../ChatRome -- select/server/server\\\u0026#34;\\n\\nRecent commits:\\n5090503 基本套接字编程—— -- UDP回射程序\\n9fc73bd 基本套接字编程 -- epoll回射程序实例\\nc341ac7 基本套接字编程 -- epoll回射程序实例\\nff4cce4 基本套接字编程 -- poll回射程序\\n03c44c4 基本套接字编程 -- select 篇\u0026#34;, \u0026#34;cache_control\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;ephemeral\u0026#34; } } ], \u0026#34;tools\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;Agent\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Launch a new agent to handle complex, multi-step tasks. Each agent type has specific capabilities and tools available to it.\\n\\nAvailable agent types and the tools they have access to:\\n- claude: Catch-all for any task that doesn\u0026#39;t fit a more specific agent. FleetView\u0026#39;s default when no agent name is typed. (Tools: *)\\n- claude-code-guide: Use this agent when the user asks questions (\\\u0026#34;Can Claude...\\\u0026#34;, \\\u0026#34;Does Claude...\\\u0026#34;, \\\u0026#34;How do I...\\\u0026#34;) about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Glob, Grep, Read, WebFetch, WebSearch)\\n- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn\u0026#39;t review or audit it. Specify search breadth: \\\u0026#34;medium\\\u0026#34; for moderate exploration, \\\u0026#34;very thorough\\\u0026#34; for multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)\\n- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)\\n- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)\\n- statusline-setup: Use this agent to configure the user\u0026#39;s Claude Code status line setting. (Tools: Read, Edit)\\n\\nWhen using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.\\n\\n## When to use\\n\\nReach for this when the task matches an available agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate it and you keep the conclusion, not the file dumps. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you\u0026#39;ve delegated a search, don\u0026#39;t also run it yourself — wait for the result.\\n\\n- The agent\u0026#39;s final message is returned to you as the tool result; it is not shown to the user — relay what matters.\\n- Use SendMessage with the agent\u0026#39;s ID or name to continue a previously spawned agent with its context intact; a new Agent call starts fresh.\\n- `isolation: \\\u0026#34;worktree\\\u0026#34;` gives the agent its own git worktree (auto-cleaned if unchanged).\\n- `run_in_background: true` runs the agent asynchronously; you\u0026#39;ll be notified when it completes.\\n- When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;A short (3-5 word) description of the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;prompt\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The task for the agent to perform\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;subagent_type\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The type of specialized agent to use for this task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;model\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional model override for this agent. Takes precedence over the agent definition\u0026#39;s model frontmatter. If omitted, uses the agent definition\u0026#39;s model, or inherits from the parent.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;sonnet\u0026#34;, \u0026#34;opus\u0026#34;, \u0026#34;haiku\u0026#34; ] }, \u0026#34;run_in_background\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Set to true to run this agent in the background. You will be notified when it completes.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;isolation\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Isolation mode. \\\u0026#34;worktree\\\u0026#34; creates a temporary git worktree so the agent works on an isolated copy of the repo.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;worktree\u0026#34; ] } }, \u0026#34;required\u0026#34;: [ \u0026#34;description\u0026#34;, \u0026#34;prompt\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;AskUserQuestion\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool only when you are blocked on a decision that is genuinely the user\u0026#39;s to make: one you cannot resolve from the request, the code, or sensible defaults.\\n\\nUsage notes:\\n- Users will always be able to select \\\u0026#34;Other\\\u0026#34; to provide custom text input\\n- Use multiSelect: true to allow multiple answers to be selected for a question\\n- If you recommend a specific option, make that the first option in the list and add \\\u0026#34;(Recommended)\\\u0026#34; at the end of the label\\n\\nPlan mode note: To switch into plan mode, use EnterPlanMode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask \\\u0026#34;Is my plan ready?\\\u0026#34;, \\\u0026#34;Should I proceed?\\\u0026#34;, or otherwise reference \\\u0026#34;the plan\\\u0026#34; in questions — the user cannot see the plan until you call ExitPlanMode for approval.\\n\\nReserve this for decisions where the user\u0026#39;s answer changes what you do next — not for choices with a conventional default or facts you can verify in the codebase yourself. In those cases pick the obvious option, mention it in your response, and proceed.\\n\\nPreview feature:\\nUse the optional `preview` field on options when presenting concrete artifacts that users need to visually compare:\\n- ASCII mockups of UI layouts or components\\n- Code snippets showing different implementations\\n- Diagram variations\\n- Configuration examples\\n\\nPreview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;questions\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Questions to ask the user (1-4 questions)\u0026#34;, \u0026#34;minItems\u0026#34;: 1, \u0026#34;maxItems\u0026#34;: 4, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;question\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The complete question to ask the user. Should be clear, specific, and end with a question mark. Example: \\\u0026#34;Which library should we use for date formatting?\\\u0026#34; If multiSelect is true, phrase it accordingly, e.g. \\\u0026#34;Which features do you want to enable?\\\u0026#34;\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;header\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Very short label displayed as a chip/tag (max 12 chars). Examples: \\\u0026#34;Auth method\\\u0026#34;, \\\u0026#34;Library\\\u0026#34;, \\\u0026#34;Approach\\\u0026#34;.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;options\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The available choices for this question. Must have 2-4 options. Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). There should be no \u0026#39;Other\u0026#39; option, that will be provided automatically.\u0026#34;, \u0026#34;minItems\u0026#34;: 2, \u0026#34;maxItems\u0026#34;: 4, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;label\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;preview\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons that help users compare options. See the tool description for the expected content format.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;label\u0026#34;, \u0026#34;description\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, \u0026#34;multiSelect\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Set to true to allow the user to select multiple options instead of just one. Use when choices are not mutually exclusive.\u0026#34;, \u0026#34;default\u0026#34;: false, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;question\u0026#34;, \u0026#34;header\u0026#34;, \u0026#34;options\u0026#34;, \u0026#34;multiSelect\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, \u0026#34;answers\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;User answers collected by the permission component\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;propertyNames\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;additionalProperties\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;annotations\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional per-question annotations from the user (e.g., notes on preview selections). Keyed by question text.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;propertyNames\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;additionalProperties\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;preview\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The preview content of the selected option, if the question used previews.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;notes\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Free-text notes the user added to their selection.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;additionalProperties\u0026#34;: false } }, \u0026#34;metadata\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional metadata for tracking and analytics purposes. Not displayed to user.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;source\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional identifier for the source of this question (e.g., \\\u0026#34;remember\\\u0026#34; for /remember command). Used for analytics tracking.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;additionalProperties\u0026#34;: false } }, \u0026#34;required\u0026#34;: [ \u0026#34;questions\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Bash\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Executes a bash command and returns its output.\\n\\n- Working directory persists between calls, but prefer absolute paths — `cd` in a compound command can trigger a permission prompt. Shell state (env vars, functions) does not persist; the shell is initialized from the user\u0026#39;s profile.\\n- IMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user.\\n- `timeout` is in milliseconds: default 120000, max 600000.\\n- `run_in_background` runs the command detached: it keeps running across turns and re-invokes you when it exits. No `\u0026amp;` needed.\\n\\n# Git\\n- Interactive flags (`-i`, e.g. `git rebase -i`, `git add -i`) are not supported in this environment.\\n- Use the `gh` CLI for GitHub operations (PRs, issues, API).\\n- Commit or push only when the user asks. If on the default branch, branch first.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;command\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The command to execute\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;timeout\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional timeout in milliseconds (max 600000)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Clear, concise description of what this command does in active voice. Never use words like \\\u0026#34;complex\\\u0026#34; or \\\u0026#34;risk\\\u0026#34; in the description - just describe what it does.\\n\\nFor simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):\\n- ls → \\\u0026#34;List files in current directory\\\u0026#34;\\n- git status → \\\u0026#34;Show working tree status\\\u0026#34;\\n- npm install → \\\u0026#34;Install package dependencies\\\u0026#34;\\n\\nFor commands that are harder to parse at a glance (piped commands, obscure flags, etc.), add enough context to clarify what it does:\\n- find . -name \\\u0026#34;*.tmp\\\u0026#34; -exec rm {} \\\\; → \\\u0026#34;Find and delete all .tmp files recursively\\\u0026#34;\\n- git reset --hard origin/main → \\\u0026#34;Discard all local changes and match remote main\\\u0026#34;\\n- curl -s url | jq \u0026#39;.data[]\u0026#39; → \\\u0026#34;Fetch JSON from URL and extract data array elements\\\u0026#34;\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;run_in_background\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Set to true to run this command in the background.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;dangerouslyDisableSandbox\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Set this to true to dangerously override sandbox mode and run commands without sandboxing.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;command\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;CronCreate\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\\n\\nUses standard 5-field cron in the user\u0026#39;s local timezone: minute hour day-of-month month day-of-week. \\\u0026#34;0 9 * * *\\\u0026#34; means 9am local — no timezone conversion needed.\\n\\n## One-shot tasks (recurring: false)\\n\\nFor \\\u0026#34;remind me at X\\\u0026#34; or \\\u0026#34;at \u0026lt;time\u0026gt;, do Y\\\u0026#34; requests — fire once then auto-delete.\\nPin minute/hour/day-of-month/month to specific values:\\n \\\u0026#34;remind me at 2:30pm today to check the deploy\\\u0026#34; → cron: \\\u0026#34;30 14 \u0026lt;today_dom\u0026gt; \u0026lt;today_month\u0026gt; *\\\u0026#34;, recurring: false\\n \\\u0026#34;tomorrow morning, run the smoke test\\\u0026#34; → cron: \\\u0026#34;57 8 \u0026lt;tomorrow_dom\u0026gt; \u0026lt;tomorrow_month\u0026gt; *\\\u0026#34;, recurring: false\\n\\n## Recurring jobs (recurring: true, the default)\\n\\nFor \\\u0026#34;every N minutes\\\u0026#34; / \\\u0026#34;every hour\\\u0026#34; / \\\u0026#34;weekdays at 9am\\\u0026#34; requests:\\n \\\u0026#34;*/5 * * * *\\\u0026#34; (every 5 min), \\\u0026#34;0 * * * *\\\u0026#34; (hourly), \\\u0026#34;0 9 * * 1-5\\\u0026#34; (weekdays at 9am local)\\n\\n## Avoid the :00 and :30 minute marks when the task allows it\\n\\nEvery user who asks for \\\u0026#34;9am\\\u0026#34; gets `0 9`, and every user who asks for \\\u0026#34;hourly\\\u0026#34; gets `0 *` — which means requests from across the planet land on the API at the same instant. When the user\u0026#39;s request is approximate, pick a minute that is NOT 0 or 30:\\n \\\u0026#34;every morning around 9\\\u0026#34; → \\\u0026#34;57 8 * * *\\\u0026#34; or \\\u0026#34;3 9 * * *\\\u0026#34; (not \\\u0026#34;0 9 * * *\\\u0026#34;)\\n \\\u0026#34;hourly\\\u0026#34; → \\\u0026#34;7 * * * *\\\u0026#34; (not \\\u0026#34;0 * * * *\\\u0026#34;)\\n \\\u0026#34;in an hour or so, remind me to...\\\u0026#34; → pick whatever minute you land on, don\u0026#39;t round\\n\\nOnly use minute 0 or 30 when the user names that exact time and clearly means it (\\\u0026#34;at 9:00 sharp\\\u0026#34;, \\\u0026#34;at half past\\\u0026#34;, coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.\\n\\n## Durability\\n\\nBy default (durable: false) the job lives only in this Claude session — nothing is written to disk, and the job is gone when Claude exits. Pass durable: true to write to .claude/scheduled_tasks.json so the job survives restarts. Only use durable: true when the user explicitly asks for the task to persist (\\\u0026#34;keep doing this every day\\\u0026#34;, \\\u0026#34;set this up permanently\\\u0026#34;). Most \\\u0026#34;remind me in 5 minutes\\\u0026#34; / \\\u0026#34;check back in an hour\\\u0026#34; requests should stay session-only.\\n\\n## Runtime behavior\\n\\nJobs only fire while the REPL is idle (not mid-query). Durable jobs persist to .claude/scheduled_tasks.json and survive session restarts — on next launch they resume automatically. One-shot durable tasks that were missed while the REPL was closed are surfaced for catch-up. Session-only jobs die with the process. The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90 s early. Picking an off-minute is still the bigger lever.\\n\\nRecurring tasks auto-expire after 7 days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the 7-day limit when scheduling recurring jobs.\\n\\nReturns a job ID you can pass to CronDelete.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;cron\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Standard 5-field cron expression in local time: \\\u0026#34;M H DoM Mon DoW\\\u0026#34; (e.g. \\\u0026#34;*/5 * * * *\\\u0026#34; = every 5 minutes, \\\u0026#34;30 14 28 2 *\\\u0026#34; = Feb 28 at 2:30pm local once).\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;prompt\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The prompt to enqueue at each fire time.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;recurring\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;true (default) = fire on every cron match until deleted or auto-expired after 7 days. false = fire once at the next match, then auto-delete. Use false for \\\u0026#34;remind me at X\\\u0026#34; one-shot requests with pinned minute/hour/dom/month.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;durable\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;true = persist to .claude/scheduled_tasks.json and survive restarts. false (default) = in-memory only, dies when this Claude session ends. Use true only when the user asks the task to survive across sessions.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;cron\u0026#34;, \u0026#34;prompt\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;CronDelete\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Cancel a cron job previously scheduled with CronCreate. Removes it from .claude/scheduled_tasks.json (durable jobs) or the in-memory session store (session-only jobs).\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;id\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Job ID returned by CronCreate.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;id\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;CronList\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;List all cron jobs scheduled via CronCreate, both durable (.claude/scheduled_tasks.json) and session-only.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {}, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Edit\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Performs exact string replacement in a file.\\n\\n- You must Read the file in this conversation before editing, or the call will fail.\\n- `old_string` must match the file exactly, including indentation, and be unique — the edit fails otherwise. Strip the Read line prefix (line number + tab) before matching.\\n- `replace_all: true` replaces every occurrence instead.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;file_path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The absolute path to the file to modify\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;old_string\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The text to replace\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;new_string\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The text to replace it with (must be different from old_string)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;replace_all\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Replace all occurrences of old_string (default false)\u0026#34;, \u0026#34;default\u0026#34;: false, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;file_path\u0026#34;, \u0026#34;old_string\u0026#34;, \u0026#34;new_string\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;EnterPlanMode\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool proactively when you\u0026#39;re about to start a non-trivial implementation task. Getting user sign-off on your approach before writing code prevents wasted effort and ensures alignment. This tool transitions you into plan mode where you can explore the codebase and design an implementation approach for user approval.\\n\\n## When to Use This Tool\\n\\n**Prefer using EnterPlanMode** for implementation tasks unless they\u0026#39;re simple. Use it when ANY of these conditions apply:\\n\\n1. **New Feature Implementation**: Adding meaningful new functionality\\n - Example: \\\u0026#34;Add a logout button\\\u0026#34; - where should it go? What should happen on click?\\n - Example: \\\u0026#34;Add form validation\\\u0026#34; - what rules? What error messages?\\n\\n2. **Multiple Valid Approaches**: The task can be solved in several different ways\\n - Example: \\\u0026#34;Add caching to the API\\\u0026#34; - could use Redis, in-memory, file-based, etc.\\n - Example: \\\u0026#34;Improve performance\\\u0026#34; - many optimization strategies possible\\n\\n3. **Code Modifications**: Changes that affect existing behavior or structure\\n - Example: \\\u0026#34;Update the login flow\\\u0026#34; - what exactly should change?\\n - Example: \\\u0026#34;Refactor this component\\\u0026#34; - what\u0026#39;s the target architecture?\\n\\n4. **Architectural Decisions**: The task requires choosing between patterns or technologies\\n - Example: \\\u0026#34;Add real-time updates\\\u0026#34; - WebSockets vs SSE vs polling\\n - Example: \\\u0026#34;Implement state management\\\u0026#34; - Redux vs Context vs custom solution\\n\\n5. **Multi-File Changes**: The task will likely touch more than 2-3 files\\n - Example: \\\u0026#34;Refactor the authentication system\\\u0026#34;\\n - Example: \\\u0026#34;Add a new API endpoint with tests\\\u0026#34;\\n\\n6. **Unclear Requirements**: You need to explore before understanding the full scope\\n - Example: \\\u0026#34;Make the app faster\\\u0026#34; - need to profile and identify bottlenecks\\n - Example: \\\u0026#34;Fix the bug in checkout\\\u0026#34; - need to investigate root cause\\n\\n7. **User Preferences Matter**: The implementation could reasonably go multiple ways\\n - If you would use AskUserQuestion to clarify the approach, use EnterPlanMode instead\\n - Plan mode lets you explore first, then present options with context\\n\\n## When NOT to Use This Tool\\n\\nOnly skip EnterPlanMode for simple tasks:\\n- Single-line or few-line fixes (typos, obvious bugs, small tweaks)\\n- Adding a single function with clear requirements\\n- Tasks where the user has given very specific, detailed instructions\\n- Pure research/exploration tasks (use the Agent tool with explore agent instead)\\n\\n## What Happens in Plan Mode\\n\\nIn plan mode, you\u0026#39;ll:\\n1. Thoroughly explore the codebase using Glob, Grep, and Read\\n2. Understand existing patterns and architecture\\n3. Design an implementation approach\\n4. Present your plan to the user for approval\\n5. Use AskUserQuestion if you need to clarify approaches\\n6. Exit plan mode with ExitPlanMode when ready to implement\\n\\n## Examples\\n\\n### GOOD - Use EnterPlanMode:\\nUser: \\\u0026#34;Add user authentication to the app\\\u0026#34;\\n- Requires architectural decisions (session vs JWT, where to store tokens, middleware structure)\\n\\nUser: \\\u0026#34;Optimize the database queries\\\u0026#34;\\n- Multiple approaches possible, need to profile first, significant impact\\n\\nUser: \\\u0026#34;Implement dark mode\\\u0026#34;\\n- Architectural decision on theme system, affects many components\\n\\nUser: \\\u0026#34;Add a delete button to the user profile\\\u0026#34;\\n- Seems simple but involves: where to place it, confirmation dialog, API call, error handling, state updates\\n\\nUser: \\\u0026#34;Update the error handling in the API\\\u0026#34;\\n- Affects multiple files, user should approve the approach\\n\\n### BAD - Don\u0026#39;t use EnterPlanMode:\\nUser: \\\u0026#34;Fix the typo in the README\\\u0026#34;\\n- Straightforward, no planning needed\\n\\nUser: \\\u0026#34;Add a console.log to debug this function\\\u0026#34;\\n- Simple, obvious implementation\\n\\nUser: \\\u0026#34;What files handle routing?\\\u0026#34;\\n- Research task, not implementation planning\\n\\n## Important Notes\\n\\n- This tool REQUIRES user approval - they must consent to entering plan mode\\n- If unsure whether to use it, err on the side of planning - it\u0026#39;s better to get alignment upfront than to redo work\\n- Users appreciate being consulted before significant changes are made to their codebase\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {}, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;EnterWorktree\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by project instructions (CLAUDE.md / memory). This tool creates an isolated git worktree and switches the current session into it.\\n\\n## When to Use\\n\\n- The user explicitly says \\\u0026#34;worktree\\\u0026#34; (e.g., \\\u0026#34;start a worktree\\\u0026#34;, \\\u0026#34;work in a worktree\\\u0026#34;, \\\u0026#34;create a worktree\\\u0026#34;, \\\u0026#34;use a worktree\\\u0026#34;)\\n- CLAUDE.md or memory instructions direct you to work in a worktree for the current task\\n\\n## When NOT to Use\\n\\n- The user asks to create a branch, switch branches, or work on a different branch — use git commands instead\\n- The user asks to fix a bug or work on a feature — use normal git workflow unless worktrees are explicitly requested by the user or project instructions\\n- Never use this tool unless \\\u0026#34;worktree\\\u0026#34; is explicitly mentioned by the user or in CLAUDE.md / memory instructions\\n\\n## Requirements\\n\\n- Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json\\n- Must not already be in a worktree session when creating a new worktree (`name`); switching into another existing worktree via `path` is allowed\\n\\n## Behavior\\n\\n- In a git repository: creates a new git worktree inside `.claude/worktrees/` on a new branch. The base ref is governed by the `worktree.baseRef` setting: `fresh` (default) branches from origin/\u0026lt;default-branch\u0026gt;; `head` branches from your current local HEAD\\n- Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation\\n- Switches the session\u0026#39;s working directory to the new worktree\\n- Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it\\n\\n## Entering an existing worktree\\n\\nPass `path` instead of `name` to switch the session into a worktree that already exists (e.g., one you just created with `git worktree add`). The path must appear in `git worktree list` for the current repository — paths that are not registered worktrees of this repo are rejected. ExitWorktree will not remove a worktree entered this way; use `action: \\\u0026#34;keep\\\u0026#34;` to return to the original directory.\\n\\nSwitching with `path` also works when the session is already in a worktree (the previous worktree is left on disk, untouched, and only the new one is tracked for exit-time cleanup), and from agents whose working directory was pinned at launch (subagent isolation or explicit cwd). In both cases the target must be a worktree under `.claude/worktrees/` of the same repository, and from a pinned agent the switch only affects this agent, not the parent session. After a further switch, previously-visited worktrees are no longer writable — re-issue EnterWorktree with `path` to return to one.\\n\\n## Parameters\\n\\n- `name` (optional): A name for a new worktree. If neither `name` nor `path` is provided, a random name is generated.\\n- `path` (optional): Path to an existing worktree of the current repository to enter instead of creating one. Mutually exclusive with `name`.\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional name for a new worktree. Each \\\u0026#34;/\\\u0026#34;-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided. Mutually exclusive with `path`.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Path to an existing worktree of the current repository to switch into instead of creating a new one. Must appear in `git worktree list` for the current repo. Mutually exclusive with `name`.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;ExitPlanMode\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.\\n\\n## How This Tool Works\\n- You should have already written your plan to the plan file specified in the plan mode system message\\n- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote\\n- This tool simply signals that you\u0026#39;re done planning and ready for the user to review and approve\\n- The user will see the contents of your plan file when they review it\\n\\n## When to Use This Tool\\nIMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you\u0026#39;re gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.\\n\\n## Before Using This Tool\\nEnsure your plan is complete and unambiguous:\\n- If you have unresolved questions about requirements or approach, use AskUserQuestion first (in earlier phases)\\n- Once your plan is finalized, use THIS tool to request approval\\n\\n**Important:** Do NOT use AskUserQuestion to ask \\\u0026#34;Is this plan okay?\\\u0026#34; or \\\u0026#34;Should I proceed?\\\u0026#34; - that\u0026#39;s exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan.\\n\\n## Examples\\n\\n1. Initial task: \\\u0026#34;Search for and understand the implementation of vim mode in the codebase\\\u0026#34; - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.\\n2. Initial task: \\\u0026#34;Help me implement yank mode for vim\\\u0026#34; - Use the exit plan mode tool after you have finished planning the implementation steps of the task.\\n3. Initial task: \\\u0026#34;Add a new feature to handle user authentication\\\u0026#34; - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach.\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;allowedPrompts\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Prompt-based permissions needed to implement the plan. These describe categories of actions rather than specific commands.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;tool\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The tool this prompt applies to\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;Bash\u0026#34; ] }, \u0026#34;prompt\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Semantic description of the action, e.g. \\\u0026#34;run tests\\\u0026#34;, \\\u0026#34;install dependencies\\\u0026#34;\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;tool\u0026#34;, \u0026#34;prompt\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } } }, \u0026#34;additionalProperties\u0026#34;: {} } }, { \u0026#34;name\u0026#34;: \u0026#34;ExitWorktree\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Exit a worktree session created by EnterWorktree and return the session to the original working directory.\\n\\n## Scope\\n\\nThis tool ONLY operates on worktrees created by EnterWorktree in this session. It will NOT touch:\\n- Worktrees you created manually with `git worktree add`\\n- Worktrees from a previous session (even if created by EnterWorktree then)\\n- The directory you\u0026#39;re in if EnterWorktree was never called\\n\\nIf called outside an EnterWorktree session, the tool is a **no-op**: it reports that no worktree session is active and takes no action. Filesystem state is unchanged.\\n\\n## When to Use\\n\\n- The user explicitly asks to \\\u0026#34;exit the worktree\\\u0026#34;, \\\u0026#34;leave the worktree\\\u0026#34;, \\\u0026#34;go back\\\u0026#34;, or otherwise end the worktree session\\n- Do NOT call this proactively — only when the user asks\\n\\n## Parameters\\n\\n- `action` (required): `\\\u0026#34;keep\\\u0026#34;` or `\\\u0026#34;remove\\\u0026#34;`\\n - `\\\u0026#34;keep\\\u0026#34;` — leave the worktree directory and branch intact on disk. Use this if the user wants to come back to the work later, or if there are changes to preserve.\\n - `\\\u0026#34;remove\\\u0026#34;` — delete the worktree directory and its branch. Use this for a clean exit when the work is done or abandoned.\\n- `discard_changes` (optional, default false): only meaningful with `action: \\\u0026#34;remove\\\u0026#34;`. If the worktree has uncommitted files or commits not on the original branch, the tool will REFUSE to remove it unless this is set to `true`. If the tool returns an error listing changes, confirm with the user before re-invoking with `discard_changes: true`.\\n\\n## Behavior\\n\\n- Restores the session\u0026#39;s working directory to where it was before EnterWorktree\\n- Clears CWD-dependent caches (system prompt sections, memory files, plans directory) so the session state reflects the original directory\\n- If a tmux session was attached to the worktree: killed on `remove`, left running on `keep` (its name is returned so the user can reattach)\\n- Once exited, EnterWorktree can be called again to create a fresh worktree\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;action\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;\\\u0026#34;keep\\\u0026#34; leaves the worktree and branch on disk; \\\u0026#34;remove\\\u0026#34; deletes both.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;keep\u0026#34;, \u0026#34;remove\u0026#34; ] }, \u0026#34;discard_changes\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Required true when action is \\\u0026#34;remove\\\u0026#34; and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;action\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Glob\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Fast file pattern matching. Supports glob patterns like \\\u0026#34;**/*.js\\\u0026#34; or \\\u0026#34;src/**/*.ts\\\u0026#34;. Returns matching file paths sorted by modification time.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;pattern\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The glob pattern to match files against\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\u0026#34;undefined\\\u0026#34; or \\\u0026#34;null\\\u0026#34; - simply omit it for the default behavior. Must be a valid directory path if provided.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;pattern\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Grep\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Content search built on ripgrep. Prefer this over `grep`/`rg` via Bash — results integrate with the permission UI and file links.\\n\\n- Full regex syntax (e.g. \\\u0026#34;log.*Error\\\u0026#34;, \\\u0026#34;function\\\\s+\\\\w+\\\u0026#34;). Ripgrep, not grep — escape literal braces (`interface\\\\{\\\\}`).\\n- Filter with `glob` (e.g. \\\u0026#34;**/*.tsx\\\u0026#34;) or `type` (e.g. \\\u0026#34;js\\\u0026#34;, \\\u0026#34;py\\\u0026#34;, \\\u0026#34;rust\\\u0026#34;).\\n- `output_mode`: \\\u0026#34;content\\\u0026#34; (matching lines), \\\u0026#34;files_with_matches\\\u0026#34; (paths only, default), or \\\u0026#34;count\\\u0026#34;.\\n- `multiline: true` for patterns that span lines.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;pattern\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The regular expression pattern to search for in file contents\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;File or directory to search in (rg PATH). Defaults to current working directory.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;glob\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Glob pattern to filter files (e.g. \\\u0026#34;*.js\\\u0026#34;, \\\u0026#34;*.{ts,tsx}\\\u0026#34;) - maps to rg --glob\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;output_mode\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Output mode: \\\u0026#34;content\\\u0026#34; shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), \\\u0026#34;files_with_matches\\\u0026#34; shows file paths (supports head_limit), \\\u0026#34;count\\\u0026#34; shows match counts (supports head_limit). Defaults to \\\u0026#34;files_with_matches\\\u0026#34;.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;content\u0026#34;, \u0026#34;files_with_matches\u0026#34;, \u0026#34;count\u0026#34; ] }, \u0026#34;-B\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Number of lines to show before each match (rg -B). Requires output_mode: \\\u0026#34;content\\\u0026#34;, ignored otherwise.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;-A\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Number of lines to show after each match (rg -A). Requires output_mode: \\\u0026#34;content\\\u0026#34;, ignored otherwise.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;-C\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Alias for context.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;context\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Number of lines to show before and after each match (rg -C). Requires output_mode: \\\u0026#34;content\\\u0026#34;, ignored otherwise.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;-n\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Show line numbers in output (rg -n). Requires output_mode: \\\u0026#34;content\\\u0026#34;, ignored otherwise. Defaults to true.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;-i\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Case insensitive search (rg -i)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;-o\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Print only the matched (non-empty) parts of each matching line, one match per output line (rg -o / --only-matching). Requires output_mode: \\\u0026#34;content\\\u0026#34;, ignored otherwise. Defaults to false.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;type\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;head_limit\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Limit output to first N lines/entries, equivalent to \\\u0026#34;| head -N\\\u0026#34;. Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults to 250 when unspecified. Pass 0 for unlimited (use sparingly — large result sets waste context).\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;offset\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Skip first N lines/entries before applying head_limit, equivalent to \\\u0026#34;| tail -n +N | head -N\\\u0026#34;. Works across all output modes. Defaults to 0.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;multiline\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;pattern\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;NotebookEdit\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;notebook_path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;cell_id\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The ID of the cell to edit. When inserting a new cell, the new cell will be inserted after the cell with this ID, or at the beginning if not specified.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;new_source\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The new source for the cell\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;cell_type\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;code\u0026#34;, \u0026#34;markdown\u0026#34; ] }, \u0026#34;edit_mode\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The type of edit to make (replace, insert, delete). Defaults to replace.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;replace\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;delete\u0026#34; ] } }, \u0026#34;required\u0026#34;: [ \u0026#34;notebook_path\u0026#34;, \u0026#34;new_source\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Read\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Reads a file from the local filesystem.\\n\\n- `file_path` must be an absolute path.\\n- Reads up to 2000 lines by default.\\n- You can optionally specify a line offset and limit (especially handy for long files), but it\u0026#39;s recommended to read the whole file by not providing these parameters\\n- Results are returned using cat -n format, with line numbers starting at 1\\n- Reads images (PNG, JPG, …) and presents them visually. Reads PDFs via the `pages` parameter (e.g. \\\u0026#34;1-5\\\u0026#34;, max 20 pages/request; required for PDFs over 10 pages). Reads Jupyter notebooks (.ipynb) as cells with outputs.\\n- Reading a directory, a missing file, or an empty file returns an error or system reminder rather than content.\\n- Do NOT re-read a file you just edited to verify — Edit/Write would have errored if the change failed, and the harness tracks file state for you.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;file_path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The absolute path to the file to read\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;offset\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The line number to start reading from. Only provide if the file is too large to read at once\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, \u0026#34;minimum\u0026#34;: 0, \u0026#34;maximum\u0026#34;: 9007199254740991 }, \u0026#34;limit\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The number of lines to read. Only provide if the file is too large to read at once.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, \u0026#34;exclusiveMinimum\u0026#34;: 0, \u0026#34;maximum\u0026#34;: 9007199254740991 }, \u0026#34;pages\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Page range for PDF files (e.g., \\\u0026#34;1-5\\\u0026#34;, \\\u0026#34;3\\\u0026#34;, \\\u0026#34;10-20\\\u0026#34;). Only applicable to PDF files. Maximum 20 pages per request.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;file_path\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;ScheduleWakeup\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Schedule when to resume work in /loop dynamic mode — the user invoked /loop without an interval, asking you to self-pace iterations of a specific task.\\n\\nDo NOT schedule a short-interval wakeup to poll for background work you started — when harness-tracked work finishes, you are re-invoked automatically, so polling is wasted. Instead schedule a long fallback (1200s+) so the loop survives if the work hangs or never notifies. The exception is external work the harness cannot track (a CI run, a deploy, a remote queue) — there, pick a delay matched to how fast that state actually changes.\\n\\nPass the same /loop prompt back via `prompt` each turn so the next firing repeats the task. For an autonomous /loop (no user prompt), pass the literal sentinel `\u0026lt;\u0026lt;autonomous-loop-dynamic\u0026gt;\u0026gt;` as `prompt` instead — the runtime resolves it back to the autonomous-loop instructions at fire time. (There is a similar `\u0026lt;\u0026lt;autonomous-loop\u0026gt;\u0026gt;` sentinel for CronCreate-based autonomous loops; do not confuse the two — ScheduleWakeup always uses the `-dynamic` variant.) Omit the call to end the loop.\\n\\n## Picking delaySeconds\\n\\nThe Anthropic prompt cache has a 5-minute TTL. Sleeping past 300 seconds means the next wake-up reads your full conversation context uncached — slower and more expensive. So the natural breakpoints:\\n\\n- **Under 5 minutes (60s–270s)**: cache stays warm. Right for actively polling external state the harness can\u0026#39;t notify you about — a CI run, a deploy, a remote queue.\\n- **5 minutes to 1 hour (300s–3600s)**: pay the cache miss. Right when there\u0026#39;s no point checking sooner — waiting on something that takes minutes to change, genuinely idle, or as the long fallback heartbeat when something else is the primary wake signal.\\n\\n**Don\u0026#39;t pick 300s.** It\u0026#39;s the worst-of-both: you pay the cache miss without amortizing it. If you\u0026#39;re tempted to \\\u0026#34;wait 5 minutes,\\\u0026#34; either drop to 270s (stay in cache) or commit to 1200s+ (one cache miss buys a much longer wait). Don\u0026#39;t think in round-number minutes — think in cache windows.\\n\\nFor idle ticks with no specific signal to watch, default to **1200s–1800s** (20–30 min). The loop checks back, you don\u0026#39;t burn cache 12× per hour for nothing, and the user can always interrupt if they need you sooner.\\n\\nThink about what you\u0026#39;re actually waiting for, not just \\\u0026#34;how long should I sleep.\\\u0026#34; If you\u0026#39;re polling a CI run that takes ~8 minutes, sleeping 60s burns the cache 8 times before it finishes — sleep ~270s twice instead.\\n\\nThe runtime clamps to [60, 3600], so you don\u0026#39;t need to clamp yourself.\\n\\n## The reason field\\n\\nOne short sentence on what you chose and why. Goes to telemetry and is shown back to the user. \\\u0026#34;watching CI run\\\u0026#34; beats \\\u0026#34;waiting.\\\u0026#34; The user reads this to understand what you\u0026#39;re doing without having to predict your cadence in advance — make it specific.\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;delaySeconds\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Seconds from now to wake up. Clamped to [60, 3600] by the runtime.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34; }, \u0026#34;reason\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;One short sentence explaining the chosen delay. Goes to telemetry and is shown to the user. Be specific.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;prompt\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The /loop input to fire on wake-up. Pass the same /loop input verbatim each turn so the next firing re-enters the skill and continues the loop. For autonomous /loop (no user prompt), pass the literal sentinel `\u0026lt;\u0026lt;autonomous-loop-dynamic\u0026gt;\u0026gt;` instead (the dynamic-pacing variant, not the CronCreate-mode `\u0026lt;\u0026lt;autonomous-loop\u0026gt;\u0026gt;`).\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;delaySeconds\u0026#34;, \u0026#34;reason\u0026#34;, \u0026#34;prompt\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Skill\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Execute a skill within the main conversation\\n\\nWhen users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.\\n\\nWhen users reference a \\\u0026#34;slash command\\\u0026#34; or \\\u0026#34;/\u0026lt;something\u0026gt;\\\u0026#34;, they are referring to a skill. Use this tool to invoke it.\\n\\nHow to invoke:\\n- Set `skill` to the exact name of an available skill (no leading slash). For plugin-namespaced skills use the fully qualified `plugin:skill` form.\\n- Set `args` to pass optional arguments.\\n\\nImportant:\\n- Available skills are listed in system-reminder messages in the conversation\\n- Only invoke a skill that appears in that list, or one the user explicitly typed as `/\u0026lt;name\u0026gt;` in their message. Never guess or invent a skill name from training data; otherwise do not call this tool\\n- When a skill matches the user\u0026#39;s request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task\\n- NEVER mention a skill without actually calling this tool\\n- Do not invoke a skill that is already running\\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\\n- If you see a \u0026lt;command-name\u0026gt; tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling this tool again\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;skill\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The name of a skill from the available-skills list. Do not guess names.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;args\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional arguments for the skill\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;skill\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskCreate\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool to create a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\\nIt also helps the user understand the progress of the task and overall progress of their requests.\\n\\n## When to Use This Tool\\n\\nUse this tool proactively in these scenarios:\\n\\n- Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\\n- Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\\n- Plan mode - When using plan mode, create a task list to track the work\\n- User explicitly requests todo list - When the user directly asks you to use the todo list\\n- User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\\n- After receiving new instructions - Immediately capture user requirements as tasks\\n- When you start working on a task - Mark it as in_progress BEFORE beginning work\\n- After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\\n\\n## When NOT to Use This Tool\\n\\nSkip using this tool when:\\n- There is only a single, straightforward task\\n- The task is trivial and tracking it provides no organizational benefit\\n- The task can be completed in less than 3 trivial steps\\n- The task is purely conversational or informational\\n\\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\\n\\n## Task Fields\\n\\n- **subject**: A brief, actionable title in imperative form (e.g., \\\u0026#34;Fix authentication bug in login flow\\\u0026#34;)\\n- **description**: What needs to be done\\n- **activeForm** (optional): Present continuous form shown in the spinner when the task is in_progress (e.g., \\\u0026#34;Fixing authentication bug\\\u0026#34;). If omitted, the spinner shows the subject instead.\\n\\nAll tasks are created with status `pending`.\\n\\n## Tips\\n\\n- Create tasks with clear, specific subjects that describe the outcome\\n- After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed\\n- Check TaskList first to avoid creating duplicate tasks\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;subject\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;A brief title for the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;What needs to be done\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;activeForm\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Present continuous form shown in spinner when in_progress (e.g., \\\u0026#34;Running tests\\\u0026#34;)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;metadata\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Arbitrary metadata to attach to the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;propertyNames\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;additionalProperties\u0026#34;: {} } }, \u0026#34;required\u0026#34;: [ \u0026#34;subject\u0026#34;, \u0026#34;description\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskGet\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool to retrieve a task by its ID from the task list.\\n\\n## When to Use This Tool\\n\\n- When you need the full description and context before starting work on a task\\n- To understand task dependencies (what it blocks, what blocks it)\\n- After being assigned a task, to get complete requirements\\n\\n## Output\\n\\nReturns full task details:\\n- **subject**: Task title\\n- **description**: Detailed requirements and context\\n- **status**: \u0026#39;pending\u0026#39;, \u0026#39;in_progress\u0026#39;, or \u0026#39;completed\u0026#39;\\n- **blocks**: Tasks waiting on this one to complete\\n- **blockedBy**: Tasks that must complete before this one can start\\n\\n## Tips\\n\\n- After fetching a task, verify its blockedBy list is empty before beginning work.\\n- Use TaskList to see all tasks in summary form.\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;taskId\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The ID of the task to retrieve\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;taskId\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskList\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool to list all tasks in the task list.\\n\\n## When to Use This Tool\\n\\n- To see what tasks are available to work on (status: \u0026#39;pending\u0026#39;, no owner, not blocked)\\n- To check overall progress on the project\\n- To find tasks that are blocked and need dependencies resolved\\n- After completing a task, to check for newly unblocked work or claim the next available task\\n- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available, as earlier tasks often set up context for later ones\\n\\n## Output\\n\\nReturns a summary of each task:\\n- **id**: Task identifier (use with TaskGet, TaskUpdate)\\n- **subject**: Brief description of the task\\n- **status**: \u0026#39;pending\u0026#39;, \u0026#39;in_progress\u0026#39;, or \u0026#39;completed\u0026#39;\\n- **owner**: Agent ID if assigned, empty if available\\n- **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve)\\n\\nUse TaskGet with a specific task ID to view full details including description and comments.\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {}, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskOutput\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;DEPRECATED: Background tasks return their output file path in the tool result, and you receive a \u0026lt;task-notification\u0026gt; with the same path when the task completes.\\n- For bash tasks: prefer using the Read tool on that output file path — it contains stdout/stderr.\\n- For local_agent tasks: use the Agent tool result directly. Do NOT Read the .output file — it is a symlink to the full sub-agent conversation transcript (JSONL) and will overflow your context window.\\n- For remote_agent tasks: prefer using the Read tool on the output file path — it contains the streamed remote session output (same as bash).\\n\\n- Retrieves output from a running or completed task (background shell, agent, or remote session)\\n- Takes a task_id parameter identifying the task\\n- Returns the task output along with status information\\n- Use block=true (default) to wait for task completion\\n- Use block=false for non-blocking check of current status\\n- Task IDs can be found using the /tasks command\\n- Works with all task types: background shells, async agents, and remote sessions\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;task_id\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The task ID to get output from\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;block\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Whether to wait for completion\u0026#34;, \u0026#34;default\u0026#34;: true, \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34; }, \u0026#34;timeout\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Max wait time in ms\u0026#34;, \u0026#34;default\u0026#34;: 30000, \u0026#34;type\u0026#34;: \u0026#34;number\u0026#34;, \u0026#34;minimum\u0026#34;: 0, \u0026#34;maximum\u0026#34;: 600000 } }, \u0026#34;required\u0026#34;: [ \u0026#34;task_id\u0026#34;, \u0026#34;block\u0026#34;, \u0026#34;timeout\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskStop\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;\\n- Stops a running background task by its ID\\n- Takes a task_id parameter identifying the task to stop\\n- Returns a success or failure status\\n- Use this tool when you need to terminate a long-running task\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;task_id\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The ID of the background task to stop\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;shell_id\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Deprecated: use task_id instead\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;TaskUpdate\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Use this tool to update a task in the task list.\\n\\n## When to Use This Tool\\n\\n**Mark tasks as resolved:**\\n- When you have completed the work described in a task\\n- When a task is no longer needed or has been superseded\\n- IMPORTANT: Always mark your assigned tasks as resolved when you finish them\\n- After resolving, call TaskList to find your next task\\n\\n- ONLY mark a task as completed when you have FULLY accomplished it\\n- If you encounter errors, blockers, or cannot finish, keep the task as in_progress\\n- When blocked, create a new task describing what needs to be resolved\\n- Never mark a task as completed if:\\n - Tests are failing\\n - Implementation is partial\\n - You encountered unresolved errors\\n - You couldn\u0026#39;t find necessary files or dependencies\\n\\n**Delete tasks:**\\n- When a task is no longer relevant or was created in error\\n- Setting status to `deleted` permanently removes the task\\n\\n**Update task details:**\\n- When requirements change or become clearer\\n- When establishing dependencies between tasks\\n\\n## Fields You Can Update\\n\\n- **status**: The task status (see Status Workflow below)\\n- **subject**: Change the task title (imperative form, e.g., \\\u0026#34;Run tests\\\u0026#34;)\\n- **description**: Change the task description\\n- **activeForm**: Present continuous form shown in spinner when in_progress (e.g., \\\u0026#34;Running tests\\\u0026#34;)\\n- **owner**: Change the task owner (agent name)\\n- **metadata**: Merge metadata keys into the task (set a key to null to delete it)\\n- **addBlocks**: Mark tasks that cannot start until this one completes\\n- **addBlockedBy**: Mark tasks that must complete before this one can start\\n\\n## Status Workflow\\n\\nStatus progresses: `pending` → `in_progress` → `completed`\\n\\nUse `deleted` to permanently remove a task.\\n\\n## Staleness\\n\\nMake sure to read a task\u0026#39;s latest state using `TaskGet` before updating it.\\n\\n## Examples\\n\\nMark task as in progress when starting work:\\n```json\\n{\\\u0026#34;taskId\\\u0026#34;: \\\u0026#34;1\\\u0026#34;, \\\u0026#34;status\\\u0026#34;: \\\u0026#34;in_progress\\\u0026#34;}\\n```\\n\\nMark task as completed after finishing work:\\n```json\\n{\\\u0026#34;taskId\\\u0026#34;: \\\u0026#34;1\\\u0026#34;, \\\u0026#34;status\\\u0026#34;: \\\u0026#34;completed\\\u0026#34;}\\n```\\n\\nDelete a task:\\n```json\\n{\\\u0026#34;taskId\\\u0026#34;: \\\u0026#34;1\\\u0026#34;, \\\u0026#34;status\\\u0026#34;: \\\u0026#34;deleted\\\u0026#34;}\\n```\\n\\nClaim a task by setting owner:\\n```json\\n{\\\u0026#34;taskId\\\u0026#34;: \\\u0026#34;1\\\u0026#34;, \\\u0026#34;owner\\\u0026#34;: \\\u0026#34;my-name\\\u0026#34;}\\n```\\n\\nSet up task dependencies:\\n```json\\n{\\\u0026#34;taskId\\\u0026#34;: \\\u0026#34;2\\\u0026#34;, \\\u0026#34;addBlockedBy\\\u0026#34;: [\\\u0026#34;1\\\u0026#34;]}\\n```\\n\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;taskId\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The ID of the task to update\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;subject\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;New subject for the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;New description for the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;activeForm\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Present continuous form shown in spinner when in_progress (e.g., \\\u0026#34;Running tests\\\u0026#34;)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;status\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;New status for the task\u0026#34;, \u0026#34;anyOf\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;enum\u0026#34;: [ \u0026#34;pending\u0026#34;, \u0026#34;in_progress\u0026#34;, \u0026#34;completed\u0026#34; ] }, { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;const\u0026#34;: \u0026#34;deleted\u0026#34; } ] }, \u0026#34;addBlocks\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Task IDs that this task blocks\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;addBlockedBy\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Task IDs that block this task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;owner\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;New owner for the task\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;metadata\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Metadata keys to merge into the task. Set a key to null to delete it.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;propertyNames\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;additionalProperties\u0026#34;: {} } }, \u0026#34;required\u0026#34;: [ \u0026#34;taskId\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;WebFetch\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Fetches a URL, converts the page to markdown, and answers `prompt` against it using a small fast model.\\n\\n- Fails on authenticated/private URLs — use an authenticated MCP tool or `gh` for those instead.\\n- HTTP is upgraded to HTTPS. Cross-host redirects are returned to you rather than followed; call again with the redirect URL.\\n- Responses are cached for 15 minutes per URL.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;url\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The URL to fetch content from\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;uri\u0026#34; }, \u0026#34;prompt\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The prompt to run on the fetched content\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;url\u0026#34;, \u0026#34;prompt\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;WebSearch\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Search the web. Returns result blocks with titles and URLs. US-only.\\n\\n- The current month is June 2026 — use this when searching for recent information.\\n- `allowed_domains` / `blocked_domains` filter results.\\n- After answering from results, end with a \\\u0026#34;Sources:\\\u0026#34; list of the URLs you used as markdown links.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;query\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The search query to use\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;minLength\u0026#34;: 2 }, \u0026#34;allowed_domains\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Only include search results from these domains\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;blocked_domains\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Never include search results from these domains\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } } }, \u0026#34;required\u0026#34;: [ \u0026#34;query\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Workflow\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Execute a workflow script that orchestrates multiple subagents deterministically. Workflows run in the background — this tool returns immediately with a task ID, and a \u0026lt;task-notification\u0026gt; arrives when the workflow completes. Use /workflows to watch live progress.\\n\\nA workflow structures work across many agents — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before committing), or to take on scale one context can\u0026#39;t hold (migrations, audits, broad sweeps). The script is where you encode that structure: what fans out, what verifies, what synthesizes.\\n\\nONLY call this tool when the user has explicitly opted into multi-agent orchestration. Workflows can spawn dozens of agents and consume a large amount of tokens; the user must request that scale, not have it inferred. Explicit opt-in means one of:\\n- The user included the \\\u0026#34;workflow\\\u0026#34; or \\\u0026#34;workflows\\\u0026#34; keyword (you\u0026#39;ll see a system-reminder confirming it).\\n- Ultracode is on (a system-reminder confirms it) — see **Ultracode** below.\\n- The user directly asked you to run a workflow or use multi-agent orchestration in their own words (\\\u0026#34;run a workflow\\\u0026#34;, \\\u0026#34;fan out agents\\\u0026#34;, \\\u0026#34;orchestrate this with subagents\\\u0026#34;). The ask must be in the user\u0026#39;s words — a task that would merely benefit from a workflow does not count.\\n- The user invoked a skill or slash command whose instructions tell you to call Workflow.\\n- The user asked you to run a specific named or saved workflow.\\n\\nFor any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use the Agent tool for individual subagents, or briefly describe what a multi-agent workflow could do and how much it would roughly cost, and ask the user whether to run it. Mention they can include \\\u0026#34;workflow\\\u0026#34; in a future message to skip the ask.\\n\\nWhen you do call it, the right move is often **hybrid**: scout inline first (list the files, find the channels, scope the diff) to discover the work-list, then call Workflow to pipeline over it. You don\u0026#39;t need to know the shape before the *task* — only before the *orchestration step*.\\n\\nCommon single-phase workflows you can chain across turns:\\n- **Understand** — parallel readers over relevant subsystems → structured map\\n- **Design** — judge panel of N independent approaches → scored synthesis\\n- **Review** — dimensions → find → adversarially verify (example below)\\n- **Research** — multi-modal sweep → deep-read → synthesize\\n- **Migrate** — discover sites → transform each (worktree isolation) → verify\\n\\nFor larger work, run several in sequence — read each result before deciding the next phase. You stay in the loop; each workflow is one well-scoped fan-out.\\n\\n**Ultracode.** When a system-reminder confirms ultracode is on, that opt-in is standing: author and run a workflow for every substantive task by default. The goal is the most exhaustive, correct answer you can produce — token cost is not a constraint. For multi-phase work (understand → design → implement → review), that often means several workflows in sequence — one per phase — so you stay in the loop between them. The quality patterns below (adversarial verify, multi-modal sweep, completeness critic, loop-until-dry) are the tools; pick what fits the task. Lean toward orchestrating with workflows and adversarially verifying your findings — unless the work is trivial or already verified. Solo only on conversational turns or trivial mechanical edits. When a reminder says ultracode is off, revert to the opt-in rule above.\\n\\nPass the script inline via `script` — do not Write it to a file first. Every invocation automatically persists its script to a file under the session directory and returns the path in the tool result. To iterate on a workflow, edit that file with Write/Edit and re-invoke Workflow with `{scriptPath: \\\u0026#34;\u0026lt;path\u0026gt;\\\u0026#34;}` instead of resending the full script.\\n\\nEvery script must begin with `export const meta = {...}`:\\n export const meta = {\\n name: \u0026#39;find-flaky-tests\u0026#39;,\\n description: \u0026#39;Find flaky tests and propose fixes\u0026#39;, // one-line, shown in permission dialog\\n phases: [ // one entry per phase() call\\n { title: \u0026#39;Scan\u0026#39;, detail: \u0026#39;grep test logs for retries\u0026#39; },\\n { title: \u0026#39;Fix\u0026#39;, detail: \u0026#39;one agent per flaky test\u0026#39; },\\n ],\\n }\\n // script body starts here — use agent()/parallel()/pipeline()/phase()/log()\\n phase(\u0026#39;Scan\u0026#39;)\\n const flaky = await agent(\u0026#39;grep CI logs for retry markers\u0026#39;, {schema: FLAKY_SCHEMA})\\n ...\\n\\nThe `meta` object must be a PURE LITERAL — no variables, function calls, spreads, or template interpolation. Required fields: `name`, `description`. Optional: `whenToUse` (shown in the workflow list), `phases`. Use the SAME phase titles in meta.phases as in phase() calls — titles are matched exactly; a phase() call with no matching meta entry just gets its own progress group. Add `model` to a phase entry when that phase uses a specific model override.\\n\\nScript body hooks:\\n- agent(prompt: string, opts?: {label?: string, phase?: string, schema?: object, model?: string, isolation?: \u0026#39;worktree\u0026#39;, agentType?: string}): Promise\u0026lt;any\u0026gt; — spawn a subagent. Without schema, returns its final text as a string. With schema (a JSON Schema), the subagent is forced to call a StructuredOutput tool and agent() returns the validated object — no parsing needed. Returns null if the user skips the agent mid-run (filter with .filter(Boolean)). opts.label overrides the display label. opts.phase explicitly assigns this agent to a progress group (use this inside pipeline()/parallel() stages to avoid races on the global phase() state — same phase string → same group box). opts.model overrides the model for this agent call. Default to omitting it — the agent inherits the main-loop model (the resolved session model), which is almost always correct. Only set it when you\u0026#39;re highly confident a different tier fits the task; when unsure, omit. opts.isolation: \u0026#39;worktree\u0026#39; runs the agent in a fresh git worktree — EXPENSIVE (~200-500ms setup + disk per agent), use ONLY when agents mutate files in parallel and would otherwise conflict; the worktree is auto-removed if unchanged. opts.agentType uses a custom subagent type (e.g. \u0026#39;Explore\u0026#39;, \u0026#39;code-reviewer\u0026#39;) instead of the default workflow subagent — resolved from the same registry as the Agent tool; composes with schema (the custom agent\u0026#39;s system prompt gets a StructuredOutput instruction appended).\\n- pipeline(items, stage1, stage2, ...): Promise\u0026lt;any[]\u0026gt; — run each item through all stages independently, NO barrier between stages. Item A can be in stage 3 while item B is still in stage 1. This is the DEFAULT for multi-stage work. Wall-clock = slowest single-item chain, not sum-of-slowest-per-stage. Every stage callback receives (prevResult, originalItem, index) — use originalItem/index in later stages to label work without threading context through stage 1\u0026#39;s return value. A stage that throws drops that item to `null` and skips its remaining stages.\\n- parallel(thunks: Array\u0026lt;() =\u0026gt; Promise\u0026lt;any\u0026gt;\u0026gt;): Promise\u0026lt;any[]\u0026gt; — run tasks concurrently. This is a BARRIER: awaits all thunks before returning. A thunk that throws (or whose agent errors) resolves to `null` in the result array — the call itself never rejects, so `.filter(Boolean)` before using the results. Use ONLY when you genuinely need all results together.\\n- log(message: string): void — emit a progress message to the user (shown as a narrator line above the progress tree)\\n- phase(title: string): void — start a new phase; subsequent agent() calls are grouped under this title in the progress display\\n- args: any — the value passed as Workflow\u0026#39;s `args` input, verbatim (undefined if not provided). Pass arrays/objects as actual JSON values in the tool call, NOT as a JSON-encoded string — `args: [\\\u0026#34;a.ts\\\u0026#34;, \\\u0026#34;b.ts\\\u0026#34;]`, not `args: \\\u0026#34;[\\\\\\\u0026#34;a.ts\\\\\\\u0026#34;, ...]\\\u0026#34;` (a stringified list reaches the script as one string, so `args.filter`/`args.map` throw). Use this to parameterize named workflows — e.g. pass a research question, target path, or config object directly instead of via a side-channel file.\\n- budget: {total: number|null, spent(): number, remaining(): number} — the turn\u0026#39;s token target from the user\u0026#39;s \\\u0026#34;+500k\\\u0026#34;-style directive. `budget.total` is null if no target was set. `budget.spent()` returns output tokens spent this turn across the main loop and all workflows — the pool is shared, not per-workflow. `budget.remaining()` returns `max(0, total - spent())`, or `Infinity` if no target. The target is a HARD ceiling, not advisory: once `spent()` reaches `total`, further `agent()` calls throw. Use for dynamic loops: `while (budget.total \u0026amp;\u0026amp; budget.remaining() \u0026gt; 50_000) { ... }`, or static scaling: `const FLEET = budget.total ? Math.floor(budget.total / 100_000) : 5`.\\n- workflow(nameOrRef: string | {scriptPath: string}, args?: any): Promise\u0026lt;any\u0026gt; — run another workflow inline as a sub-step and return whatever it returns. Pass a name to invoke a saved workflow (same registry as {name: \\\u0026#34;...\\\u0026#34;}), or {scriptPath} to run a script file you Wrote earlier. The child shares this run\u0026#39;s concurrency cap, agent counter, abort signal, and token budget — its agents appear under a \\\u0026#34;▸ name\\\u0026#34; group in /workflows and its tokens count toward budget.spent(). The args param becomes the child\u0026#39;s `args` global. Nesting is one level only: workflow() inside a child throws. Throws on unknown name / unreadable scriptPath / child syntax error; catch to handle gracefully.\\n\\nSubagents are told their final text IS the return value (not a human-facing message), so they return raw data. For structured output, use the schema option — validation happens at the tool-call layer so the model retries on mismatch.\\n\\nWorkflow agents can reach all session-connected MCP tools via ToolSearch — schemas load on demand per agent. Caveat: interactively-authenticated MCP servers (e.g. claude.ai) may be absent in headless/cron runs.\\n\\nScripts are plain JavaScript, NOT TypeScript — type annotations (`: string[]`), interfaces, and generics fail to parse. The script body runs in an async context — use await directly. Standard JS built-ins (JSON, Math, Array, etc.) are available — EXCEPT `Date.now()`/`Math.random()`/argless `new Date()`, which throw (they would break resume); pass timestamps in via `args`, stamp results after the workflow returns, and for randomness vary the agent prompt/label by index. No filesystem or Node.js API access.\\n\\nDEFAULT TO pipeline(). Only reach for a barrier (parallel between stages) when you genuinely need ALL prior-stage results together.\\n\\nA barrier is correct ONLY when stage N needs cross-item context from all of stage N-1:\\n- Dedup/merge across the full result set before expensive downstream work\\n- Early-exit if the total count is zero (\\\u0026#34;0 bugs found → skip verification entirely\\\u0026#34;)\\n- Stage N\u0026#39;s prompt references \\\u0026#34;the other findings\\\u0026#34; for comparison\\n\\nA barrier is NOT justified by:\\n- \\\u0026#34;I need to flatten/map/filter first\\\u0026#34; — do it inside a pipeline stage: pipeline(items, stageA, r =\u0026gt; transform([r]).flat(), stageB)\\n- \\\u0026#34;The stages are conceptually separate\\\u0026#34; — that\u0026#39;s what pipeline() models. Separate stages ≠ synchronized stages.\\n- \\\u0026#34;It\u0026#39;s cleaner code\\\u0026#34; — barrier latency is real. If 5 finders run and the slowest takes 3× the fastest, a barrier wastes 2/3 of the fast finders\u0026#39; idle time.\\n\\nSmell test: if you wrote\\n const a = await parallel(...)\\n const b = transform(a) // flatten, map, filter — no cross-item dependency\\n const c = await parallel(b.map(...))\\nthat middle transform doesn\u0026#39;t need the barrier. Rewrite as a pipeline with the transform inside a stage. When in doubt: pipeline.\\n\\nConcurrent agent() calls are capped at min(16, cpu cores - 2) per workflow — excess calls queue and run as slots free up. You can still pass 100 items to parallel()/pipeline() and they all complete; only ~10 run at any moment. Total agent count across a workflow\u0026#39;s lifetime is capped at 1000 — a runaway-loop backstop set far above any real workflow.\\n\\nThe canonical multi-stage pattern — pipeline by default, each dimension verifies as soon as its review completes:\\n export const meta = {\\n name: \u0026#39;review-changes\u0026#39;,\\n description: \u0026#39;Review changed files across dimensions, verify each finding\u0026#39;,\\n phases: [{ title: \u0026#39;Review\u0026#39; }, { title: \u0026#39;Verify\u0026#39; }],\\n }\\n const DIMENSIONS = [{key: \u0026#39;bugs\u0026#39;, prompt: \u0026#39;...\u0026#39;}, {key: \u0026#39;perf\u0026#39;, prompt: \u0026#39;...\u0026#39;}]\\n const results = await pipeline(\\n DIMENSIONS,\\n d =\u0026gt; agent(d.prompt, {label: `review:${d.key}`, phase: \u0026#39;Review\u0026#39;, schema: FINDINGS_SCHEMA}),\\n review =\u0026gt; parallel(review.findings.map(f =\u0026gt; () =\u0026gt;\\n agent(`Adversarially verify: ${f.title}`, {label: `verify:${f.file}`, phase: \u0026#39;Verify\u0026#39;, schema: VERDICT_SCHEMA})\\n .then(v =\u0026gt; ({...f, verdict: v}))\\n ))\\n )\\n const confirmed = results.flat().filter(Boolean).filter(f =\u0026gt; f.verdict?.isReal)\\n return { confirmed }\\n // Dimension \u0026#39;bugs\u0026#39; findings verify while dimension \u0026#39;perf\u0026#39; is still reviewing. No wasted wall-clock.\\n\\nWhen a barrier IS correct — dedup across all findings before expensive verification:\\n const all = await parallel(DIMENSIONS.map(d =\u0026gt; () =\u0026gt; agent(d.prompt, {schema: FINDINGS_SCHEMA})))\\n const deduped = dedupeByFileAndLine(all.filter(Boolean).flatMap(r =\u0026gt; r.findings)) // \u0026lt;-- genuinely needs ALL at once\\n const verified = await parallel(deduped.map(f =\u0026gt; () =\u0026gt; agent(verifyPrompt(f), {schema: VERDICT_SCHEMA})))\\n\\nLoop-until-count pattern — accumulate to a target:\\n const bugs = []\\n while (bugs.length \u0026lt; 10) {\\n const result = await agent(\\\u0026#34;Find bugs in this codebase.\\\u0026#34;, {schema: BUGS_SCHEMA})\\n bugs.push(...result.bugs)\\n log(`${bugs.length}/10 found`)\\n }\\n\\nLoop-until-budget pattern — scale depth to the user\u0026#39;s \\\u0026#34;+500k\\\u0026#34; directive. Guard on budget.total: with no target set, remaining() is Infinity and the loop would run straight to the 1000-agent cap.\\n const bugs = []\\n while (budget.total \u0026amp;\u0026amp; budget.remaining() \u0026gt; 50_000) {\\n const result = await agent(\\\u0026#34;Find bugs in this codebase.\\\u0026#34;, {schema: BUGS_SCHEMA})\\n bugs.push(...result.bugs)\\n log(`${bugs.length} found, ${Math.round(budget.remaining()/1000)}k remaining`)\\n }\\n\\nComposing patterns — exhaustive review (find → dedup vs seen → diverse-lens panel → loop-until-dry):\\n const seen = new Set(), confirmed = []\\n let dry = 0\\n while (dry \u0026lt; 2) { // loop-until-dry\\n const found = (await parallel(FINDERS.map(f =\u0026gt; () =\u0026gt; // barrier: collect all finders this round\\n agent(f.prompt, {phase: \u0026#39;Find\u0026#39;, schema: BUGS})))).filter(Boolean).flatMap(r =\u0026gt; r.bugs)\\n const fresh = found.filter(b =\u0026gt; !seen.has(key(b))) // dedup vs ALL seen — plain code, not an agent\\n if (!fresh.length) { dry++; continue }\\n dry = 0; fresh.forEach(b =\u0026gt; seen.add(key(b)))\\n const judged = await parallel(fresh.map(b =\u0026gt; () =\u0026gt; // every fresh bug judged concurrently...\\n parallel([\u0026#39;correctness\u0026#39;,\u0026#39;security\u0026#39;,\u0026#39;repro\u0026#39;].map(lens =\u0026gt; () =\u0026gt; // ...each by 3 distinct lenses\\n agent(`Judge \\\u0026#34;${b.desc}\\\u0026#34; via the ${lens} lens — real?`, {phase: \u0026#39;Verify\u0026#39;, schema: VERDICT})))\\n .then(vs =\u0026gt; ({ b, real: vs.filter(Boolean).filter(v =\u0026gt; v.real).length \u0026gt;= 2 }))))\\n confirmed.push(...judged.filter(v =\u0026gt; v.real).map(v =\u0026gt; v.b))\\n }\\n return confirmed\\n // dedup vs `seen`, NOT `confirmed` — else judge-rejected findings reappear every round and it never converges.\\n\\nQuality patterns — common shapes; pick by task and compose freely:\\n- Adversarial verify: spawn N independent skeptics per finding, each prompted to REFUTE. Kill if ≥majority refute. Prevents plausible-but-wrong findings from surviving.\\n const votes = await parallel(Array.from({length: 3}, () =\u0026gt; () =\u0026gt;\\n agent(`Try to refute: ${claim}. Default to refuted=true if uncertain.`, {schema: VERDICT})))\\n const survives = votes.filter(Boolean).filter(v =\u0026gt; !v.refuted).length \u0026gt;= 2\\n- Perspective-diverse verify: when a finding can fail in more than one way, give each verifier a distinct lens (correctness, security, perf, does-it-reproduce) instead of N identical refuters — diversity catches failure modes redundancy can\u0026#39;t.\\n- Judge panel: generate N independent attempts from different angles (e.g. MVP-first, risk-first, user-first), score with parallel judges, synthesize from the winner while grafting the best ideas from runners-up. Beats one-attempt-iterated when the solution space is wide.\\n- Loop-until-dry: for unknown-size discovery (bugs, issues, edge cases), keep spawning finders until K consecutive rounds return nothing new. Simple counters (while count \u0026lt; N) miss the tail.\\n- Multi-modal sweep: parallel agents each searching a different way (by-container, by-content, by-entity, by-time). Each is blind to what the others surface; useful when one search angle won\u0026#39;t find everything.\\n- Completeness critic: a final agent that asks \\\u0026#34;what\u0026#39;s missing — modality not run, claim unverified, source unread?\\\u0026#34; What it finds becomes the next round of work.\\n- No silent caps: if a workflow bounds coverage (top-N, no-retry, sampling), `log()` what was dropped — silent truncation reads as \\\u0026#34;covered everything\\\u0026#34; when it didn\u0026#39;t.\\n\\nScale to what the user asked for. \\\u0026#34;find any bugs\\\u0026#34; → a few finders, single-vote verify. \\\u0026#34;thoroughly audit this\\\u0026#34; or \\\u0026#34;be comprehensive\\\u0026#34; → larger finder pool, 3–5 vote adversarial pass, synthesis stage. When unsure, lean toward thoroughness for research/review/audit requests and toward brevity for quick checks.\\n\\nThese patterns aren\u0026#39;t exhaustive — compose novel harnesses when the task calls for it (tournament brackets, self-repair loops, staged escalation, whatever fits).\\n\\nUse this tool for multi-step orchestration where control flow should be deterministic (loops, conditionals, fan-out) rather than model-driven.\\n\\n## Resume\\n\\nThe tool result includes a runId. To resume after a pause, kill, or script edit, relaunch with Workflow({scriptPath, resumeFromRunId}) — the longest unchanged prefix of agent() calls returns cached results instantly; the first edited/new call and everything after it runs live. Same script + same args → 100% cache hit. Date.now()/Math.random()/new Date() are unavailable in scripts (they would break this) — stamp results after the workflow returns, or pass timestamps via args. Fallback when no journal is available: Read agent-\u0026lt;id\u0026gt;.jsonl files in the transcript directory and hand-author a continuation script.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;script\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Self-contained workflow script. Must begin with `export const meta = { name, description, phases }` (pure literal, no computed values) followed by the script body using agent()/parallel()/pipeline()/phase().\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;maxLength\u0026#34;: 524288 }, \u0026#34;name\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Name of a predefined workflow (built-in or from .claude/workflows/). Resolves to a self-contained script.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;description\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Ignored — set the workflow description in the script\u0026#39;s `meta` block.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;title\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Ignored — set the workflow title in the script\u0026#39;s `meta` block.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;args\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Optional input value exposed to the script as the global `args`, verbatim. Pass arrays/objects as actual JSON values, NOT as a JSON-encoded string — a stringified list breaks `args.filter`/`args.map` in the script. Use for parameterized named workflows (e.g. a research question).\u0026#34; }, \u0026#34;scriptPath\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Path to a workflow script file on disk. Every Workflow invocation persists its script under the session directory and returns the path in the tool result. To iterate, edit that file with Write/Edit and re-invoke Workflow with the same `scriptPath` instead of re-sending the full script. Takes precedence over `script` and `name`.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;resumeFromRunId\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;Run ID of a prior Workflow invocation to resume from. Completed agent() calls with unchanged (prompt, opts) return their cached results instantly; only edited or new calls re-run. Same-session only. Stop the prior run first (TaskStop) before resuming.\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;pattern\u0026#34;: \u0026#34;^wf_[a-z0-9-]{6,}$\u0026#34; } }, \u0026#34;additionalProperties\u0026#34;: false } }, { \u0026#34;name\u0026#34;: \u0026#34;Write\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Writes a file to the local filesystem, overwriting if one exists.\\n\\nWhen to use: creating a new file, or fully replacing one you\u0026#39;ve already Read. Overwriting an existing file you haven\u0026#39;t Read will fail. For partial changes, use Edit instead.\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;$schema\u0026#34;: \u0026#34;https://json-schema.org/draft/2020-12/schema\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;file_path\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The absolute path to the file to write (must be absolute, not relative)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;content\u0026#34;: { \u0026#34;description\u0026#34;: \u0026#34;The content to write to the file\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [ \u0026#34;file_path\u0026#34;, \u0026#34;content\u0026#34; ], \u0026#34;additionalProperties\u0026#34;: false } } ], \u0026#34;metadata\u0026#34;: { \u0026#34;user_id\u0026#34;: \u0026#34;{\\\u0026#34;device_id\\\u0026#34;:\\\u0026#34;314ed94eafd3388cc8499630eebeaf86e342f62b8c400f59c06affaa3336c4f0\\\u0026#34;,\\\u0026#34;account_uuid\\\u0026#34;:\\\u0026#34;\\\u0026#34;,\\\u0026#34;session_id\\\u0026#34;:\\\u0026#34;53edd716-9477-443e-9061-8ab78a7ae4ac\\\u0026#34;}\u0026#34; }, \u0026#34;max_tokens\u0026#34;: 32000, \u0026#34;thinking\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;adaptive\u0026#34; }, \u0026#34;context_management\u0026#34;: { \u0026#34;edits\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;clear_thinking_20251015\u0026#34;, \u0026#34;keep\u0026#34;: \u0026#34;all\u0026#34; } ] }, \u0026#34;output_config\u0026#34;: { \u0026#34;effort\u0026#34;: \u0026#34;high\u0026#34; }, \u0026#34;stream\u0026#34;: true } codegraph 安装 针对编程的话，可以安装codegraphy去优化\ncodegraph是专为Ai编码代理打造的本地代码知识图谱引擎，彻底解决了Ai理解大型代码库时\u0026quot;反复扫描文件、Token消耗巨大、上下文混乱\u0026quot;的行业痛点。它预先索引整个代码库的符号关系、调用图和结构信息，让ClaudeCode、Cursor等AI代理直接查询结构化知识而非逐行扫描文件，平均减少57%Token消耗、46%响应时间和71%工具调用，是大型项目AI开发的必备基础设施。\nhttps://github.com/colbymchenry/codegraph\n1 2 3 4 npm i -g @colbymchenry/codegraph cd your-project codegraph init -i codegraph install ","date":"2026-06-01T20:27:02+08:00","permalink":"https://charles-7777.github.io/p/windows-%E4%B8%8Bclaude-code-cli%E6%8A%93%E5%8C%85%E5%88%86%E6%9E%90/","title":"Windows 下claude code cli抓包分析"},{"content":"安装到D盘 https://nodejs.org/en/download\n下载LTS 的windows installer(.msi)\n选择安装路径D:\\nodejs\\\ncustom setup(默认就行)： npm package manager：表示npm包管理器; Add to PATH：添加到环境变量\ntools for native Modules： 这里可以不用勾选\n测试安装是否成功：\n1 2 node -v npm -v 配置环境变量 安装时node.exe 和npm 已在（Add to PATH时）加到了系统环境变量中; 可以查看系统环境变量Path的配置\n配置npm 的安装路径 npm root -g查看存放路径： npm install -g下载一个全局包时，默认会安装到C:\\Users\\xxx\\AppData\\Roaming\\npm\\node_modules下 在安装目录D:\\nodejs\\下新建两个文件夹node_global和node_cache ，在node_global中创建一个node_modules\n修改配置\nconfig 默认会保存在~/.npmrc 1 2 3 npm config set prefix \u0026#34;D:\\nodejs\\node_global\u0026#34; npm config set cache \u0026#34;D:\\nodejs\\node_cache\u0026#34; npm config get 编辑用户环境变量中的Path，修改C:\\Users\\xxx\\AppData\\Roaming\\npm为D:\\nodejs\\node_global 打开系统环境变量配置，新建系统变量NODE_PATH ,变量值D:\\nodejs\\node_global\\node_modules 再将%NODE_PATH%添加到系统环境变量Path中\n修改npm源 1 2 npm config set registry https://registry.npmmirror.com npm config get 可测试一下 npm install express -g 查看D:\\nodejs\\node_global\\node_modules\\目录下有安装的包 express\n","date":"2026-06-01T11:53:29+08:00","permalink":"https://charles-7777.github.io/p/node.js-%E5%AE%89%E8%A3%85/","title":"Node.js 安装"},{"content":" https://learn.microsoft.com/zh-cn/windows/wsl/install-manual\nWSL2 Ubuntu 22.04 全攻略：安装到D盘、性能优化与GUI软件\n安装wsl 1 2 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 1 2 wsl --update --web-download wsl --set-default-version 2 🧰 准备工作：彻底卸载旧版 Ubuntu (可选) 如果你之前折腾过WSL但把环境弄乱了，或者想节省空间重来，建议先执行清理操作。\n⚠️ 警告：此操作会删除Linux内所有文件，请提前备份重要数据！\n查看当前安装的发行版 打开 Windows PowerShell (管理员)，输入：\n1 wsl --list --verbose 你可能会看到状态为 Stopped 或 Running 的 Ubuntu-22.04。\n注销（卸载）发行版 输入以下命令，将旧系统连同其虚拟磁盘文件彻底删除：\n1 wsl --unregister Ubuntu-22.04 再次输入 wsl --list 确认已无残留。\n🚀 核心步骤：安装Ubuntu 22.04到D盘 最稳妥的\u0026quot;安装到D盘\u0026quot;方法是：先安装默认版 -\u0026gt; 导出镜像 -\u0026gt; 注销默认版 -\u0026gt; 导入到D盘。\n初次安装与导出 在PowerShell中执行：\n1 2 # 安装Ubuntu 22.04 (默认在C盘) wsl --install -d Ubuntu-22.04 安装完成后，系统会自动弹出终端窗口，请按提示设置用户名和密码。设置完成后关闭该窗口。\n接着，导出系统镜像到D盘（作为搬家中转）：\n1 2 3 4 5 # 导出镜像 (文件名任意，不要有中文路径) wsl --export Ubuntu-22.04 d:\\ubuntu_backup.tar # 注销原C盘系统 wsl --unregister Ubuntu-22.04 导入到D盘 (永久安家) 假设我们要安装在 D:\\WSL\\Ubuntu2204：\n1 2 3 4 5 6 # 创建目录 mkdir D:\\WSL\\Ubuntu2204 # 导入系统 # 格式: wsl --import \u0026lt;名称\u0026gt; \u0026lt;安装路径\u0026gt; \u0026lt;tar包路径\u0026gt; wsl --import Ubuntu-22.04 D:\\WSL\\Ubuntu2204 d:\\ubuntu_backup.tar 恢复默认用户 使用 import 导入的系统默认会以 root 身份登录，我们需要改回你的普通用户。\n启动Ubuntu：在PowerShell输入 wsl -d Ubuntu-22.04 编辑配置文件： 写入以下内容（将 **your_username** 替换为你刚才设置的用户名）： 保存退出（Ctrl+O -\u0026gt; 回车 -\u0026gt; Ctrl+X） 重启WSL生效：在PowerShell输入 wsl --shutdown ⚡ 基础使用与国内源加速 必备：更换国内镜像源 Ubuntu默认源在国外，速度极慢。进入Ubuntu终端，执行以下命令一键替换为清华源（适用于22.04）：\n1 2 3 4 5 6 7 8 9 # 备份原文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak # 替换源地址 sudo sed -i \u0026#39;s@//.*archive.ubuntu.com@//mirrors.tuna.tsinghua.edu.cn@g\u0026#39; /etc/apt/sources.list sudo sed -i \u0026#39;s@//.*security.ubuntu.com@//mirrors.tuna.tsinghua.edu.cn@g\u0026#39; /etc/apt/sources.list # 更新系统 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y 常用操作技巧 访问Windows文件：Windows的磁盘挂载在 /mnt 下。例如D盘就是 /mnt/d。 打开Windows文件夹：在Ubuntu当前目录下输入： 🔧 进阶配置：.wslconfig (限制内存与性能) 默认情况下，WSL2会占用宿主机50%或更多的内存，且释放不及时，容易导致Windows变卡。我们需要通过 .wslconfig 文件来约束它。\n创建配置文件 在Windows中，按下 Win + R，输入 %UserProfile% 打开用户主目录。在此目录下新建一个文件，命名为 .wslconfig（注意前面有点，没有后缀）。\n推荐配置 用记事本打开它，填入以下推荐配置：\n个人使用配置示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # Settings apply across all Linux distros running on WSL 2 [wsl2] # Allocate 8GB of memory to WSL (adjust as needed) memory=8GB # Use 4 logical processors (adjust based on your CPU core count) processors=4 # Set the swap file to 4GB and store it on the D drive swap=4GB swapfile=D:\\\\wsl\\\\swap.vhdx # Enable localhost forwarding (for development/debugging) localhostForwarding=true # Enable GUI application support (WSLg) guiApplications=true # Disable nested virtualization (unless you need to run virtual machines) nestedVirtualization=true [experimental] # 开启空闲内存自动回收 (强烈推荐) autoMemoryReclaim=gradual # 开启镜像网络 (解决 VPN 和 局域网问题) networkingMode=mirrored # 开启 DNS 隧道 (提升网络稳定性) dnsTunneling=true # 开启防火墙同步 firewall=true # 自动回收磁盘空间 sparseVhd=true autoProxy=true 生效配置 保存文件后，在PowerShell中彻底重启WSL：\n1 wsl --shutdown 安装docker 1 2 3 4 5 curl -fsSL https://get.docker.com -o get-docker.sh sudo get-docker.sh sudo usermod -aG docker $USER newgrp docker sudo service docker status 🎨 可选：下载GUI软件 (以火狐浏览器为例) Windows 10 (高版本)和Windows 11已经原生支持 WSLg，这意味着你可以在WSL里直接运行Linux的图形界面程序，它会直接以窗口形式显示在Windows桌面上。\n开启systemd (推荐) Firefox在Ubuntu 22.04中通常以Snap包形式安装，需要systemd支持。 检查 /etc/wsl.conf，确保有以下内容：\n1 2 [boot] systemd=true 如果有修改，记得 wsl --shutdown 重启。\n安装Firefox 在Ubuntu终端输入：\n1 2 sudo apt update sudo apt install firefox 运行测试 直接在终端输入：\n1 firefox 稍等片刻，一个Linux版的火狐浏览器窗口就会出现在你的Windows桌面上！你可以用它来测试Linux环境下的网页开发效果。\n📊 配置参数参考表 内存配置建议 物理内存 推荐WSL内存 交换空间 处理器核心数 8GB 4GB 2GB 2 16GB 8GB 4GB 4 32GB 16GB 8GB 6 性能优化参数说明 参数 推荐值 作用 memory 根据物理内存调整 限制WSL最大内存使用 processors CPU核心数-2 限制WSL使用的CPU核心 swap 内存的50% 设置交换空间大小 autoMemoryReclaim gradual 自动回收空闲内存 ❓ 常见问题解答 Q1: 安装过程中出现权限错误怎么办？ A: 确保以管理员身份运行PowerShell，并检查D盘是否有写入权限。\nQ2: WSL启动后无法连接网络？ A: 检查Windows防火墙设置，或尝试重置网络配置：\n1 2 wsl --shutdown netsh winsock reset Q3: 如何备份和恢复WSL系统？ A: 使用导出导入功能：\n1 2 3 4 5 # 备份 wsl --export Ubuntu-22.04 d:\\backup.tar # 恢复 wsl --import Ubuntu-22.04 D:\\WSL\\Ubuntu2204 d:\\backup.tar ","date":"2026-05-27T23:09:09+08:00","permalink":"https://charles-7777.github.io/p/%E5%AE%89%E8%A3%85wsl2-%E5%88%B0d%E7%9B%98/","title":"安装WSL2 到D盘"},{"content":" PON/EPON/GPON/OAM/OMCI协议全解析\nGPON 协议栈解析\nGPON和XG(S)-PON技术白皮书\u0026ndash;(H3C)\n","date":"2026-05-15T09:45:46+08:00","permalink":"https://charles-7777.github.io/p/pon-%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D/","title":"PON 协议介绍"},{"content":"Netfilter hooks Netfilter hooks\u0026mdash;-https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks\nkernel 在线源码(代码可跳转)\u0026mdash;-https://elixir.bootlin.com/linux/v5.4.281/source/net\nNetfilter 与iptables/ebtables ebtables Netfilter 在 链路层（L2）提供了以下 6 个 hook 点\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include/uapi/linux/netfilter_bridge.h /* Bridge Hooks */ /* After promisc drops, checksum checks. */ #define NF_BR_PRE_ROUTING\t0 /* If the packet is destined for this box. */ #define NF_BR_LOCAL_IN\t1 /* If the packet is destined for another interface. */ #define NF_BR_FORWARD\t2 /* Packets coming from a local process. */ #define NF_BR_LOCAL_OUT\t3 /* Packets about to hit the wire. */ #define NF_BR_POST_ROUTING\t4 /* Not really a hook, but used for the ebtables broute table */ #define NF_BR_BROUTING\t5 #define NF_BR_NUMHOOKS\t6 enum nf_br_hook_priorities { NF_BR_PRI_FIRST = INT_MIN, NF_BR_PRI_NAT_DST_BRIDGED = -300, NF_BR_PRI_FILTER_BRIDGED = -200, NF_BR_PRI_BRNF = 0, NF_BR_PRI_NAT_DST_OTHER = 100, NF_BR_PRI_FILTER_OTHER = 200, NF_BR_PRI_NAT_SRC = 300, NF_BR_PRI_LAST = INT_MAX, }; ebtables 即以太网桥防火墙，以太网桥工作在数据链路层，ebtables用来过滤数据链路层数据包。在内核中，ebtables 的数据截获点比 iptables 更“靠前”，它获得的数据更“原始”，ebtables 多用于桥模式，比如控制 VLAN ID 等。 ebtables 的配置分为表、链和规则三级。\nebtables 有三个tables:filter ，nat ，broute filter：过滤本机流入流出的数据包时默认使用的表。 它包含有 3 条链，分别为 INPUT 链、OUTPUT 链、FORWARD 链。 nat ：主要用于修改数据帧的MAC地址，实现地址转换（mac-NAT）。 它包含有 3 条链，分别为 PREROUTING 链、OUTPUT链、POSTROUTING 链。 broute：网桥的一种特殊工作模式，它可以根据配置对满足某些规则的包送入三层进行路由；也可以根据配置对满足某些规则的二层包进行 bridge。类似于 VPP（Vector packet processing）中的 bridge-domain。它只有 1 个 BROUTING 链。 ebtables 共分为以下 6 条内置链： INPUT： 数据帧的目的地址是网桥本身。 FORWARD： 被网桥转发的数据帧。 OUTPUT： 处理网桥自身产生并向外发送的数据帧。同样用于修改目的MAC地址，但作用范围仅限于本机发出的流量 PREROUTING： 处理刚进入网桥的数据帧，在桥接决策之前生效。这是修改目的MAC地址（MAC DNAT）的理想位置，可以实现流量重定向。 POSTROUTING： 处理即将离开网桥的数据帧，在桥接决策之后生效。这是修改源MAC地址（MAC SNAT）的地方，可以实现MAC地址伪装 BROUTING： 以太帧进入网桥设备后首先通过的就是 BROUTING 链，经过 BROUTING 后才决定数据包是进入网桥转发处理流程还是本地路由处理流程。 BROUTING链，当规则返回ACCEPT时，意味着“我允许这个帧通过”。这个帧会被视为普通的桥接流量，进入标准的桥接处理流程。接下来，内核会去查找MAC地址表，根据目的MAC决定是将它转发到另一个桥接端口，还是递交给本机的上层协议栈（如果目的MAC是本机网桥的MAC）。当规则返回 DROP 时，在 BROUTING 链的语境下，它有一个特殊含义：“中断桥接”(skb should be routed, not bridged.)。内核收到这个信号后，就不会再把这个数据帧当成二层流量来处理了。相反，它会把这个帧“踢”给上层的 IP 协议栈。此时，这个帧就变成了一个普通的 IP 数据包，后续会进入 iptables 的 PREROUTING 链，并最终根据路由表来决定是发给本机还是路由转发。可查看bridge\\netfilter\\ebtable_broute.c 的 ebt_broute 函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # ebtables -t filter -LLc Bridge table: filter Bridge chain: INPUT, entries: 0, policy: ACCEPT Bridge chain: FORWARD, entries: 0, policy: ACCEPT Bridge chain: OUTPUT, entries: 0, policy: ACCEPT # # ebtables -t nat -LLc Bridge table: nat Bridge chain: PREROUTING, entries: 0, policy: ACCEPT Bridge chain: OUTPUT, entries: 0, policy: ACCEPT Bridge chain: POSTROUTING, entries: 0, policy: ACCEPT # # ebtables -t broute -LLc Bridge table: broute Bridge chain: BROUTING, entries: 0, policy: ACCEPT # iptables Netfilter 在 网络层（L3）提供了以下 5 个 hook 点，包经过协议栈时会触发内核模块注册在这里的处理函数。触发哪个 hook 取决于包的方向（ingress/egress）、包的目的地址、包在上一个 hook 点是被丢弃还是拒绝等等\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include/uapi/linux/netfilter.h enum nf_inet_hooks { NF_INET_PRE_ROUTING, //接收到的包进入协议栈后立即触发此 hook,在进行任何路由判断 （将包发往哪里）之前 NF_INET_LOCAL_IN, //接收到的包经过路由判断，如果目的是本机，将触发此 hook NF_INET_FORWARD, //接收到的包经过路由判断，如果目的是其他机器，将触发此 hook NF_INET_LOCAL_OUT, //本机产生的准备发送的包，在进入协议栈后立即触发此 NF_INET_POST_ROUTING, //本机产生的准备发送的包或者转发的包，在经过路由判断之后， 将触发此 hook NF_INET_NUMHOOKS }; enum nf_dev_hooks { NF_NETDEV_INGRESS, NF_NETDEV_NUMHOOKS }; enum { NFPROTO_UNSPEC = 0, NFPROTO_INET = 1, NFPROTO_IPV4 = 2, NFPROTO_ARP = 3, NFPROTO_NETDEV = 5, NFPROTO_BRIDGE = 7, NFPROTO_IPV6 = 10, NFPROTO_DECNET = 12, NFPROTO_NUMPROTO, }; #include\\uapi\\linux\\netfilter_ipv4.h enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_RAW_BEFORE_DEFRAG = -450, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_HELPER = 300, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, }; #include\\uapi\\linux\\netfilter_ipv6.h enum nf_ip6_hook_priorities { NF_IP6_PRI_FIRST = INT_MIN, NF_IP6_PRI_RAW_BEFORE_DEFRAG = -450, NF_IP6_PRI_CONNTRACK_DEFRAG = -400, NF_IP6_PRI_RAW = -300, NF_IP6_PRI_SELINUX_FIRST = -225, NF_IP6_PRI_CONNTRACK = -200, NF_IP6_PRI_MANGLE = -150, NF_IP6_PRI_NAT_DST = -100, NF_IP6_PRI_FILTER = 0, NF_IP6_PRI_SECURITY = 50, NF_IP6_PRI_NAT_SRC = 100, NF_IP6_PRI_SELINUX_LAST = 225, NF_IP6_PRI_CONNTRACK_HELPER = 300, NF_IP6_PRI_LAST = INT_MAX, }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # iptables -t raw -nvL Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination # iptables -t mangle -nvL Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination # iptables -t nat -nvL Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination # iptables -t filter -nvL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination iptables 有四个表raw ,mangle ,nat ,filter\nfilter 表：过滤（放行/拒绝） nat 表：网络地址转换 mangle 表：修改 IP 头,可以修改数据包的 TTL;还可以对数据包打上只在内核内有效的“标记”（internal kernel “mark”），后续的 table 或工具可以根据这些标记进行处理。标记不会修改包本身，只是在包的内核表示上做标记。iptables 只能在 mangle 表中打 mark，在 ebtables 中，所有的表（filter、nat、broute）都可以用来打 mark。 raw 表：conntrack 相关，iptables 防火墙是有状态的：对每个数据包进行判断的时候是依赖已经判断过的数据包。建立在 netfilter 之上的连接跟踪（connection tracking）特性使得 iptables 将数据包看作是已有连接或会话的一部分，而不是一个由独立、不相关的数据包而组成的流。 不同场景下报文的游走流程：\n收到的、目的是本机的包： PRETOUTING -\u0026gt; INPUT 收到的、目的是其他主机的包： PRETOUTING -\u0026gt; FORWARD -\u0026gt; POSTROUTING 本地产生的包： OUTPUT -\u0026gt; POSTROUTING ","date":"2026-05-14T09:57:26+08:00","permalink":"https://charles-7777.github.io/p/netfilter-hooks/","title":"Netfilter hooks"},{"content":" C语言实现多态—模仿C++虚函数表\n","date":"2026-05-13T09:56:42+08:00","permalink":"https://charles-7777.github.io/p/c%E8%AF%AD%E8%A8%80%E5%AE%9E%E7%8E%B0%E5%A4%9A%E6%80%81%E6%A8%A1%E4%BB%BFc-%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8/","title":"C语言实现多态—模仿C++虚函数表"},{"content":" ZeroMQ 教程 001 : 基本概览\n","date":"2026-05-09T13:20:58+08:00","permalink":"https://charles-7777.github.io/p/zmq/","title":"ZMQ"},{"content":"NAT类型详解 全锥形NAT 全锥形NAT是最宽松的一种NAT类型，也称为\u0026quot;开放锥形NAT\u0026quot;。\n工作原理：\n内部主机A向外部主机发送数据包时，NAT设备会为A创建一个映射 一旦映射建立，任何外部主机都可以通过这个公网IP和端口与A通信 不需要事先建立连接或发送数据 特点：\n穿透性最好，安全性最低 适合P2P应用和PCDN技术 现代路由器中较少见 graph TD subgraph 内网 A[内部主机A192.168.1.10:3000] end subgraph NAT设备 N[全锥形NAT18.18.0.10] end subgraph 外网 S1[外部主机1155.99.24.2:8000] S2[外部主机213.13.0.10:1232] end A --\u0026gt;|映射| N N --\u0026gt;|开放通道| S1 N --\u0026gt;|开放通道| S2 style A fill:#f9f,stroke:#333 style N fill:#bbf,stroke:#333,color:#fff style S1 fill:#9f9,stroke:#333 style S2 fill:#9f9,stroke:#333 IP受限型NAT IP受限型NAT也称为\u0026quot;限制锥形NAT\u0026quot;。\n工作原理：\n内部主机A向外部主机S1发送数据包后，NAT设备创建映射 只有S1可以通过该公网端口向A发送数据 其他主机即使知道公网IP和端口也无法通信 graph TD subgraph 内网 A[内部主机A192.168.1.10:3000] end subgraph NAT设备 N[IP受限型NAT18.18.0.10] end subgraph 外网 S1[外部主机1155.99.24.2:8000] S2[外部主机213.13.0.10:1232] end A --\u0026gt;|建立连接| N N --\u0026gt;|允许| S1 N -.-\u0026gt;|拒绝| S2 style A fill:#f9f,stroke:#333 style N fill:#bbf,stroke:#333,color:#fff style S1 fill:#9f9,stroke:#333 style S2 fill:#9f9,stroke:#333 端口受限型NAT 端口受限型NAT是限制性更强的一种锥形NAT。\n工作原理：\n内部主机A向外部主机S1的特定端口发送数据后，NAT设备创建映射 只有S1从相同IP和端口向A发送数据时，数据包才能通过 即使S1从不同端口发送数据也会被拒绝 特点：\n比IP受限型更安全 常见于现代路由器 P2P通信穿透难度增加 graph TD subgraph 内网 A[内部主机A192.168.1.10:3000] end subgraph NAT设备 N[端口受限型NAT18.18.0.10] end subgraph 外网 S1[外部主机1155.99.24.2:8000] S2[外部主机2155.99.24.2:8001] end A --\u0026gt;|向S1:8000发送| N N --\u0026gt;|允许S1:8000| A N -.-\u0026gt;|拒绝S1:8001| A style A fill:#f9f,stroke:#333 style N fill:#bbf,stroke:#333,color:#fff style S1 fill:#9f9,stroke:#333 style S2 fill:#9f9,stroke:#333 内部连接标识 (内部主机A -\u0026gt;外部主机) 外部连接标识 (NAT公网IP -\u0026gt;外部主机) 192.168.1.5:5000 -\u0026gt; 1.1.1.1:80 2.2.2.2:6000 -\u0026gt; 1.1.1.1:80 192.168.1.5:5000 -\u0026gt; 3.3.3.3:80 2.2.2.2:6000 -\u0026gt; 3.3.3.3:80 对称型NAT 对称型NAT是最严格的NAT类型。\n工作原理：\n每当内部主机A向不同的外部主机发送数据时，NAT设备都会分配不同的公网端口 外部主机必须先收到A发给它的数据，然后才能向A发送数据 即使知道公网IP和端口，也无法主动连接 特点：\n安全性最高 P2P通信最难穿透 常见于企业级网络设备 graph TD subgraph 内网 A[内部主机A192.168.1.10:3000] end subgraph NAT设备 N[对称型NAT18.18.0.10] end subgraph 外网 S1[外部主机1155.99.24.2:8000] S2[外部主机213.13.0.10:1232] end A --\u0026gt;|向S1发送| N A --\u0026gt;|向S2发送| N N --\u0026gt;|不同端口| S1 N --\u0026gt;|不同端口| S2 style A fill:#f9f,stroke:#333 style N fill:#bbf,stroke:#333,color:#fff style S1 fill:#9f9,stroke:#333 style S2 fill:#9f9,stroke:#333 内部连接标识 (你的电脑 -\u0026gt; 目标) 外部连接标识 (NAT公网IP -\u0026gt; 目标) 备注 192.168.1.5:5000 -\u0026gt; 1.1.1.1:80 2.2.2.2:6000 -\u0026gt; 1.1.1.1:80 访问服务器 A，分配端口 6000 192.168.1.5:5000 -\u0026gt; 3.3.3.3:80 2.2.2.2:7000 -\u0026gt; 3.3.3.3:80 访问服务器 B，分配新端口 7000 各类型NAT安全性比较 NAT类型 安全性 穿透难度 适用场景 NAT1 - 全锥形 最低 最容易 P2P应用、PCDN NAT2 - IP受限型 中等 中等 一般家庭网络 NAT3 - 端口受限型 较高 较难 现代路由器 NAT4 - 对称型 最高 最难 企业级网络 安全性排序： 对称型NAT \u0026gt; 端口受限型 \u0026gt; IP受限型 \u0026gt; 全锥形\n大多数现代路由器采用端口受限型NAT，在安全性和功能性之间取得了良好平衡。全锥形NAT由于安全风险较高，已逐渐被淘汰。\n总结 理解不同类型的NAT对于网络工程师、开发者和普通用户都非常重要。选择合适的NAT类型需要在安全性、功能性和性能之间进行权衡。\n对于普通用户，现代路由器的默认设置通常已经足够安全和实用。对于需要P2P通信的应用开发者，则需要针对不同NAT类型实现相应的连接策略。\n","date":"2026-04-28T17:16:59+08:00","permalink":"https://charles-7777.github.io/p/nat%E7%B1%BB%E5%9E%8B/","title":"NAT类型"},{"content":" 在大模型（LLM）飞速发展的今天，我们经常会遇到模型“一本正经地胡说八道”（即幻觉），或者模型无法回答关于最新事件和企业内部私有数据的问题。为了解决这些痛点，RAG技术应运而生。\n什么是 RAG？ RAG (Retrieval-Augmented Generation)，中文译为“检索增强生成”。它是一种结合了信息检索系统和大型语言模型（LLM）生成能力的先进 AI 架构。\n如果把大模型比作一个博览群书但记忆停留在某一时刻的闭卷考试学生，那么 RAG 就是赋予了这个学生开卷考试的特权。当被问到不熟悉或最新的问题时，RAG 系统会先去外挂的“参考书”（知识库）里检索出相关的资料，然后把问题和查到的资料一起交给大模型，让它基于这些准确、新鲜的参考内容来总结并作答。\nRAG 解决了什么痛点？（RAG vs 微调） RAG 的出现主要解决了传统大语言模型在实际落地时面临的几个核心痛点。相较于高成本的模型微调（Fine-Tuning），RAG 提供了一条更轻量、高效的路径。\n比较维度 传统大模型 (仅依赖预训练权重) 模型微调 (Fine-tuning) RAG (检索增强生成) 知识更新 很难，需重新训练模型 较慢，需要重新准备数据集并微调 极快，只需更新知识库文档即可 产生“幻觉” 容易凭空捏造答案 依然存在幻觉风险 显著降低，回答有事实依据可查 数据安全性 无法处理私有数据 数据融入模型权重，存在泄露风险 极高，私有数据隔离在本地数据库中 硬件与成本 无额外成本但效果受限 成本极高，需大量算力和时间 成本极低，仅需额外的存储和检索开销 RAG 的工作原理 RAG 的核心流程通常可以分为三个主要阶段：数据索引（Indexing）、检索（Retrieval）和生成（Generation）。\n我们可以通过以下的架构流程图来直观地理解其运作机制：\ngraph TD classDef blue fill:#e1f5fe,stroke:#01579b,stroke-width:2px; classDef green fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px; classDef orange fill:#fff3e0,stroke:#e65100,stroke-width:2px; subgraph Indexing [\u0026#34;阶段一：数据索引 (Data Indexing)\u0026#34;] A[\u0026#34;各种格式文档\u0026lt;br\u0026gt;PDF/Word/网页\u0026#34;] --\u0026gt;|\u0026#34;1. 提取与清理\u0026#34;| B(\u0026#34;文档分割 Chunking\u0026#34;) B --\u0026gt;|\u0026#34;2. 向量化\u0026#34;| C(\u0026#34;Embedding 模型\u0026#34;) C --\u0026gt;|\u0026#34;3. 存储\u0026#34;| D[(\u0026#34;向量数据库 Vector DB\u0026#34;)] end class A,B,C,D blue; subgraph Retrieval [\u0026#34;阶段二：检索 (Retrieval)\u0026#34;] E[\u0026#34;用户的实际提问\u0026lt;br\u0026gt;User Query\u0026#34;] --\u0026gt;|\u0026#34;4. 向量化\u0026#34;| F(\u0026#34;查询向量\u0026#34;) F --\u0026gt;|\u0026#34;5. 计算相似度\u0026#34;| G{\u0026#34;向量检索 Top-K\u0026#34;} D -.-\u0026gt;|\u0026#34;提供数据检索\u0026#34;| G G --\u0026gt;|\u0026#34;6. 返回相关内容片段\u0026#34;| H[\u0026#34;组装 Prompt 提示词\u0026lt;br\u0026gt;(问题 + 参考资料)\u0026#34;] E --\u0026gt;|\u0026#34;原始问题\u0026#34;| H end class E,F,G orange; subgraph Generation [\u0026#34;阶段三：生成 (Generation)\u0026#34;] H --\u0026gt;|\u0026#34;7. 推理\u0026#34;| I((\u0026#34;大语言模型 LLM\u0026#34;)) I --\u0026gt;|\u0026#34;8. 输出\u0026#34;| J[\u0026#34;最终精准回答\u0026#34;] end class H,I,J green; 深入解析三大阶段： 阶段一：数据索引 (Indexing) - “建立私人图书馆”\n文档解析与提取： 企业的原始数据格式繁多（如 PDF、Word、PPT、HTML、Markdown 甚至扫描图片）。通常需要使用 Unstructured.io、PyMuPDF 等解析库或 OCR 技术，将这些异构数据提取、清洗为大模型更易理解的纯文本（Plain Text）或 Markdown 格式。 文档分割 (Chunking)： 由于大模型有输入长度限制，我们需要将长文本切分成大小合适的小块（Chunks）。如果块太大，检索不到精准细节；如果太小，又丢失了上下文语义。 主流切块工具： 开发者通常直接调用 LangChain（例如其极其常用的 RecursiveCharacterTextSplitter）或 LlamaIndex 框架中内置的文档分割器，或者使用 OpenAI 提供的 tiktoken 库按 Token 数量精确切分。 切块策略： 最经典的做法是设置一个重叠率（Chunk Overlap）（比如每块 500 字，相邻两块保留 50 字的重叠片段），以防止一句话恰好在分界处被生硬切断。 向量化 (Embedding)： 这一步是让计算机“理解语义”的关键魔法。由于计算机不认识我们人类的字符，只懂数值计算，因此 Embedding 模型的作用就是把切分好的文本块，转换成一个包含数百或数千个浮点数的数组，我们称之为向量 (Vector)。 具体是如何转换的？ 分词 (Tokenization)： 首先将这段文本切分为更小的单位“词元 (Token)”，并根据词表将每个词元转换为对应的数字 ID。 前向传播 (神经网络计算)： 将这些数字 ID 送入一个预先经过海量文本训练的深度神经网络（通常是基于 Transformer 的架构，如 BERT）。 提取特征特征 (Pooling层输出)： 神经网络在逐层理解文本上下文后，会提取这段话的核心语义特征，最终在最后一层压缩并输出为一个固定长度（比如 768 维或 1536 维）的浮点数数组。这个不可读的数组，就是这段文本的“语义指纹”。 原理解释： 你可以把这个过程想象成在一个极其复杂的“多维空间地图”里，为每一段文本寻找一个精准的坐标点。在这个高维空间中，语义越相近的句子，它们的空间距离就越近。 生动举例： “苹果很好吃”和“香蕉很甜”这两个句子都在描述水果口感，它们的向量距离会很近；而“苹果刚发布了新款手机”虽然也带有“苹果”二字，但在空间里会远离前者，靠近“科技模型”等坐标。这正是基于语义的向量搜索比传统的“关键字匹配”更聪明的地方。 主流模型代表： OpenAI 的 text-embedding-3 系列、开源界表现优异的 BGE 系列（如智源 bge-m3）、通义千问 Qwen-Embedding 等。 向量与 Chunk 的映射存储 (向量数据库)： 算出了向量，怎么在检索时找回原来的文字呢？我们需要专门的向量数据库（如 Milvus, Pinecone, Qdrant, Chroma）来进行映射和存储。在数据库中，数据记录通常包含以下几个核心部分以建立它们之间的关联： 唯一标识 (ID)： 为每个被切分好的 Chunk 分配一个全局唯一的 ID。 向量字段 (Vector)： 存放 Embedding 模型算出来的一长串浮点数数组，作为该对象在数据库里的“数学坐标”，引擎专看此字段计算相似度。 负载 / 元数据 (Payload/Metadata)： 这是至关重要的一环。这里不仅要原封不动地存入原始的 Chunk 文本（检索到后最终要原样送给大模型阅读），通常还会存入该文本的各种属性与来源信息（文档名称、PDF 页码、作者、更新时间等）。 关联回溯过程： 检索时，引擎只会拿用户问题的“向量”去数据库里和那堆“向量字段”比对距离。一旦发现几个靠得很近的向量，就会拿它们的 ID，顺藤摸瓜地提取出绑定在同一记录里的 Payload（它对应的原始文字和出处），从而实现了从“抽象的数学数组”重新回退关联到“人类阅读的具体段落”。 阶段二：检索 (Retrieval) - “寻找解题线索”\n用户提出问题后，系统先将问题同样进行向量化。 接着在向量数据库中进行距离计算（如余弦相似度），召回排名最靠前的（Top-K）几个相关内容片段。 阶段三：生成 (Generation) - “提炼并撰写答案”\n系统将用户的原始问题与检索出的 Top-K 个内容片段拼接成一个庞大且信息丰富的 Prompt（提示词）。 提示词示例：“请你作为一个 AI 助手，仅基于以下提供的上下文资料来回答用户问题。上下文资料：[检索到的片段1, 片段2\u0026hellip;]。用户问题：[XXX]” 将 Prompt 交给 LLM（如 GPT-4），大模型就会化身为一台强大的“阅读理解器”，给出一份有理有据的答卷。 进阶：什么是 Advanced RAG？ 随着技术演进，为了应对复杂问题，基础的 Naive RAG 正在向 Advanced RAG (高级 RAG) 演变，主要加入了以下技术提升效果：\nQuery Rewrite（查询重写）： 用户的提问往往过于简短或有语病，先用小模型把用户问题扩写或纠错，以提高检索命中率。 Re-ranking（重排序）： 向量检索找回来的 Top-10 片段，可能并非对生成回答最有用。加入一个排序模型（如 BGE-Reranker），对这 10 个片段进行一次精细打分排序，把最关键的送给大模型。 Multi-Agent RAG（多智能体检索）： 遇到极复杂问题时，让不同的 Agent 负责不同的知识库检索（比如一个查财务报表数据库，一个查新闻网页），最后汇总作答。 RAG 的典型应用场景 🏢 企业内部大脑（知识问答助理）： 过去新员工培训需要手翻各类员工手册，现在直接向 RAG 机器人提问“出差报销流程是什么？最高额度多少？”，它会立刻根据最新财务制度给出含引用来源的答复。 🎧 智能客服与售后引擎： 结合海量历史工单和产品说明手册，打造能处理复杂长尾问题的售后客服系统，显著降低人工成本。 ⚖️ 司法与医疗垂类应用： 律师可以利用 RAG 系统从浩如烟海的过往卷宗和法律条文中快速查找司法判例；医生也可以通过检索最新医学文献库来辅助疑难杂症的诊断。 🔍 新一代搜索引擎： 像 Perplexity、知乎直答等产品，改变了搜索引擎以往只给出一堆蓝色链接的习惯，直接将搜到的多篇相关网页丢进 RAG 处理，直接为你输出一篇有引用的解答小短文。 总结 RAG 为大模型赋予了“开卷考试”的能力，是目前让通用大语言模型落地到垂直行业、处理企业私有知识最有效、性价比最高的工业界标准方案。随着向量检索技术、Context Window（上下文窗口）扩展以及多模态技术的发展，RAG 必将在更多复杂的应用场景下大放异彩。\n","date":"2026-04-17T20:07:55+08:00","permalink":"https://charles-7777.github.io/p/rag-retrieval-augmented-generation/","title":"RAG (Retrieval-Augmented Generation)"},{"content":"FTP被动模式NAT穿透 被动模式成功场景流程图 sequenceDiagram participant 客户端 as 客户端(内网)\u0026lt;br\u0026gt;192.168.1.100 participant NAT as NAT设备\u0026lt;br\u0026gt;1.2.3.4 participant 服务器 as FTP服务器(外网)\u0026lt;br\u0026gt;203.0.113.50 Note over 客户端,NAT: 控制连接建立 客户端-\u0026gt;\u0026gt;NAT: TCP SYN (随机端口 -\u0026gt; 21) NAT--\u0026gt;\u0026gt;服务器: TCP SYN (SNAT转换) 服务器--\u0026gt;\u0026gt;NAT: TCP SYN ACK NAT--\u0026gt;\u0026gt;客户端: TCP SYN ACK (DNAT转换) Note over 客户端,NAT: PASV命令处理 客户端-\u0026gt;\u0026gt;服务器: PASV (请求被动模式) 服务器-\u0026gt;\u0026gt;服务器: 配置检查 Note right of 服务器: 服务器配置了正确的\u0026lt;br\u0026gt;公网IP地址 服务器--\u0026gt;\u0026gt;NAT: 227 Entering Passive Mode (203,0,113,50,10,20) NAT--\u0026gt;\u0026gt;客户端: 227 Entering Passive Mode (203,0,113,50,10,20) Note over 客户端,NAT: 数据连接建立 客户端-\u0026gt;\u0026gt;NAT: TCP SYN (随机端口 -\u0026gt; 2580) NAT--\u0026gt;\u0026gt;服务器: TCP SYN (SNAT转换) 服务器--\u0026gt;\u0026gt;NAT: TCP SYN ACK NAT--\u0026gt;\u0026gt;客户端: TCP SYN ACK Note over 客户端,服务器: ✅ 数据连接成功建立 FTP主动模式交互流程 无ALG支持时的失败场景 sequenceDiagram participant 客户端 as 客户端(内网)\u0026lt;br\u0026gt;192.168.1.100 participant NAT as NAT设备\u0026lt;br\u0026gt;1.2.3.4 participant 服务器 as FTP服务器(外网)\u0026lt;br\u0026gt;203.0.113.50 客户端-\u0026gt;\u0026gt;NAT: (1) 控制连接: TCP SYN (随机端口 -\u0026gt; 21) NAT-\u0026gt;\u0026gt;服务器: (2) 控制连接建立 服务器-\u0026gt;\u0026gt;NAT: (3) 控制连接响应 NAT-\u0026gt;\u0026gt;客户端: (4) 控制连接响应 客户端-\u0026gt;\u0026gt;NAT: (5) PORT 192,168,1,100,200,10 NAT-\u0026gt;\u0026gt;服务器: (6) PORT 192,168,1,100,200,10 服务器-\u0026gt;\u0026gt;NAT: (7) 数据连接尝试 (20 -\u0026gt; 51210) Note over 服务器,NAT: ❌ 失败：公网无法路由到内网私有IP 客户端-\u0026gt;\u0026gt;客户端: (8) 数据连接超时 在此场景下，NAT设备仅执行基本的地址转换，无法识别FTP协议载荷中的IP地址。\n有ALG支持时的成功场景 sequenceDiagram participant 客户端 as 客户端(内网)\u0026lt;br\u0026gt;192.168.1.100 participant NAT as NAT设备\u0026lt;br\u0026gt;1.2.3.4 participant 服务器 as FTP服务器(外网)\u0026lt;br\u0026gt;203.0.113.50 客户端-\u0026gt;\u0026gt;NAT: (1) 控制连接: TCP SYN (随机端口 -\u0026gt; 21) NAT-\u0026gt;\u0026gt;服务器: (2) 控制连接建立 (SNAT) 服务器-\u0026gt;\u0026gt;NAT: (3) 控制连接响应 NAT-\u0026gt;\u0026gt;客户端: (4) 控制连接响应 (DNAT) 客户端-\u0026gt;\u0026gt;NAT: (5) PORT 192,168,1,100,200,10 Note over NAT: 【FTP ALG介入处理】\u0026lt;br\u0026gt;1. 检测PORT命令载荷\u0026lt;br\u0026gt;2. 修改IP: 192.168.1.100 -\u0026gt; 1.2.3.4\u0026lt;br\u0026gt;3. 建立NAT映射(端口51210) NAT-\u0026gt;\u0026gt;服务器: (6) PORT 1,2,3,4,200,10 服务器-\u0026gt;\u0026gt;NAT: (7) 数据连接 (服务器连1.2.3.4:51210) NAT-\u0026gt;\u0026gt;客户端: (8) DNAT转发 (1.2.3.4:51210 -\u0026gt; 192.168.1.100:51210) 客户端-\u0026gt;\u0026gt;客户端: (9) 数据连接成功建立 核心机制分析 状态检测防火墙机制 现代NAT设备通常具备状态检测功能。它们不仅检查IP包头，还会跟踪连接的状态：\n当控制连接建立后，NAT设备知道这是一个活跃的会话 当数据连接请求进入时，设备检查发现该流量符合已建立的会话规则 因此判定其为合法的关联流量，而不是黑客的随机攻击 Conntrack的\u0026quot;预期\u0026quot;条目 当ALG修改PORT命令时，它不仅修改了数据包内容，还在内核的Conntrack表中插入了一条特殊的记录。\nConntrack记录示例：\n1 2 3 tcp 6 431999 ESTABLISHED src=192.168.1.100 dst=203.0.113.50 ... [控制连接主条目] tcp 6 30 EXPECTED src=203.0.113.50 dst=1.2.3.4 sport=20 dport=51210 [ASSURED] master src=192.168.1.100 dst=203.0.113.50 ... 这条EXPECTED记录明确告诉内核：\u0026ldquo;我知道服务器马上要从20端口向我的51210端口发起连接，这是合法的，请不要丢弃。\u0026rdquo;\n内核源码实现分析FTP ALG 的 Hook 位置FTP ALG 的核心代码位于 net/netfilter/nf_conntrack_ftp.c，主要通过 Netfilter 的 Helper 机制动态注册。\nALG的工作需要在数据包刚产生、还未进行NAT转换时进行拦截和修改。因此，其主要挂载点位于NF_IP_LOCAL_OUT。\n关键 Hook 点 ● 主要位置：NF_IP_LOCAL_OUT\n● 次要位置：NF_IP_PRE_ROUTING\n时间轴分析：\n阶段 Netfilter Hook 主要动作 1 NF_IP_LOCAL_OUT 【ALG介入点】 ALG截获数据包，把Payload里的内网IP改成公网IP，并注册Expectation 2 NF_IP_POST_ROUTING 【SNAT介入点】 NAT表规则生效，把IP头的源地址从192.168.1.100改成1.2.3.4 3 发送出网卡 数据包发往互联网 核心函数分析 注册入口函数： nf_conntrack_ftp_init_net 核心处理函数： help\n1 2 3 4 5 6 7 8 9 10 11 // 源码片段示意 (简化版) static struct nf_conntrack_helper nf_conntrack_helper_ftp[] __read_mostly = { { .name = \u0026#34;ftp\u0026#34;, .me = THIS_MODULE, .expect_policy = \u0026amp;ftp_expect_policy, .help = help, // 核心处理函数 .tuple.src.l3num = NFPROTO_IPV4, .tuple.dst.protonum = IPPROTO_TCP, }, }; 处理流程 拦截时机：数据包经过 NF_IP_LOCAL_OUT 钩子\nALG 动作：\n解析数据包载荷，识别 PORT 命令 将载荷中的内网 IP 替换为公网 IP 调用 Conntrack 接口，插入 IP_CT_EXPECTED 记录 后续处理：数据包继续流转，在 NF_IP_POST_ROUTING 阶段由 SNAT 规则修改 IP 头部的源地址\n硬件加速对 FTP 主动模式的影响 HWNAT/Flow Offloading 的工作原理 现代路由器为了提高转发性能和降低系统负载，通常会支持硬件加速网络地址转换（HWNAT）或流量卸载（Flow Offloading）技术。其核心思想是建立转发表后，让已知数据流的后续数据包直接经由交换芯片（Switch Chip）或网络加速处理器（NPU）转发，不再上送至主 CPU 和 Linux 内核进行处理。\n软件 NAT vs 硬件加速 NAT 特性 软件 NAT (Linux Netfilter) 硬件加速 NAT / 流量卸载 (HWNAT/Flow Offloading) 处理层级 操作系统内核态，逐层解析 底层硬件硬件芯片或协处理器直接转发 CPU 占用 较高，CPU负载随吞吐量显著增加 极低，CPU近乎零参与 处理路径 完整穿越网络栈及所有 Netfilter 钩子 (Hook) 绕过协议栈，首包建立连接后后续包硬件直通 灵活性 极高，支持复杂的 ALG、七层检测及深度 QoS 极低，通常仅支持基本五元组匹配及基础的 NAT 修改 为什么硬件加速导致主动模式失败 控制连接建立：客户端连接服务器的 21 端口，可能被硬件加速接管 发送 PORT 命令： 正常流程：包应进入 Netfilter 的 LOCAL_OUT 链，被 ALG 捕获 HWNAT 流程：包可能直接绕过 Netfilter 的软件钩子 结果： ALG 没看到包：因为包没经过 Netfilter，ALG 模块没有机会运行 内容未修改：服务器收到的PORT命令中依然是内网 IP Expectation 未创建：Conntrack 表中没有生成\u0026quot;预期\u0026quot;条目 ALG FTP SIP PPTP RTSP\n📌 总结：为什么这些协议需要 ALG?\n这类协议之所以让普通 NAT 路由器“头疼”，是因为它们都具备一个共同特征：控制流与数据流分离，且数据流的连接信息（IP/Port）被放在应用层载荷中。\n📺 RTSP（实时流协议） 场景：网络摄像头监控、流媒体点播。 过程：客户端通过 554 端口（控制通道）发送 SETUP 指令，告诉服务器：“请通过 UDP 协议，把视频流发到我的 内网IP 和 随机端口（如 5000）”。 问题：普通 NAT 只修改 IP 包头，不修改包体。服务器收到指令后，拿着那个不可达的“内网IP”去发视频流，导致黑屏或花屏。 ALG 作用：RTSP ALG 会深度解析 SETUP 指令，把包体里的“内网IP:端口”修改为“公网IP:映射端口”，确保视频流能正确打洞进入内网。 📂 FTP（文件传输协议 - 主动模式） 场景：文件上传下载。 过程：客户端连接服务器 21 端口发指令：“我要下载文件，请连接我的 内网IP 的 2000端口 发送数据”。 问题：服务器尝试连接那个内网 IP，连接超时，文件传输失败。 ALG 作用：FTP ALG 拦截 PORT 指令，修改其中的 IP 和端口，并临时在防火墙上打开对应的数据端口。 📞 SIP / H.323（VoIP 网络电话） 场景：IP 电话、视频会议。 过程：信令包（SDP 协议）里写着：“请把语音流（RTP）发到我的 内网IP 的 3000端口”。 问题：对方听到了拨号音，但一旦接通，语音包发往内网 IP，导致单通（一方听不到声音）或静音。 ALG 作用：SIP ALG 修改 SDP 包里的 IP 地址，保证语音流能双向互通。 🌐 PPTP / IPSec（老旧 VPN 协议） 场景：远程办公接入公司内网。 过程：在建立隧道握手时，数据包里携带了用于校验或加密的特定参数（如 Call ID 或 SPI）。 问题：普通 NAT 可能会改变这些参数的校验和，或者因为 PPTP 没有端口号（使用 GRE 协议）而导致 NAT 无法区分不同用户的流量，导致VPN 拨号卡死或连上后无法上网。 ALG 作用：专门识别 GRE 包或 IPSec 包，维护一张复杂的会话表，确保加密隧道能穿过 NAT。 普通 NAT 就像一个只会看信封的邮递员，它只负责把信封上的“收件人/发件人”（IP头）改掉，不管信里写了什么。 ALG 就像一个会拆信检查的秘书。当它发现信里写着“请联系我旧的电话号码（内网IP）”时，它会把信纸上的号码涂改成“新的电话号码（公网IP）”，然后再把信发出去。\nSTUN TUN 和 ALG 虽然都是为了解决 NAT 穿透问题，但两者的实现思路截然不同：ALG 依赖路由器“帮忙修改”，而 STUN 则是客户端“自力更生”。\nSTUN 的全称是 NAT 会话穿透工具。你可以把它想象成一个位于公网的“回声服务器”或“镜子”。\n🤔 核心原理：借一双“公网的眼睛”\n位于内网的设备（比如你的电脑）通常只知道自己的内网 IP（如 192.168.1.100），它完全不知道自己在公网上的“马甲”（公网 IP 和端口）是什么。\nSTUN 的工作流程非常简单，就像照镜子： 询问（Binding Request） 内网客户端向公网的 STUN 服务器发送一个数据包，问：“你看我是谁？” NAT 转换（关键一步） 数据包经过路由器（NAT）时，路由器会给它盖上自己的“公章”：把源 IP 换成公网 IP，源端口换成一个映射端口。 回答（Binding Response） STUN 服务器收到包后，看到的就是路由器盖完章的地址。 它把这个地址写在回信里，发回给客户端：“我看你的地址是 X.X.X.X:Y。” 获知公网地址 客户端收到回信，就知道了：“原来我在公网上的门牌号是 X.X.X.X:Y！”\n🔄 客户端拿到地址后做什么？ 客户端一旦知道了自己的公网映射地址，它就会把这个地址通过信令服务器（比如 SIP 服务器或 WebRTC 的信令服务器）告诉对方。\n对方拿到这个公网地址后，直接往这个地址发数据。 路由器收到数据，查 NAT 表，转发给内网客户端。 穿透成功！ ⚠️ STUN 的致命弱点：对称型 NAT\nSTUN 虽然好用，但它搞不定一种叫对称型 NAT的路由器。\n锥形 NAT（STUN 能搞定）：不管你发给谁（STUN 服务器还是对方电脑），路由器都给你用同一个公网端口。\n对称型 NAT（STUN 搞不定）：\n你发给 STUN 服务器，路由器开了端口 1000。 你发给对方电脑，路由器觉得目标变了，于是开了个新端口 2000。 结果：STUN 告诉你端口是 1000，你告诉对方用 1000。但对方发数据过来时，路由器的 1000 端口根本没开（或者是给 STUN 专用的），导致连接失败。 解决办法：当 STUN 失败（遇到对称型 NAT）时，通常就需要 TURN（中继）登场了——既然直连不通，那就找个公网服务器帮忙转发数据吧。\n","date":"2026-04-10T18:58:08+08:00","permalink":"https://charles-7777.github.io/p/ftp%E4%B8%BB%E5%8A%A8%E6%A8%A1%E5%BC%8Fnat%E7%A9%BF%E9%80%8F%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/","title":"FTP主动模式NAT穿透原理解析"},{"content":" 在操作系统的世界里，中断（Interrupt） 是一个至关重要的概念。如果没有中断系统，CPU 将不得不用低效的“轮询”方式来处理外界事件，这不仅浪费资源，还无法实现真正的现代多任务操作系统。\n中断分为两类：硬中断（Hardware Interrupt） 和 软中断（Software Interrupt）。虽然它们只有一字之差，但产生的原因、处理机制以及应用场景却有着本质的区别。\n什么是硬中断（Hardware Interrupt） 定义与来源 硬中断是由计算机硬件的外设（如网卡、磁盘控制器、键盘、鼠标、时钟等）发出的物理电信号。每个设备或设备集都有自己专属的 IRQ（Interrupt Request，中断请求线）。 当硬件设备需要 CPU 的关注时（例如网卡接收到了一个数据包，或者你在键盘上按下一个键），它会通过 IRQ 直接向 CPU 的中断控制器发送一个信号。\n硬件驱动的本质：内核子程序而非独立进程 在理解硬中断前，必须澄清一个重要概念：硬件驱动是内核中的一个子程序，而不是一个独立的进程。\n不是进程：驱动代码平时不占用 CPU 时间片，它不独立运行，也不会出现在你的任务管理器里等待被操作系统调度。 是子程序：驱动代码“寄生”在内核空间里。只有当中断发生时，CPU 才会暂停手头的当前工作，借用当前进程的上下文，直接跳进内核去执行那段驱动代码。 如果驱动是一个独立的进程，那么中断发生时，就需要 CPU 先去调度它，等把它“睡醒”了再去跑，这样的反应速度就实在太慢了。这正是硬中断使用子程序（函数调用）而非进程去处理的根本原因——为了追求极致的快，且绝不依赖调度器。\n典型场景：键盘敲击与极速响应 以我们日常敲击键盘为例，整个底层极速流程如下： 键盘被按下 $\\rightarrow$ 键盘发出 IRQ 1 硬件信号 $\\rightarrow$ CPU 强制暂停手头工作 $\\rightarrow$ 查阅 IDT 表（中断描述符表） $\\rightarrow$ 找到 IRQ 1 对应的内核子程序入口 $\\rightarrow$ 执行键盘驱动代码（仅仅是将字符读进内存缓冲区） $\\rightarrow$ 驱动退出，返回刚才暂停的程序继续运行。\n处理机制 硬中断的发生是异步的，完全不可预测。它的处理机制极具“霸道”属性：\n直接打断 CPU：硬中断可以直接强制中止 CPU 当前正在执行的代码。 上下文切换：立刻挂起当前进程的状态，转入内核预先注册的中断处理程序（Interrupt Service Routine, ISR）。 单核处理：在多核系统中，一个硬中断通常只能打断一个 CPU 核心（特殊通道除外）。 什么是软中断（Software Interrupt）？ 定义与来源 在中文技术语境里，“软中断”这个词经常被混用，至少有两种常见含义：\n广义的软件中断：由指令、异常或系统调用触发的陷入，比如 x86 上的 INT 指令。 Linux 内核里的软中断：一种由内核安排的延后处理机制，用来承接硬中断之后不适合立刻做完的工作。 本文后面讨论的“软中断”，主要采用第二种语义，也就是 Linux 内核里的 softirq。因为网络收包、定时器、部分块设备后处理这类典型场景，说的正是这种“硬中断先抢响应，软中断再接着干活”的分工。\n处理机制 软中断可以理解为“内核安排的下半场”，它和硬中断之间的关系不是谁替代谁，而是谁先谁后、谁轻谁重：\n硬中断先做最急的事：例如确认设备、应答中断、拿到数据已经到达的事实。 较重的后续工作延后：例如把网络包交给协议栈、把数据塞进 socket 队列、唤醒等待进程，这些通常放到软中断阶段处理。 仍然运行在内核态：软中断不是普通用户进程，也不是任务管理器里能看到的独立应用；它本质上仍是内核代码。 不靠外部 IRQ 直接抢断 CPU：它不是外设拿电信号强行打断 CPU，而是由内核在硬中断返回后或检查到 pending softirq 时尽快执行。 也要求短小高效：软中断虽然比硬中断“宽松”一点，但通常仍不适合做会睡眠、会阻塞太久的工作；如果工作量太大，Linux 往往会交给 ksoftirqd 这样的内核线程继续消化。 不是内核先预判这次 softirq 很短还是很长，再决定“现场执行”还是“交给 ksoftirqd”。 更准确的流程是：硬中断先标记 softirq，硬中断返回后内核通常会先马上尝试处理一轮；如果这一轮很快就做完了，那就直接在当前这条执行路径里消化掉；如果发现待处理的工作过多、超过本轮预算，或者中断触发过于频繁，内核才会唤醒 ksoftirqd 去继续处理剩余工作。\n换句话说，更准确的理解应当是：先现场处理一轮，处理不完再交给 ksoftirqd 收尾。而且即使进入了 ksoftirqd，也不代表 softirq 就变成了可以随意睡眠、随意阻塞的普通进程逻辑；真正需要长时间阻塞或睡眠的工作，通常应该继续交给更合适的机制，例如 workqueue。\n典型场景：网卡收包中的硬中断与软中断协作 再看一个更贴近服务器场景的例子：网卡收到一个数据包时，真正发生的并不是“网卡驱动把网页直接显示出来”，而是一次典型的硬中断与软中断分工协作。\n它的底层流程可以概括为： 网卡收到以太网帧 $\\rightarrow$ 网卡通过 DMA 先把数据放进接收缓冲区或环形队列 $\\rightarrow$ 网卡发出硬中断 IRQ $\\rightarrow$ CPU 暂停当前任务并进入网卡驱动的中断处理函数 $\\rightarrow$ 驱动只做最紧急的事，比如确认中断、记录有新包到达、必要时暂时关闭过于频繁的中断 $\\rightarrow$ 标记 softirq 或进入 NAPI 轮询路径 $\\rightarrow$ 硬中断处理函数迅速返回 $\\rightarrow$ 内核在稍后的软中断上下文中把数据包交给 IP、TCP/UDP 等协议栈继续解析 $\\rightarrow$ 数据进入 socket 接收队列 $\\rightarrow$ 最后再唤醒等待数据的应用进程，比如 Nginx、浏览器或你自己的服务程序。\n这个例子里最关键的是分工：\n硬中断阶段：目标是“先确认包到了，再尽快退出”，避免长时间卡住 CPU。 软中断阶段：目标是“继续把包往协议栈和应用层方向推”，完成更重但没必要在第一时间做完的工作。 这也解释了为什么网络栈常常离不开软中断：如果把协议解析、路由、过滤、socket 入队这些事全塞进硬中断里，CPU 会被中断处理占住太久，系统整体延迟反而会更差。 在流量不高时，这些后续处理可能就在硬中断返回后很快完成；而在流量很大、包到得很密的时候，一轮 softirq 处理不完，剩下的工作就可能转给 ksoftirqd 继续消化。\n顺便区分：系统调用不等于这里说的软中断 很多资料会把系统调用也宽泛地叫作“软件中断”，这是从“软件触发陷入内核”的角度去说的，并不算完全错。但如果本文讨论的是 Linux 里和硬中断配对出现的软中断，那么它更准确地指 softirq 这一类下半部机制，而不是 read、write 这类系统调用本身。\n硬中断 vs 软中断：核心区别对比卡 为了更直观地理解，我们可以从以下几个维度进行对比：\n比较维度 硬中断 (Hardware Interrupt) 软中断 (Software Interrupt) 触发主体 外部硬件设备（磁盘、网卡、键盘等） 内核标记的延后处理任务（常由硬中断或内核代码触发） 触发时机 异步（随时发生，随机不可预测） 通常在硬中断返回后或内核检查到 pending 时执行 对 CPU 的影响 强制物理打断 CPU 的当前执行流 不依赖外部电信号抢断，而由内核在合适时机执行 基本性质 依托内核子程序（极速借用上下文） 依托内核下半部机制（延后处理后续工作） 执行优先级 极高，必须立即响应（否则可能丢失数据） 仍然较高，通常紧跟在硬中断之后尽快处理 流程差异 硬件信号 $\\rightarrow$ CPU $\\rightarrow$ 查表 $\\rightarrow$ 执行内核设备子程序 硬中断或内核代码置位 $\\rightarrow$ 内核处理 softirq $\\rightarrow$ 执行后续协议栈或内核逻辑 常见问题深度剖析（Q\u0026amp;A） 软中断也是由设备驱动程序处理数据的吗？ 答：通常是“驱动 + 内核子系统”一起完成。 以网络收包为例，驱动在硬中断阶段先负责确认设备状态、拿到新包、触发 softirq 或 NAPI；到了 softirq 阶段，内核往往会去调用驱动注册的轮询函数，把Ring Buffer的包一批批取出来,而真正把数据继续送入 IP、TCP/UDP、socket 队列的，往往是后续的内核网络协议栈代码。也就是说，驱动负责开门和搬运第一步，后面的分发和处理则常常交给更通用的内核子系统。\n软中断的操作流程是否比硬中断更短？ 答：从“触发链路”看更短，从“处理内容”看不一定更少。 软中断少了“外设通过 IRQ 线直接打断 CPU”这一步，因此它不需要硬件先发出物理中断信号。但软中断承担的，往往恰恰是硬中断故意延后下来的那部分工作，所以它执行的逻辑未必更少，只是执行时机更可控、更适合做后续处理。\n软中断处理的时机 硬中断先把 softirq 标记出来，内核在硬中断退出后先尝试立即处理一轮；如果这轮处理很快结束，那就当场完成；如果工作量太大、超出预算，或者软中断触发过于频繁，才会把剩余部分交给 ksoftirqd。也就是说，它更像“先试着现场消化，消化不完再转交”。\n硬中断处理程序中能再发生中断吗？ 答：可以发生嵌套。 在硬中断代码执行时，如果没有屏蔽其他中断，更高优先级的硬中断依然会打断当前的中断处理。为了防止死锁和栈溢出，现代操作系统倾向于让硬中断函数变得“极短”，仅仅完成硬件层面的确认和数据搬运，剩下的繁重逻辑全部交由“软中断”去延后排队执行。\n总结 无论硬中断还是软中断，本质上都是操作系统为了打破顺序执行的束缚、实现异步并发机制而设计的精妙底层逻辑。\n硬中断 表现为雷厉风行的“急行军”，通过内核子程序的形制直接切入，绝不等待调度器的安排，保证了系统能够瞬间响应外部硬件的呼唤； 软中断 则是软件系统内部严密的网络与队列，通过延后处理和有条不紊的任务分发，消化了硬中断带来的大量数据。 ","date":"2026-04-10T10:33:25+08:00","permalink":"https://charles-7777.github.io/p/%E7%A1%AC%E4%B8%AD%E6%96%AD%E4%B8%8E%E8%BD%AF%E4%B8%AD%E6%96%AD/","title":"硬中断与软中断"},{"content":" 在服务端开发中，我们往往只需要调用简单的 recv() 或 read() 就可以自底向上获取网络数据。但在这些简单的 API 背后，隐藏着 Linux 极其精密复杂的网络子系统。\n收包七步走 可以将整个收包链路宏观地归纳为以下核心阶段：\n数据帧到达：数据帧从网络链路进入网卡物理介质。 DMA 搬运数据：网卡利用 DMA 技术将网络包写入系统内存 (Ring Buffer)。 硬中断触发：网卡向 CPU 发送硬件中断信号，通知有新数据到达。 硬中断响应：CPU 暂停当前任务，执行硬中断函数，随后发出软中断请求。 软中断处理 (ksoftirqd)：专门响应软中断的内核线程 ksoftirqd 开始工作，调用网卡驱动注册的 poll 轮询函数。 封装 sk_buff：网卡驱动从 Ring Buffer 摘下数据，将其封装为内核认识的 sk_buff 数据结构。 协议栈处理：经过网络层（IP）、传输层（TCP/UDP）解析，最终把数据追加到目标 Socket 的接收队列中，等待用户进程读取。 数据包的奇幻漂流 网卡接客与 DMA 搬运 当网络包通过光纤或双绞线到达网卡（NIC）时，网卡的 MAC 模块会对其进行初步过滤校验（比如检查 MAC 地址、FCS 校验等）。验证通过后，网卡并不会立刻打扰 CPU，而是通过 DMA (Direct Memory Access) 技术，直接将这些数据原封不动地写入到由系统预先分配好的内存空间 —— 即 Ring Buffer (环形缓冲区) 中。\n这里 Ring Buffer 的内存实际上是由内核在驱动初始化时分配好的，网卡知道对应的物理地址，这就是真正的“零拷贝”（硬件层到内存层）。\n硬中断 (Hard IRQ) 唤醒 CPU 网卡将数据放到 Ring Buffer 后，通过 PCIe 线上报一个硬件中断（Hard IRQ）通知 CPU 有活儿干了。\nCPU 收到中断后，会打断当前进程上下文，转而进入中断上下文，去执行网卡驱动注册的硬中断处理函数（通常对应类似 ixgbe_msix_clean_rings 的函数）。 因为硬中断会屏蔽其他中断，为了保证系统响应速度，这部分代码必须极短、极快。内核网络栈采用的做法是：仅仅确认一下中断、清除硬件中断标志，然后触发一个软中断 (Soft IRQ)。\n硬中断的极速退出：触发软中断的核心动作非常轻量，本质上仅仅是将 NET_RX_SOFTIRQ 的标志位置位。完成置标后，硬中断处理函数立刻 return 结束，以最快速度恢复系统的中断挂起响应能力。 软中断的无缝衔接：在离开硬中断上下文的底层函数 irq_exit() 中，内核会探查是否有处于挂起状态（已标记）的软中断需要处理。一旦发现，就会在退出硬中断的尾声，直接切入并执行软中断逻辑。 源码追踪： 调用 __napi_schedule() 将当前的 NAPI (New API) 结构体挂载到当前 CPU 的 softnet_data.poll_list 上，随后触发 NET_RX_SOFTIRQ 软中断。\n1 2 3 4 static inline void __napi_schedule(struct napi_struct *n) { list_add_tail(\u0026amp;n-\u0026gt;poll_list, \u0026amp;sd-\u0026gt;poll_list); __raise_softirq_irqoff(NET_RX_SOFTIRQ); // 仅仅是置标志位 } ksoftirqd 与 NAPI 轮询接管 (Soft IRQ) ksoftirqd/x 是什么角色？ ksoftirqd/x 它是一个内核线程（每个 CPU 核心一个，x 为核心编号）。 在网络正常或低负载时，软中断往往会在刚才提到的硬中断退出阶段 (irq_exit()) 顺手被处理完。但若是遇到高并发大流量，软中断处理时间过长，为了防止系统一直处于中断上下文而“饿死”所有普通的用户进程，内核会主动中断顺手处理的行为，转而唤醒对应的后台内核线程 ksoftirqd/x，让它去接盘处理剩下的网络包。 这种设计的精妙之处在于：将不可抢占的中断上下文，转移到了受系统统一调度的内核线程上下文中处理，从而保证了 CPU 的公平分配。\n当 ksoftirqd 被调度执行时，此处软中断真正的处理入口为 net_rx_action()。它会去遍历刚刚硬中断放入 poll_list 的设备，并调用它们各自注册的 poll() 函数（即 NAPI 机制）。NAPI 机制的魅力在于：在网络高负载时，通过一次中断转为连续的轮询处理，以此来避免网络风暴带来的中断过载。\n源码追踪：\n1 2 3 4 5 6 7 8 static __latent_entropy void net_rx_action(struct softirq_action *h) { // 遍历 poll_list 上的 napi 结构 while (!list_empty(\u0026amp;list)) { struct napi_struct *n = list_first_entry(\u0026amp;list, struct napi_struct, poll_list); // 调用网卡驱动特定的 poll 函数 (如 ixgbe_poll) work = n-\u0026gt;poll(n, weight); } } 生成灵魂数据结构：sk_buff 在网卡驱动特定的 poll() 函数内部（比如 e1000 网卡的 e1000_clean_rx_irq），驱动会从 Ring Buffer 中读取之前 DMA 写入的数据帧。\n此时，内核会为这段数据分配一个核心数据结构：sk_buff (Socket Buffer)。sk_buff 是 Linux 网络内核的“通用货币”，无论是在 MAC 层、IP 层还是 TCP 层，它的指针通过加减头部空间的方式在各个协议层中穿梭，避免了层与层之间数据的内存拷贝。\n接下来，利用 napi_gro_receive() （通常伴随 GRO：Generic Receive Offload，合并多个零散包）或者直接 netif_receive_skb() 将 sk_buff 喂给上层的网络协议栈。\n协议栈的层层剥茧与 Socket 投递 数据包进入 netif_receive_skb() 后，内核会根据 MAC 头部的以太网类型（比如 ETH_P_IP）将包交给对应的网络层 (IP 层) 处理。\nIP 层 (ip_rcv)：\n验证 IP 头部、TTL 等。 查询路由表，判断这个包是给本机的，还是需要转发的（Netfilter / iptables PREROUTING 链也是在此介入）。 如果是送往本机，调用 ip_local_deliver()，最终剥离 IP 头，根据上层协议找到 UDP 或 TCP 处理函数。 传输层 (TCP: tcp_v4_rcv / UDP: udp_rcv)： 以 TCP 为例：\nTCP 协议栈会根据包里的 \u0026lt;源 IP，源端口，目的 IP，目的端口\u0026gt; 查找到对应的 socket 实例。 解析 TCP 报头、确认序列号、滑动窗口等复杂逻辑。 核心处理函数 tcp_rcv_established() 会把除去所有头部后的纯净 Payload 数据放到 socket 的接收队列 (Receive Queue) 中。 源码追踪：投递到 Socket 队列中并唤醒用户进程\n1 2 3 4 5 // 将准备好的数据加入 sk_receive_queue skb_queue_tail(\u0026amp;sk-\u0026gt;sk_receive_queue, skb); // 调用对应的回调唤醒处于阻塞态的用户进程 (如 epoll_wait / recv) sk-\u0026gt;sk_data_ready(sk); 最终：用户态的进程苏醒 当 sk_data_ready() 被调用时，如果进程之前因为调用 recv() 或 epoll_wait() 处于睡眠状态，它现在就会被唤醒。此时进程调度器再次选中该用户进程，从内核空间将 socket 接收队列里的数据拷贝到用户态的 Buffer 中。至此，整个网络包的接收流程才算圆满结束！\n总结思考 DMA + CPU 缓存协同：规避了最外设到内存的 CPU 拷贝开销。 NAPI（中断 + 轮询）：用“硬中断”保证低延迟通知，用“软中断+轮询（poll）”处理高并发网络包，完美平衡了资源消耗与吞吐量。 sk_buff 元数据控制：各层协议处理过程中，数据体本身的内存不会发生流动拷贝，仅仅是首部指针在变化。 十年码农内功：网络收包详细过程\nlinux6.9内核网络篇-收包源码分析 Linux内核网卡收包流程，以及cpu、设备关开中断情况\n","date":"2026-04-09T20:33:31+08:00","permalink":"https://charles-7777.github.io/p/linux-%E6%94%B6%E5%8C%85%E6%B5%81%E7%A8%8B/","title":"Linux 收包流程"},{"content":"TCP 本身没有“包”的概念 在我们深入讨论之前，最重要的一点是：TCP 是一种面向字节流（Stream-Oriented）的协议，它本身没有“粘包”问题，因为它根本不认识“包”。\n我们可以把 TCP 连接想象成一根两端对等的水管。发送方（A端）往水管里倒水，可以一次倒一桶，也可以连续倒很多杯。对于接收方（B端）来说，它只能看到一股连续不断的水流从水管里流出，它并不知道A端是分几次、每次用多大的容器倒的水。\nTCP 的核心承诺是：\n可靠性：保证所有字节都会被对方收到。 有序性：保证字节的顺序与发送时的顺序一致。 但它不承诺保留发送方应用层写入操作的边界。 换句话说，你在发送端调用了三次 send()，每次发送一个“消息包”，接收端完全可能通过一次 recv() 就接收到了这三个“消息包”的全部内容（粘包），或者只接收到第一个“消息包”的一部分（拆包）。\n因此，所谓的“粘包”或“拆包”问题，并非 TCP 的缺陷，而是应用层在处理无边界的字节流时遇到的挑战。\n为什么会“粘”在一起？ 既然是应用层的问题，那为什么会产生这种现象呢？主要有以下几个内核层面的原因：\nNagle 算法：为了提高网络效率，TCP 协议栈的 Nagle 算法会等待一小段时间，将多个小的发送请求合并成一个大的 TCP 段（Segment）再发送出去。 TCP 接收缓冲区：接收方收到的 TCP 段会存放在接收缓冲区。当应用层调用 recv() 时，如果缓冲区里已经到达了多个 TCP 段的数据，recv() 可能会一次性读取出来。 MSS/MTU 限制：如果应用层要发送的数据大于最大段大小（MSS），TCP 会自动将其拆分成多个 TCP 段。接收方需要多次读取才能获得一个完整的应用层消息。 内核 vs 应用层：明确分工\n内核负责：丢包重传、乱序重排、重复去重、流量控制、拥塞控制。内核把不可靠的 IP 网络，封装成可靠的 TCP 字节流。 应用层负责：消息边界（解决粘包）、业务逻辑、加密压缩。 一句话：内核保证可靠传输，应用层定义消息边界。\n问题的本质：如何在字节流中定义消息边界 既然 TCP 是无边界的，那么解决方案的核心就在于：发送方和接收方必须在应用层共同遵守一个协议，用来清晰地定义一条消息从哪里开始，到哪里结束。\n一旦接收方知道了消息的边界，它就可以从 TCP 字节流中准确地分割出一条条完整的消息。下面是三种最经典的实现方案。\n三大经典应用层解决方案 固定长度协议 (Fixed-Length Framing) 这是最简单直接的一种方法。\n原理：发送方和接收方约定，每一条应用层消息都具有固定的长度，例如 64 字节。 处理流程： 发送方：将消息封装成 64 字节。如果消息不足，则用特殊字符（如空格、\\0）填充。 接收方：每次都从 TCP 流中读取 64 字节。一旦读满，就认为这是一个完整的消息。 优点：实现极其简单，没有复杂的解析逻辑。 缺点：灵活性极差，会造成带宽浪费（当消息远小于固定长度时），也无法处理大于固定长度的消息。 适用场景：适用于消息长度恒定不变的特定场景（如某些传感器数据）。 特殊分隔符协议 (Delimiter-based Framing) 这种方法通过一个特殊的标记来划分消息。\n原理：发送方和接收方约定一个不会在正常消息内容中出现的特殊字符或字符串序列（例如 \\r\\n）作为消息的边界。 处理流程： 发送方：在每条消息的末尾添加这个特殊的分隔符。 接收方：不断从 TCP 流中读取数据并进行扫描，直到找到分隔符为止。从上一个分隔符到当前分隔符之间的数据，就是一条完整的消息。 真实案例：HTTP/1.1 协议使用 \\r\\n 作为每行请求头/响应头的分隔符，并使用一个空的 \\r\\n\\r\\n 来标记整个头部的结束。 优点：实现相对简单，灵活性比固定长度协议高很多。 缺点： 转义问题：如果消息内容本身包含了分隔符，就必须对内容中的分隔符进行转义，否则会导致消息被错误解析。 效率问题：接收方需要逐字节扫描数据以查找分隔符，当消息很大时可能会有性能开销。 自定义消息结构：长度前缀协议 (Length-Prefixed Framing / TLV) 这是现代网络编程中最常用、最灵活、最可靠的方案，是解决粘包问题的工业标准方案。\n原理：在每条可变长度的消息数据（Value）前，附加一个固定长度的头部。这个头部中包含类型（Type） 和长度（Length） 字段，明确地说明了消息类型和紧随其后的 Body 部分有多长。因此该方案也常被称为 TLV (Type-Length-Value) 格式。 优点： 边界清晰：通过长度前缀，可以精确地知道每条消息的边界，无需扫描内容。 高效灵活：可以传输任意长度的数据，没有数据浪费，解析效率高。 扩展性强：Header 中的 Type 字段可以区分消息类型，Length 字段便于解析，非常便于未来对协议进行扩展。 缺点：实现上比分隔符和固定长度协议稍复杂，需要处理好半包读取。 方案对比总结 方案 优点 缺点 适用场景 固定长度 实现极简单 空间浪费，长度难选 所有消息等长的极简场景 分隔符 可读性好，直观 需要转义，扫描略慢 文本协议（如 HTTP, Redis） TLV (长度前缀) 灵活、高效、二进制安全 实现稍复杂 生产环境、高性能场景首选 为什么 UDP 没有粘包问题？ 这是一个很好的对比问题。UDP 是数据报协议，不是流式协议。\n原因：UDP 的头部有一个 16位的长度字段，明确指示了这个数据报的字节数。内核据此知道每个数据报的起止位置，因此保留消息边界。\n结果：在 UDP 中，每次 sendto() 调用都严格对应一次 recvfrom() 调用，绝对不会发生粘包。但代价是 UDP 不保证可靠性（可能丢包、乱序）。\n总结 “TCP粘包”：问题不在于 TCP 协议本身，而在于它作为字节流协议不保留应用层消息边界。这是特性，不是缺陷。\n解决思路清晰：解决粘包问题的唯一方法，是在应用层定义消息边界。\n三种主流方案：固定长度、特殊分隔符、长度前缀（TLV）。其中 TLV 方案最通用、最强大，是现代网络编程的首选。\n明确分工:\n内核 (TCP协议栈) 负责：可靠传输（丢包重传、乱序重排、流量控制）。 应用层 (你的代码) 负责：消息边界（使用 TLV 等方案）和业务逻辑。 ","date":"2026-04-07T20:30:04+08:00","permalink":"https://charles-7777.github.io/p/tcp%E7%B2%98%E5%8C%85/","title":"TCP粘包"},{"content":"https://www.python.org/ftp/python/3.11.7/python-3.11.7-embed-amd64.zip 下载到D盘解压\nhttps://bootstrap.pypa.io/pip/get-pip.py 下载到D:\\python-3.11.7-embed-amd64\n将D:\\python-3.11.7-embed-amd64 和 D:\\python-3.11.7-embed-amd64\\Scripts 添加到系统环境变量 python get-pip.py\n修改D:\\python-3.11.7-embed-amd64\\python311._pth文件，将import site前的#号去掉，即让其生效 不注释； pip config debug\n创建D:\\python-3.11.7-embed-amd64\\pip.ini\npip config set site.index-url http://mirrors.aliyun.com/pypi/simple/\npip config set site.trusted-host mirrors.aliyun.com\npip config debug\npython -m site\npip config set site.cache-dir D:\\PipCache\npip cache info\npip 还可以暂时手动指定安装源 pip install open-webui \u0026ndash;no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple pip install open-webui -i https://pypi.tuna.tsinghua.edu.cn/simple\n","date":"2026-04-04T22:47:34+08:00","permalink":"https://charles-7777.github.io/p/python%E4%BE%BF%E6%90%BA%E7%89%88%E5%AE%89%E8%A3%85/","title":"Python便携版安装"},{"content":"U-Boot的多阶段启动架构 现代嵌入式系统中的U-Boot采用多阶段启动架构，这种设计主要是为了克服SoC内部SRAM容量有限(通常只有几十到几百KB)的限制。U-Boot的启动过程可以分为三个主要阶段：\nBootROM/BL1阶段（SoC内部固化的ROM）\n负责最基础的硬件初始化 配置内部时钟、设置复位向量 初始化用于加载下一阶段代码的存储接口 从预设的启动介质(如SD卡、eMMC、SPI Flash等)加载SPL（Secondary Program Loader次级程序加载器）到SRAM 将控制权移交给SPL SPL/BL2阶段（在芯片内部SRAM中运行）\n初始化DDR控制器和外部DRAM 检测内存大小和类型，建立初步内存映射 将完整的U-Boot镜像从外部存储设备加载到DRAM中 完成基本的堆栈设置和C语言运行环境准备 跳转到加载到DRAM中的完整U-Boot执行 U-Boot主体/BL3阶段（在外部DRAM中运行）\n使用C语言完成更复杂的外设初始化 加载和处理设备树(DTB) 读取环境变量设置(如bootargs、bootcmd) 提供交互式命令行界面 从存储介质加载Linux内核镜像 设置内核启动参数并跳转执行内核 这种分阶段的设计使得U-Boot能够在有限的硬件资源下逐步扩展可用内存空间，最终实现完整的引导功能。\nU-Boot启动过程详解 CPU初始化和异常向量设置 U-Boot的启动始于汇编代码的执行，这是因为在系统刚上电时，CPU处于一种非常原始的状态，没有栈、没有MMU，C代码无法直接运行。\n1 2 3 reset: bl _reset /* 复位入口 */ b . 复位入口处的代码会执行以下关键操作：\n模式切换：关闭中断，切换到SVC模式(特权模式)，确保执行环境稳定\n1 CPSR = 0x1F /* 进入SVC32模式，禁止FIQ/IRQ中断 */ 异常向量表设置：初始化中断/异常向量表，处理未定义指令、中断等事件\n1 VBAR = 0x87800000 /* 设置向量表地址 */ CP15寄存器初始化：关闭MMU和Cache，清除TLB\n1 2 3 4 MRC p15, 0, r0, c1, c0, 0 /* 读取SCTLR */ ORR r0, r0, #0x00000001 /* 设置V位，关闭MMU */ ORR r0, r0, #0x00000010 /* 设置C位，关闭Cache */ MCR p15, 0, r0, c1, c0, 0 /* 写回SCTLR */ 这一阶段的核心目标是为后续的C代码执行创建一个稳定、可预测的运行环境，确保在没有MMU和Cache的情况下，程序能够正确地访问物理地址空间。\n关键硬件初始化 在CPU基本配置完成后，U-Boot会执行一些关键硬件初始化操作：\n时钟配置：设置CPU核心、总线及外设时钟，确保各部分以正确频率运行\n1 2 /* 配置主频 */ cpu_set_freq(800000000); 内存控制器初始化：配置DRAM控制器，使物理内存可用(如DDR初始化)\n1 2 /* 初始化DDR控制器 */ ddr_init(); Cache和MMU管理：关闭Cache和MMU，避免初始阶段因地址映射导致的错误\n1 2 /* 关闭缓存和MMU */ mmu_cacheoff(); 这些操作为后续的代码重定位和更复杂的系统初始化奠定了基础。值得注意的是，不同硬件平台的初始化细节会有很大差异，但核心思想保持一致。\n代码重定位(Relocation) 由于SPL通常运行在SRAM中，而SRAM容量有限，U-Boot需要将自身代码从Flash/ROM复制到更大的DRAM空间执行：\n1 2 3 4 5 6 7 8 9 10 11 /* 代码重定位 */ bl board_init_f ... board_init_f: /* 复制代码到DRAM */ bl image_copy /* 更新全局数据指针 */ ldr r0, _start str r0, [sp, #GD_SIZE - 12] /* 跳转到重定位后的代码 */ bl _start 代码重定位过程包括：\n复制到RAM：将U-Boot自身从Flash/ROM复制到DRAM的高地址端(如0x8FF00000) 地址重定位：调整全局变量和函数指针，确保重定位后代码能正确访问数据 BSS段清零：清除未初始化全局变量区域，避免随机值影响逻辑 代码重定位是U-Boot启动过程中的一个关键转折点，它使U-Boot能够利用外部大容量DRAM，从而执行更复杂的初始化任务。\nC语言环境准备 重定位完成后，U-Boot需要建立完整的C语言运行环境：\n1 2 3 4 5 6 7 8 9 /* 初始化堆栈 */ gd-\u0026gt;sp = (long)CONFIG_SYS_INIT SP_ADDR; /* 初始化全局数据 */ gd-\u0026gt;bd = (struct bd_info*)(gd-\u0026gt;sp - sizeof(struct bd_info)); gd-\u0026gt;sp -= sizeof(struct bd_info); /* 设置堆栈指针 */ __asm__ __volatile__ (\u0026#34;mov sp, %0\u0026#34; : : \u0026#34;r\u0026#34;(gd-\u0026gt;sp)); 这一阶段主要包括：\n堆栈设置：初始化堆栈指针，为C代码提供运行环境 全局数据结构：创建并初始化global_data结构，存储系统关键信息 内存管理初始化：设置内存分配器，为后续内存操作做准备 C语言环境准备完成后，U-Boot就可以使用更高级的C语言功能继续初始化系统。\n板级外设初始化 在C语言环境完全建立后，U-Boot会执行板级外设初始化：\n1 2 3 4 5 6 7 8 9 10 11 12 void board_init_f(int BootDRAM) { /* 初始化硬件 */ cpu_init(); /* 初始化外设 */ board_init(); /* 初始化时钟 */ clock_init(); /* 初始化环境变量 */ env_init(); /* 其他初始化... */ } 板级初始化通常包括：\n串口调试：初始化UART，启用串口输出调试信息(如printf)\n1 2 /* 初始化串口 */ serial_init(); 存储设备初始化：初始化SD卡、NAND Flash等存储控制器\n1 2 3 /* 初始化存储设备 */ nand_init(); 闪存编程擦除：这是为系统准备启动过程所必需的[(deep_research_source_group_web_11)]。 设备树加载：解析设备树(DTB)，为内核提供硬件描述信息\n1 2 /* 加载设备树 */ fdt_load(); 环境变量加载：从Flash或EEPROM读取环境变量(如bootargs、bootcmd)\n1 2 /* 加载环境变量 */ env_load(); 板级外设初始化是U-Boot启动过程中最复杂、最具平台差异性的部分，它为系统提供了基本的输入输出能力和存储访问能力。\n环境变量加载机制 环境变量是U-Boot的重要特性，它允许用户配置启动参数：\n1 2 3 4 5 6 7 8 9 10 11 int env_load(void) { struct env_driver *drv; /* 根据启动介质获取对应的环境驱动 */ drv = env_driver_lookup(ENVOP辅加载, 0); /* 调用驱动的加载函数 */ if (drv-\u0026gt;load()) return 0; /* 如果加载失败，返回错误 */ return -ENODEV; } 环境变量加载过程包括：\n存储位置确定：根据启动介质类型(SPI Flash、NAND等)选择对应的驱动 环境变量读取：从存储介质读取环境变量数据 数据校验：通过CRC校验确保环境变量数据完整 环境变量解析：将读取的二进制数据解析为键值对形式 环境变量为U-Boot提供了灵活性和可配置性，用户可以通过bootcmd环境变量指定自动启动命令，通过bootargs设置内核启动参数。\n内核加载与启动参数设置 完成系统初始化后，U-Boot会加载Linux内核镜像：\n1 2 3 4 5 /* 加载内核镜像 */ load_image(\u0026#34;kernel\u0026#34;, \u0026amp;kernel_addr, \u0026amp;kernel_size); /* 设置内核启动参数 */ set Bootargs(\u0026#34;console=ttyS0,115200 root=/dev/mmcblk0p2\u0026#34;); 内核加载和启动参数设置包括：\n内核镜像加载：从指定存储介质加载内核镜像到内存中\n1 2 /* 加载zImage */ loadz(\u0026#34;kernel\u0026#34;, \u0026amp;kernel_addr, \u0026amp;kernel_size); 设备树加载：加载设备树二进制文件(DTB)到内存\n1 2 3 4 /* 设置设备树地址 */ setenv(\u0026#34;fdtaddr\u0026#34;, \u0026#34;0x48000000\u0026#34;); /* 加载设备树 */ load(\u0026#34;dtb\u0026#34;, \u0026#34;0x48000000\u0026#34;, \u0026#34;imx6ull-som-evk.dtb\u0026#34;); 启动参数配置：设置内核启动参数(bootargs)\n1 2 /* 设置内核启动参数 */ setenv(\u0026#34;bootargs\u0026#34;, \u0026#34;console=ttyS0,115200 root=/dev/mmcblk0p2 earlycon\u0026#34;); 启动命令执行：通过bootcmd环境变量执行启动命令\n1 2 /* 设置自动启动命令 */ setenv(\u0026#34;bootcmd\u0026#34;, \u0026#34;bootz ${kernel_addr} - ${fdtaddr}\u0026#34;); 内核加载是U-Boot启动过程中的关键步骤，它决定了系统将运行哪个Linux内核版本，以及内核将如何初始化硬件。\nU-Boot的跳转执行机制 完成所有初始化和内核加载后，U-Boot会通过bootz或bootm命令跳转到内核入口：\n1 2 /* 执行bootz命令 */ run Command(\u0026#34;bootz\u0026#34;); 跳转执行过程包括：\n寄存器参数设置：根据ARM架构的启动协议设置寄存器\n1 2 3 4 /* 设置r0为内核入口地址 */ mov r0, #0x80008000 /* 设置r2为设备树地址 */ ldr r2, _fdt_addr 内核启动参数传递：设置内核命令行参数\n1 2 /* 设置内核命令行参数 */ str r1, [r0, #0x100] /* 假设内核参数在0x100偏移处 */ 跳转到内核入口：通过指令跳转到内核程序\n1 2 /* 跳转到内核入口 */ blx r0 在ARM架构中，启动内核时需要遵循特定的寄存器约定：\nr0：内核入口地址 r1：内核大小（可选） r2：设备树地址（如果使用设备树） sp：栈指针，指向内核堆栈 跳转到内核是整个启动流程的最终环节，此时U-Boot已完成所有准备工作，将控制权移交给了Linux内核。\nU-Boot与Linux内核的交互机制 U-Boot与Linux内核之间的交互主要通过以下几种方式实现：\n设备树传递机制 设备树是现代嵌入式系统中描述硬件信息的标准方式：\n1 2 3 4 5 6 7 8 /* 加载设备树 */ load_image(\u0026#34;dtb\u0026#34;, \u0026amp;fdt_addr, \u0026amp;fdt_size); /* 调整设备树缓冲区大小 */ fdtResize(); /* 验证设备树完整性 */ fdtPrint(); 设备树传递过程包括：\n设备树加载：从存储介质加载.dtb文件到内存指定地址 内存分配：为设备树分配足够的内存空间 数据校验：验证设备树的格式和完整性 参数传递：通过bootz命令将设备树地址传递给内核 设备树机制使得同一份内核可以适配多种硬件平台，极大提高了系统的可移植性。\n内核启动参数传递 内核启动参数通过bootargs环境变量传递给内核：\n1 2 3 4 5 /* 设置启动参数 */ setenv(\u0026#34;bootargs\u0026#34;, \u0026#34;console=ttyS0,115200 root=/dev/mmcblk0p2 earlycon\u0026#34;); /* 启动内核 */ bootz kernel_addr - fdt_addr; 启动参数传递机制包括：\n参数格式：空格分隔的键值对字符串 参数内容：包括控制台设置、根文件系统位置、内核调试选项等 传递方式：通过ATAG或直接通过寄存器传递（设备树方式） 内核启动参数是U-Boot与Linux内核之间最重要的交互机制之一，它决定了内核的初始化行为和系统特性。\nU-Boot启动流程的扩展功能 除了基本的启动功能外，U-Boot还提供了许多扩展功能：\n多种启动介质支持 U-Boot支持从多种存储介质加载内核和文件系统：\n1 2 3 4 5 6 7 8 /* 从SD卡加载内核 */ load Image(\u0026#34;kernel\u0026#34;, \u0026#34;MMC\u0026#34;, \u0026amp;kernel_addr); /* 从NAND Flash加载内核 */ load Image(\u0026#34;kernel\u0026#34;, \u0026#34;NAND\u0026#34;, \u0026amp;kernel_addr); /* 从网络TFTP加载内核 */ load Image(\u0026#34;kernel\u0026#34;, \u0026#34;TFTP\u0026#34;, \u0026amp;kernel_addr); 这些支持使得系统具有灵活性和可扩展性，可以适应不同的硬件配置和部署环境。\n网络启动功能 U-Boot提供了完整的网络启动支持，包括：\nDHCP客户端：获取IP地址和网络参数 TFTP客户端：从网络服务器下载内核和文件系统 HTTP/HTTPS客户端：支持通过HTTP协议获取启动文件 PXE引导：完整的PXE引导支持 网络启动功能使嵌入式系统可以实现远程更新和管理，大大提高了系统的可维护性。\n文件系统支持 U-Boot支持多种文件系统，用于加载内核和文件系统：\n1 2 3 4 5 /* 从FAT32文件系统加载内核 */ fatLoad(\u0026#34;MMC\u0026#34;, 0, \u0026#34;zImage\u0026#34;, \u0026amp;kernel_addr, \u0026amp;kernel_size); /* 从ext4文件系统加载内核 */ ext4Load(\u0026#34;NAND\u0026#34;, 0, \u0026#34;uImage\u0026#34;, \u0026amp;kernel_addr, \u0026amp;kernel_size); 支持的文件系统包括FAT16/FAT32、ext2/ext3/ext4、JFFS2等。\nU-Boot启动流程的调试与定制 调试技术 U-Boot提供了丰富的调试功能：\n命令行调试：通过printk和printf输出调试信息 环境变量调试：使用env print查看环境变量，env set修改环境变量 内存操作：使用md查看内存，mm修改内存 寄存器操作：使用mrc和mcr读写ARM协处理器寄存器 断点调试：使用b命令设置断点，c命令继续执行 1 2 3 4 5 6 7 8 /* 设置调试信息级别 */ setenv(\u0026#34;debug\u0026#34;, \u0026#34;1\u0026#34;); /* 查看环境变量 */ env Print; /* 修改启动命令 */ setenv(\u0026#34;bootcmd\u0026#34;, \u0026#34;bootz ${kernel_addr} - ${fdt_addr}\u0026#34;); 调试功能是U-Boot开发和维护过程中不可或缺的工具，它使开发者能够在系统启动的早期阶段就发现和解决潜在问题。\n定制化方法 U-Boot可以通过多种方式定制以适应不同硬件平台：\n配置文件：通过.config文件选择编译选项 板级支持包：实现board.c和lowlevel_init.S等文件 环境变量：通过bootargs和bootcmd等环境变量配置启动行为 自定义命令：添加新的U-Boot命令以扩展功能 1 2 3 4 5 6 7 8 9 /* 配置U-Boot */ make mx6ull_defconfig /* 编译U-Boot */ make -j8 /* 配置板级支持 */ cd board/myboard cp mx6ull_som-evk.c mx6ull_som-evkCustom.c U-Boot的灵活性使其成为嵌入式系统中强大的启动和管理工具，可以适应各种定制化需求。\n总结 U-Boot作为嵌入式Linux系统中不可或缺的引导加载程序，其启动流程涉及从硬件复位到Linux内核启动的全过程。通过多阶段启动架构，U-Boot能够在有限的硬件资源下逐步扩展可用内存空间，最终实现完整的系统初始化和内核引导。\nU-Boot启动流程的核心是逐步构建运行环境：从硬件复位开始，通过汇编代码完成CPU基本配置和异常向量表设置；然后在SPL阶段初始化外部DRAM，为完整U-Boot创建运行环境；最后在完整U-Boot阶段完成外设初始化、环境变量加载和内核引导。\n随着嵌入式系统的发展，U-Boot也在不断演进和增强：\n安全性增强：支持环境变量签名验证、安全启动等安全特性 网络功能扩展：支持更丰富的网络协议和远程启动方式 设备树支持完善：提供更强大的设备树编辑和调试功能 跨平台兼容性提升：支持更多处理器架构和硬件平台 参考链接：\n嵌入式Linux系统启动过程分析\nlinux在arm上的启动流程\n一张图秒懂嵌入式Linux系统的启动流程\n","date":"2026-04-03T00:33:55+08:00","permalink":"https://charles-7777.github.io/p/u-boot%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3/","title":"U-Boot启动流程详解"},{"content":"巴拿赫不动点定理（Banach Fixed-Point Theorem），又称为压缩映射定理，是泛函分析和拓扑学中一个极其基础且强大的工具。它不仅在纯数学中占有核心地位，在应用数学、计算数学、计算科学乃至经济学中都有着极其广泛的应用。\n简而言之，该定理保证了在特定的空间中，某种特定类型的变换总会“稳定”在唯一的一个点上，并且给我们提供了一种通过不断迭代来寻找这个“稳定点”的通用方法。\n核心概念与前提 在正式给出定理之前，我们需要先了解几个数学分析和拓扑学中的基础概念：\n度量空间 (Metric Space) 度量空间是一个赋予了“距离”概念的集合。具体来说，它是一个二元组 $(X, d)$，其中 $X$ 是一个非空集合，$d$ 是一个定义在 $X \\times X$ 上的实值函数（称为度量或距离），满足以下四个性质（对于任意 $x, y, z \\in X$）：\n非负性：$d(x, y) \\ge 0$ 不可分性：$d(x, y) = 0$ 当且仅当 $x = y$ 对称性：$d(x, y) = d(y, x)$ 三角不等式：$d(x, z) \\le d(x, y) + d(y, z)$ 柯西列 (Cauchy Sequence) 在度量空间 $(X, d)$ 中，如果一个序列 $(x_n)$ 满足以下条件：\n$$ \\forall \\varepsilon \u003e 0, \\exists N \\in \\mathbb{N}, s.t. \\forall m, n \\geq N: d(x_m, x_n) \u003c \\varepsilon $$那么这个序列就被称为柯西列。直观地说，随着项数的增加，柯西列中的元素彼此之间会变得“极其靠近”。\n完备度量空间 (Complete Metric Space) 如果在一个度量空间里，所有的柯西列都收敛于该空间内的一个点，那么这个空间就是“完备”的（即空间里没有“漏洞”）。常见的实数集 $\\mathbb{R}$ 和欧几里得空间 $\\mathbb{R}^n$ 就是完备度量空间，而有理数集 $\\mathbb{Q}$ 则不是，因为一些由有理数构成的柯西列最终逼近的值可能是无理数（比如 $\\sqrt{2}$），这个极限点不在有理数空间内部。\n压缩映射 (Contraction Mapping) 假设有一个映射 $T: X \\to X$。如果有这样一个常数 $q$（其中 $0 \\le q \u0026lt; 1$），使得对于空间 $X$ 中的任意两个点 $x$ 和 $y$，都满足： $$d(T(x), T(y)) \\le q \\cdot d(x, y)$$ （其中 $d$ 为空间中的距离函数） 那么 $T$ 就被称为一个压缩映射。通俗来讲，经过这个映射处理后，任意两点之间的距离都会严格按比例缩小。\n巴拿赫不动点定理 定理内容： 设 $(X, d)$ 是一个非空的完备度量空间，且 $T: X \\to X$ 是一个压缩映射。那么：\n$T$ 在 $X$ 中存在唯一的一个不动点 $x^*$，即满足 $T(x^*) = x^*$。 对于 $X$ 中的任意初始点 $x_0$，由迭代公式 $x_{n+1} = T(x_n)$ 生成的序列 $(x_n)$，必将收敛于该唯一的不动点 $x^*$。 这个定理的美妙之处在于其构造性。它不仅告诉你“解存在且唯一”，还给了你一套找解的算法：随便挑一个起点开始，不断地把结果重新放进去算，最后一定会无限逼近那个唯一的答案。\n定理的证明 巴拿赫不动点定理的证明非常经典，主要分为四个步骤：构造迭代序列、证明其为柯西列、证明极限是不动点、证明不动点的唯一性。\n第一步：构造迭代序列 在空间 $X$ 中任取任意一个初始点 $x_0$，我们通过迭代定义一个序列 $(x_n)$： $$x_1 = T(x_0)$$ $$x_2 = T(x_1) = T^2(x_0)$$ $$\\vdots$$ $$x_n = T(x_{n-1}) = T^n(x_0)$$第二步：证明 $(x_n)$ 是柯西列 首先评估相邻两项的距离，根据压缩性质有： $$d(x_1, x_2) = d(T(x_0), T(x_1)) \\le q \\cdot d(x_0, x_1)$$ 继续递推，对于任意 $n \\ge 1$： $$d(x_n, x_{n+1}) \\le q^n \\cdot d(x_0, x_1)$$对于任意的 $m \u0026gt; n$，利用三角不等式： $$d(x_n, x_m) \\le d(x_n, x_{n+1}) + d(x_{n+1}, x_{n+2}) + \\dots + d(x_{m-1}, x_m)$$ $$\\le (q^n + q^{n+1} + \\dots + q^{m-1}) \\cdot d(x_0, x_1)$$ $$= q^n \\frac{1 - q^{m-n}}{1 - q} \\cdot d(x_0, x_1)$$ 由于 $0 \\le q \u0026lt; 1$，放大后得到： $$d(x_n, x_m) \\le \\frac{q^n}{1 - q} \\cdot d(x_0, x_1)$$ 当 $n \\to \\infty$ 时，$q^n \\to 0$。因此 $d(x_n, x_m) \\to 0$。这就证明了序列 $(x_n)$ 是一个柯西列。\n第三步：证明柯西列的极限是不动点 由于 $(X, d)$ 是完备的度量空间，柯西列必在该空间内收敛。设其极限为 $x^*$，即 $\\lim_{n \\to \\infty} x_n = x^*$。 我们需要证明 $T(x^*) = x^*$。由于 $T$ 是压缩映射，它必然是连续的，利用连续性的性质： $$T(x^*) = T(\\lim_{n \\to \\infty} x_n) = \\lim_{n \\to \\infty} T(x_n) = \\lim_{n \\to \\infty} x_{n+1} = x^*$$ 因此，$x^*$ 确实是 $T$ 的一个不动点。\n第四步：证明不动点的唯一性 使用反证法，假设存在两个不同的不动点 $x^$ 和 $y^$ （即 $T(x^) = x^$ 且 $T(y^) = y^$）。 计算两点之间的距离： $$d(x^*, y^*) = d(T(x^*), T(y^*)) \\le q \\cdot d(x^*, y^*)$$ 因为 $x^* \\neq y^*$，所以它们之间的距离 $d(x^*, y^*) \u0026gt; 0$。两边同除以 $d(x^*, y^*)$ 得到： $$1 \\le q$$ 但这与压缩映射定义中 $q \u0026lt; 1$ 的限制矛盾。所以假设不成立，该不动点必须唯一。\n定理的广泛应用 巴拿赫不动点定理之所以伟大，是因为它可以用来解决各个领域中的存在唯一性问题。\n常微分方程的解 (皮卡-林德勒夫定理) 在微积分和常微分方程理论中，皮卡-林德勒夫定理（Picard-Lindelöf Theorem）用于证明一阶常微分方程初值问题的解存在且唯一。其本质正是通过构造一个积分算子（Picard 算子），并证明在适当的连续函数空间中，这个算子是一个压缩映射，因此对应的常微分方程具有唯一解。\n构造与证明简要： 考虑初值问题方程：$y\u0026rsquo;(t) = f(t, y(t))$，且初始条件为 $y(t_0) = y_0$。 我们可以对其两边积分，将微分方程转换为等价的积分方程： $$y(t) = y_0 + \\int_{t_0}^t f(s, y(s)) ds$$ 我们定义一个皮卡算子 $T$，它作用于连续函数 $y(t)$ 上： $$(Ty)(t) = y_0 + \\int_{t_0}^t f(s, y(s)) ds$$ 寻找常微分方程的解，等价于寻找这个函数空间中的不动点 $Ty = y$。只要函数 $f$ 满足李普希茨（Lipschitz）条件，对于足够小的时间区间 $[t_0-\\delta, t_0+\\delta]$，积分算子 $T$ 这个映射必然满足收缩系数 $q \u0026lt; 1$。由于闭区间上的连续函数空间是一个完备度量空间，根据巴拿赫不动点定理，微分方程必有唯一解。\n求解代数方程 (迭代法求根) 在数值分析中，要寻找方程 $f(x) = 0$ 的根，我们经常将其改写为 $x = g(x)$ 的形式。如果我们能保证 $g(x)$ 在根的某个邻域内是一个压缩映射（通常要求 $|g\u0026rsquo;(x)| \u0026lt; 1$），我们就可以从一个初始猜测值开始，不断使用 $x_{n+1} = g(x_n)$ 进行迭代。著名的牛顿法在满足条件时，其收敛性依然可以由不动点定理的变体来保证。\n具体例子： 求解方程 $\\cos(x) - x = 0$。 将其转化为找不动点的问题 $x = \\cos(x)$。定义 $g(x) = \\cos(x)$，考虑空间为闭区间 $X = [0, 1]$。 在这个区间上对任意 $x$，求导得 $|g\u0026rsquo;(x)| = |-\\sin(x)| \\le \\sin(1) \\approx 0.841 \u0026lt; 1$。 根据拉格朗日中值定理，对任意 $x, y \\in [0, 1]$： $$d(g(x), g(y)) = |g(x) - g(y)| = |g'(\\xi)||x - y| \\le 0.841 \\cdot d(x, y)$$ 这个 $g$ 显然是在完备空间 $X$ 上的压缩映射。随便你在计算器上敲下 $[0,1]$ 内的哪个数字，然后不停按 cos 键迭代，根据定理，结果必然会收敛到 $0.739085\u0026hellip;$ 这个唯一的定点上。\n动态规划与强化学习 在计算机科学和运筹学中，马尔可夫决策过程 (MDP) 和强化学习的基础是贝尔曼方程 (Bellman Equation)。计算最优策略时使用的“价值迭代 (Value Iteration)”能够收敛到最优价值函数，其深层数学原理正是：贝尔曼最优算子是无穷范数下的压缩映射。因此，无限次迭代必定能够收敛到唯一的最优解。\n原理论证： 贝尔曼最优算子 $B$ 作用于状态价值函数 $V(s)$ 上的定义为： $$(B V)(s) = \\max_a \\left[ R(s,a) + \\gamma \\sum_{s'} P(s'|s,a) V(s') \\right]$$ 这里采取无穷范数度量（最大值范数） $ | \\cdot | _ \\infty $。我们来考察算子对于两个不同价值估计 $V$ 和 $U$ 的距离缩小作用： $$ \\| B V - B U \\|_\\infty = \\max_s \\left| \\max_a \\left[ R + \\gamma \\sum P \\cdot V \\right] - \\max_a \\left[ R + \\gamma \\sum P \\cdot U \\right] \\right| $$ 运用不等式 $|\\max X - \\max Y| \\le \\max |X - Y|$，我们可以将其放缩为： $$ \\le \\max_{s,a} \\left| \\gamma \\sum_{s'} P(s'|s,a) (V(s') - U(s')) \\right| \\le \\gamma \\| V - U \\|_\\infty \\cdot \\max_{s,a} \\sum_{s'} P(s'|s,a) $$ 由于所有可能的下一状态转移概率之和为 $1$，不等式化简为： $$ \\| B V - B U \\|_\\infty \\le \\gamma \\| V - U \\|_\\infty $$ 由于折扣因子 $\\gamma \u0026lt; 1$，证明了贝尔曼最优算子 $B$ 是一个严格的压缩映射。因此，强化学习的价值迭代算法 $V_{k+1} = B(V_k)$ 注定会无视初始预估错误，完美收敛到唯一最优解。\n分形几何 (迭代函数系统) 如果你看过美丽的谢尔宾斯基三角形或是各种形态逼真的计算机生成分形树，它们往往由一组“迭代函数系统 (IFS)”生成。由于IFS中的每个函数都是压缩的，把一整个图像集合视作“点”，由巴拿赫不动点定理可知，这种变换最终收敛于一个极其复杂的几何图形——这被称为该系统的“吸引子”。\n总结 巴拿赫不动点定理展示了数学中“从抽象理论到具体算法”的典范。通过一个简单的“压缩”性质和“完备”前提，它就架起了一座连接纯粹存在论与实际计算方法之间的坚实桥梁。无论是研究宇宙边界的动态模型、训练游戏中的AI，还是仅仅想要求解一个非线性方程，不动点定理的幽灵总在背后默默工作。\n","date":"2026-04-02T10:00:00+08:00","permalink":"https://charles-7777.github.io/p/banach%E4%B8%8D%E5%8A%A8%E7%82%B9%E5%AE%9A%E7%90%86/","title":"Banach不动点定理"},{"content":" 信号是Linux系统中一种重要的进程间通信手段，它提供了一种异步通知机制，用于告知进程发生了某个事件。与管道、共享内存等同步通信方式不同，信号的到来是随机的，进程无法预知何时会收到信号。这种异步特性使得信号处理机制成为Linux内核中一个精妙而复杂的设计。 本文将从信号的生命周期出发，深入剖析Linux内核如何实现信号的产生、挂起、递达和处理，揭示从用户态到内核态再返回用户态的完整流程。\n信号的基本概念 什么是信号 信号本质上是软件层次上对中断机制的一种模拟。在原理上，一个进程收到一个信号与处理器收到一个中断请求非常相似——都是异步事件，都需要暂停当前工作去处理突发事件。\n每个信号都有一个唯一的编号，Linux系统中信号分为两大类：\n非实时信号（编号1-31）：也称为不可靠信号，不支持排队，可能丢失 实时信号（编号34-64）：也称为可靠信号，支持排队，不会丢失 信号的处理方式 当进程收到信号后，可以采取三种处理方式：\n默认处理：执行系统预设的操作，通常是终止进程、忽略信号、停止进程或产生核心转储 忽略信号：对信号不做任何处理，就像从未发生过一样 自定义捕捉：执行用户指定的信号处理函数 需要注意的是，SIGKILL和SIGSTOP这两个信号既不能被忽略，也不能被自定义捕捉，这是内核为了确保系统管理能力而做的强制规定。\n信号在内核中的数据结构 进程描述符中的信号相关字段 每个进程的task_struct中都包含了与信号相关的字段，用于记录信号的状态和处理方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct task_struct { // 信号处理函数表 struct sighand_struct *sighand; // 待处理信号集（未决信号） struct sigpending pending; // 信号阻塞掩码 sigset_t blocked; // 信号处理方式 struct signal_struct *signal; // ... }; 三张核心信号表 Linux内核为每个进程维护了三张信号相关的表：\n表类型 作用 数据结构 pending表 记录已经产生但尚未处理的信号 sigset_t位图 blocked表 记录被阻塞的信号（信号屏蔽字） sigset_t位图 handler表 记录每个信号的处理函数指针 struct k_sigaction数组 这三张表共同决定了信号的生命周期：当信号产生时，首先在pending表中标记；如果信号被阻塞，则暂不处理；当信号递达时，查询handler表确定执行何种操作。\n可靠信号与不可靠信号的区别 不可靠信号使用位图记录，每个信号只用一个bit表示。这意味着如果一个不可靠信号在未决期间多次产生，只会被记录一次。可靠信号则使用队列机制，每个信号可以包含附加信息，并支持排队。信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号。\n信号的产生与发送 信号产生的方式 信号可以通过多种方式产生：键盘输入（Ctrl+C产生SIGINT）、硬件异常（除零错误产生SIGFPE）、系统调用（kill、raise、alarm）以及软件条件（管道读端关闭后写入产生SIGPIPE）。\n发送信号的内核实现 发送信号的核心是__send_signal()函数，其大致流程如下：\n1 2 3 4 5 6 __send_signal() -\u0026gt; 设置pending位图 -\u0026gt; 分配sigqueue结构（可靠信号） -\u0026gt; complete_signal() -\u0026gt; signal_wake_up() -\u0026gt; 唤醒目标进程 信号发送的跨进程特性 信号是进程间通信的一种方式，任何进程都可以通过kill()系统调用向其他进程发送信号。内核在sys_kill()中根据目标进程的pid查找对应的task_struct，然后调用上述发送流程。\n信号的处理时机 信号处理的关键时机 信号的处理并非在产生的瞬间立即执行，而是发生在特定的时机。这种异步特性体现为进程只有在“合适的时机”才会去处理信号。这个“合适的时机”就是从内核态即将返回用户态的时刻。\n为什么选择这个时机 内核态拥有最高权限，如果在内核态直接执行用户自定义的信号处理函数，用户代码将获得内核级特权，这会造成严重的安全隐患。因此，内核必须在返回用户态后执行信号处理函数，确保处理函数在受限的用户态运行。\n时机的具体触发场景 以下场景会触发内核检查和处理信号：系统调用完成从内核态返回用户态时、中断或异常处理完成返回用户态时、进程从可中断睡眠状态被唤醒时。\n内核线程与信号 内核线程不返回用户态，因此它们不响应信号。如果内核线程需要处理信号，必须主动调用signal_pending()函数检查。\n信号捕捉的完整流程 信号捕捉是信号处理中最复杂的部分，尤其是当信号的处理函数是用户自定义函数时。下面详细剖析整个流程。\n流程概览 1 2 3 用户态(main) → 内核态(系统调用/中断) → 检查pending信号 → 发现需要捕捉的信号 → 修改用户态栈 → 返回用户态执行信号处理函数 → 处理函数返回 → sigreturn系统调用 → 恢复上下文 → 继续执行main函数 详细步骤分解 步骤1：信号处理函数的注册 用户程序通过signal()或sigaction()注册信号处理函数，内核将处理函数地址记录在task_struct-\u0026gt;sighand-\u0026gt;action[SIGINT]中。\n步骤2：信号产生与记录 当用户按下Ctrl+C，内核产生SIGINT信号，在进程的pending表中设置SIGINT位。\n步骤3：内核态返回前的检查 进程因系统调用或中断进入内核态，在准备返回用户态时，内核调用do_signal()函数，根据信号处理方式决定后续操作。\n步骤4：构建用户态栈帧 当信号需要被自定义函数处理时，handle_signal()调用setup_frame()构建特殊的栈帧：将返回地址修改为信号处理函数的入口地址，在用户栈上保存原始上下文，设置特殊返回代码使得处理函数执行完后能调用sigreturn。\n步骤5：执行信号处理函数 修改完栈帧后，内核返回用户态。由于指令指针已被修改，CPU开始执行信号处理函数。\n步骤6：返回与恢复 信号处理函数执行完毕返回时，由于栈上预设的特殊返回代码，会自动调用sigreturn()系统调用，再次进入内核态，恢复之前保存的原始上下文，然后返回用户态继续执行原程序。\n┌─────────────────────────────────────────────────────────────────┐ │ 用户态 (Ring 3) │ │ ┌─────────┐ ┌─────────────┐ │ │ │ main() │ ─────────────────→ │ sighandler()│ │ │ └────┬────┘ └──────┬──────┘ │ │ │ │ │ │ ①系统调用 ⑥返回 │ │ 或中断 (调用sigreturn) │ │ ↓ ↓ │ ├─────────────────────────────────────────────────────────────────┤ │ 内核态 (Ring 0) │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ ②处理异常/中断 ③检查pending ④do_signal ⑤setup_frame ││ │ │ ││ │ │ ⑦sigreturn系统调用 ⑧恢复上下文 ││ │ └─────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────┘\n为什么需要四次状态切换 整个过程中涉及四次用户态/内核态切换，这种设计是必要的，因为用户自定义的信号处理函数必须在用户态执行，防止恶意代码获取内核特权。\n信号处理的高级特性 信号阻塞 信号阻塞是指阻止信号被处理，但并不阻止信号产生。被阻塞的信号将一直处于pending状态，直到解除阻塞。通过sigprocmask可以灵活操作信号屏蔽字。\n信号屏蔽字 每个进程都有一个信号屏蔽字（Signal Mask），记录了当前被阻塞的信号集。当信号递达时，内核会检查屏蔽字决定是否递达。\nsigaction函数详解 与简单的signal()相比，sigaction()提供了更精细的控制：\n1 2 3 4 5 6 struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; sa_flags常用标志：SA_SIGINFO（获取详细信息）、SA_RESTART（自动重启系统调用）、SA_NODEFER（不自动阻塞当前信号）、SA_RESETHAND（处理后恢复默认行为）。\n信号合并问题 对于不可靠信号，如果多个相同信号在处理之前连续到达，它们会合并为一个。这意味着信号处理函数只被调用一次，无法通过信号计数来统计事件数量。GNU C库文档中给出了处理SIGCHLD信号的典型方式，通过扫描子进程列表来弥补信号合并带来的信息损失。\n被信号中断的系统调用 当进程在执行阻塞的系统调用（如read、sleep）时收到信号，根据设置不同有两种情况：系统调用被中断返回-1并设置errno为EINTR，或通过SA_RESTART标志自动重启系统调用。\n实际代码示例 完整的信号捕捉示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;signal.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;string.h\u0026gt; void custom_handler(int sig) { write(STDOUT_FILENO, \u0026#34;Caught SIGINT!\\n\u0026#34;, 15); } int main() { struct sigaction act, oldact; act.sa_handler = custom_handler; sigemptyset(\u0026amp;act.sa_mask); act.sa_flags = SA_RESTART; sigaction(SIGINT, \u0026amp;act, \u0026amp;oldact); printf(\u0026#34;Press Ctrl+C to test, or Ctrl+\\\\ to quit.\\n\u0026#34;); while(1) { printf(\u0026#34;Sleeping...\\n\u0026#34;); sleep(10); } return 0; } 使用SA_SIGINFO获取详细信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;signal.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; void info_handler(int sig, siginfo_t *info, void *context) { printf(\u0026#34;Signal %d received\\n\u0026#34;, sig); printf(\u0026#34; Sender PID: %d\\n\u0026#34;, info-\u0026gt;si_pid); printf(\u0026#34; Signal code: %d\\n\u0026#34;, info-\u0026gt;si_code); } int main() { struct sigaction act; act.sa_sigaction = info_handler; sigemptyset(\u0026amp;act.sa_mask); act.sa_flags = SA_SIGINFO; sigaction(SIGUSR1, \u0026amp;act, NULL); printf(\u0026#34;PID: %d\\n\u0026#34;, getpid()); printf(\u0026#34;Send SIGUSR1 to see detailed info\\n\u0026#34;); while(1) pause(); } 总结 Linux信号处理机制的精妙之处在于：异步与同步的融合——信号产生是异步的，但处理被延迟到特定的同步点（内核态返回到用户态）；安全优先的设计——用户自定义信号处理函数必须在用户态执行，通过精心构造栈帧和sigreturn机制确保安全返回；灵活的控制能力——通过信号阻塞、sigaction的多种标志位，开发者可以精细控制信号行为；可靠性与兼容性的平衡——保留传统不可靠信号的行为以兼容旧代码，同时引入可靠信号支持队列和附加信息。理解信号处理机制，不仅有助于编写健壮的信号处理代码，更能深入理解Linux内核的用户态/内核态交互方式、中断处理流程和进程调度机制。\n深入理解Linux内核信号处理机制原理\n","date":"2026-03-26T23:36:43+08:00","permalink":"https://charles-7777.github.io/p/linux%E5%86%85%E6%A0%B8%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6%E5%8E%9F%E7%90%86/","title":"Linux内核信号处理机制原理"},{"content":" 参考文件：Wi-Fi Protected Setup Specification v2.0.9, Wi-Fi Alliance, 2024\ngraph LR Registrar[Registrar] Enrollee[Enrollee] AP[AP] Registrar -- \u0026#34;E\u0026#34; --- Enrollee Registrar -- \u0026#34;M\u0026#34; --- AP Enrollee -- \u0026#34;A\u0026#34; --- AP Registrar可能是独⽴的设备或是与AP在同⼀设备上的逻辑功能模块。如果Registrar是独⽴的设备，则与AP可以通过WiFi或以太⽹进⾏连接。\nEnrollee是wifi STA设备。与AP的接⼝A是基于WiFi连接实现的。这⾥所说的WiFi连接，是WPS协议定义的特殊的WiFi连接过程，其⽬的是传递配置参数。当参数传递完毕后，这个连接将会断开。\nEnrollee和Registrar之间的接⼝E，是认证和配置传递的接⼝。认证是通过PIN码实现的。\nRegistrar和AP之间的接⼝M，原理和接⼝E类似，实现Registrar对AP的配置，这时的AP⾓⾊与Enrollee类似\nStandalone AP (APs with built-in Registrar capabilities)\nEnternal Registrar\n在 WPS 中，PIN 是设备的身份凭证，每个支持 WPS 的设备（无论是 Enrollee 还是 Registrar）都有一个 PIN： Enrollee PIN：印在设备标签上或显示在屏幕上（如打印机、摄像头）。 Registrar PIN：如果 Registrar 支持“主动输入模式”，它也可能有一个 PIN（常见于 AP 内置的 Web 界面中）。\n🧩 场景分析：当使用 外部 Registrar 时，PIN 如何工作？\n假设： Enrollee：一台新 Wi-Fi 打印机（有固定 PIN，比如 12345670）。 AP：一个普通的无线路由器（可能没有内置 Registrar，或用户选择不用）。 外部 Registrar：用户的智能手机（运行 WPS 管理 App）。\n✅ 正确流程如下： 用户操作外部 Registrar（手机）： 打开手机上的 WPS App。 选择“添加新设备” → “通过 PIN 方式”。 手动输入打印机（Enrollee）的 PIN 码（12345670）到手机 App 中。 手机（外部 Registrar）通过 UPnP 告诉 AP： “我要开始一个 PIN 注册流程，Enrollee 的 PIN 是 12345670。” AP 启动 WPS 监听模式，等待 Enrollee 连接。 打印机（Enrollee）发起连接： 用户按下打印机上的 WPS 按钮（或启动配网模式）。 打印机广播 WPS 请求，与 AP 建立 EAP-WSC 交换。 AP 充当代理，转发消息： AP 收到打印机的 WPS 消息后，不自己验证 PIN，而是通过 UPnP 将消息转发给手机（外部 Registrar）。 手机使用之前输入的 PIN (12345670) 与打印机协商密钥、验证身份。 凭证下发： 验证成功后，手机生成网络凭证（SSID + PSK），通过 UPnP 发给 AP。 AP 再将凭证通过 WPS 协议发送给打印机。\n❌ 常见误解：“在 AP 上输入外部设备的 PIN”\n这种说法通常是以下两种情况的混淆：\n情况 1：你其实用的是 内部 Registrar\n很多家用路由器 Web 界面里有个 “WPS PIN 添加设备” 的输入框。 这时 AP 自己就是 Registrar！你是在 内部 Registrar 中输入 Enrollee 的 PIN。 这 不是 外部 Registrar 场景。\n情况 2：误以为 AP 是操作主体 在外部 Registrar 模式下，所有用户交互（包括输入 PIN）都发生在外部设备（如手机/PC）上。 AP 只是默默转发数据，不会弹出任何 PIN 输入界面（除非它同时启用了内部 Registrar，但这会造成冲突，规范不推荐）。\n🔐 补充：AP 有 PIN 吗？\n有的！但用途不同： AP 的 PIN 通常用于 当 AP 作为 Enrollee 时（例如：将一个新 AP 加入现有 Mesh 网络）。 或者，当 内部 Registrar 被外部 Enrollee 主动连接时，外部设备需要输入 AP 的 PIN 来完成配对（较少见）。\n但在 外部 Registrar 管理其他 Enrollee 的场景中，AP 的 PIN 不参与流程。\n✅ 总结 “谁是 Registrar，谁就管 PIN。”\n外部 Registrar 在手机上？那就在手机输 PIN。 内部 Registrar 在路由器里？那就在路由器 Web 界面输 PIN。\n这样设计保证了灵活性：你可以用体验更好的手机 App 来管理配网，而不需要去记路由器的管理地址和密码。\nWiFi-WPS详细交互过程.pdf\n","date":"2026-03-17T19:27:21+08:00","permalink":"https://charles-7777.github.io/p/wi-fi-protected-setup-wps-%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D/","title":"Wi-Fi Protected Setup (WPS) 协议介绍"},{"content":"拉姆齐理论（Ramsey Theory）是组合数学中的一个分支，它的核心思想可以概括为：“完全的无序是不可能的”。无论一个系统有多么庞大和混乱，只要系统足够大，其中必定包含某种高度有序的子结构。\n拉姆齐问题最著名的通俗表达是派对问题（六人集会问题）： 假设在一个派对上有 6 个人，这 6 个人中，要么必定有 3 个人互相认识，要么必定有 3 个人互相都不认识。\n我们需要证明：对于 6 个顶点的完全图 $K_6$，如果将其边涂成红色或蓝色，必然存在一个红色三角形或蓝色三角形。 证明过程：\n在图 $K_6$ 中任意选择一个顶点，记为 $v$。 除去 $v$ 之外，还有 5 个顶点，因此 $v$ 必然连接着 5 条边。 根据抽屉原理（鸽巢原理），在这 5 条边中，至少有 $\\lceil 5/2 \\rceil = 3$ 条边是相同颜色的。不妨假设有 3 条边是红色的。 假设这 3 条红边分别连接顶点 $A$、$B$ 和 $C$。 现在考察顶点 $A, B, C$ 之间的 3 条边（边 $AB, BC, CA$）： 如果这 3 条边中有一条是红色的（例如 $AB$ 是红色），那么顶点 $v, A, B$ 就构成了一个红色三角形。 如果这 3 条边全都是蓝色的，那么顶点 $A, B, C$ 本身就构成了一个蓝色三角形。 在图论的语言中，拉姆齐定理可以表述为：对于给定的任意整数 $r$ 和 $s$，存在一个最小的正整数 $N$，使得对于完全图 $K_N$（即 $N$ 个顶点，且任意两个顶点之间都有边相连的图）的每一条边任意涂上红色或蓝色，必然能找到一个由 $r$ 个顶点组成的完全图（其所有边都是红色），或者找到一个由 $s$ 个顶点组成的完全图（其所有边都是蓝色）。\n这个最小的整数 $N$ 就被称为拉姆齐数（Ramsey Number），记作 $R(r, s)$。\nRamsey定理\n","date":"2026-03-06T13:23:46+08:00","permalink":"https://charles-7777.github.io/p/ramsey%E9%97%AE%E9%A2%98/","title":"Ramsey问题"},{"content":"问题描述 等周问题是变分法中的一个经典问题：在平面上，给定周长 $L$ 的所有闭合曲线中，哪一条曲线围成的面积最大？\n直观上我们知道答案是圆，下面我们将使用变分法（Variational Calculus）严格推导这一结论。\n数学建模 假设平面闭合曲线由参数方程表示： $$ \\begin{cases} x = x(t) \\\\ y = y(t) \\end{cases} \\quad t \\in [t_0, t_1] $$ 且 $x(t_0) = x(t_1), y(t_0) = y(t_1)$。\n目标泛函：面积 $A$ 根据格林公式 (Green\u0026rsquo;s Theorem)，闭合曲线围成的面积 $A$ 可以表示为： $$ A = \\frac{1}{2} \\oint (x dy - y dx) = \\frac{1}{2} \\int_{t_0}^{t_1} (x y' - y x') \\, dt $$ 其中 $x\u0026rsquo; = \\frac{dx}{dt}, y\u0026rsquo; = \\frac{dy}{dt}$。\n约束条件：周长 $L$ 曲线的弧长（周长）固定为 $L$： $$ L = \\int_{t_0}^{t_1} \\sqrt{x'^2 + y'^2} \\, dt $$拉格朗日乘子法 这是一个等周变分问题（Isoperimetric Problem），我们需要在约束条件 $L$ 下求泛函 $A$ 的极值。 通过引入拉格朗日乘子，将原本复杂的多约束优化问题转化为无约束优化问题，从而简化求解过程。 引入拉格朗日乘子 (Lagrange Multiplier) $\\lambda$，构造新的辅助泛函 $J$： $$ J = A + \\lambda L = \\int_{t_0}^{t_1} \\left[ \\frac{1}{2}(x y' - y x') + \\lambda \\sqrt{x'^2 + y'^2} \\right] \\, dt $$定义被积函数（Lagrangian）为 $F$： $$ F(x, y, x', y') = \\frac{1}{2}(x y' - y x') + \\lambda \\sqrt{x'^2 + y'^2} $$欧拉-拉格朗日方程 (Euler-Lagrange Equation) 为了使 $J$ 取得极值，$x(t)$ 和 $y(t)$ 必须同时满足欧拉-拉格朗日方程组：\n$$ \\begin{cases} \\frac{\\partial F}{\\partial x} - \\frac{d}{dt} \\left( \\frac{\\partial F}{\\partial x'} \\right) = 0 \\\\[10pt] \\frac{\\partial F}{\\partial y} - \\frac{d}{dt} \\left( \\frac{\\partial F}{\\partial y'} \\right) = 0 \\end{cases} $$计算偏导数 对于 $x$ 的方程： $$ \\frac{\\partial F}{\\partial x} = \\frac{1}{2}y' $$ $$ \\frac{\\partial F}{\\partial x'} = -\\frac{1}{2}y + \\lambda \\frac{x'}{\\sqrt{x'^2 + y'^2}} $$对于 $y$ 的方程： $$ \\frac{\\partial F}{\\partial y} = -\\frac{1}{2}x' $$ $$ \\frac{\\partial F}{\\partial y'} = \\frac{1}{2}x + \\lambda \\frac{y'}{\\sqrt{x'^2 + y'^2}} $$代入 E-L 方程 方程 1 (关于 $x$): $$ \\frac{1}{2}y' - \\frac{d}{dt} \\left( -\\frac{1}{2}y + \\lambda \\frac{x'}{\\sqrt{x'^2 + y'^2}} \\right) = 0 $$ $$ \\frac{1}{2}y' - \\left( -\\frac{1}{2}y' + \\frac{d}{dt} \\left( \\lambda \\frac{x'}{\\sqrt{x'^2 + y'^2}} \\right) \\right) = 0 $$ $$ y' - \\lambda \\frac{d}{dt} \\left( \\frac{x'}{\\sqrt{x'^2 + y'^2}} \\right) = 0 \\quad \\text{......(1)} $$方程 2 (关于 $y$): $$ -\\frac{1}{2}x' - \\frac{d}{dt} \\left( \\frac{1}{2}x + \\lambda \\frac{y'}{\\sqrt{x'^2 + y'^2}} \\right) = 0 $$ $$ -\\frac{1}{2}x' - \\left( \\frac{1}{2}x' + \\frac{d}{dt} \\left( \\lambda \\frac{y'}{\\sqrt{x'^2 + y'^2}} \\right) \\right) = 0 $$ $$ -x' - \\lambda \\frac{d}{dt} \\left( \\frac{y'}{\\sqrt{x'^2 + y'^2}} \\right) = 0 \\quad \\text{......(2)} $$积分求解 对 (1) 式关于 $t$ 积分： $$ y - \\lambda \\frac{x'}{\\sqrt{x'^2 + y'^2}} = C_1 \\quad \\Rightarrow \\quad y - C_1 = \\lambda \\frac{x'}{\\sqrt{x'^2 + y'^2}} $$对 (2) 式关于 $t$ 积分： $$ -x - \\lambda \\frac{y'}{\\sqrt{x'^2 + y'^2}} = -C_2 \\quad \\Rightarrow \\quad x - C_2 = -\\lambda \\frac{y'}{\\sqrt{x'^2 + y'^2}} $$ (这里为了形式统一，积分常数设为 $-C_2$)\n我们得到了两个方程：\n$y - C_1 = \\lambda \\frac{x\u0026rsquo;}{\\sqrt{x\u0026rsquo;^2 + y\u0026rsquo;^2}}$ $x - C_2 = -\\lambda \\frac{y\u0026rsquo;}{\\sqrt{x\u0026rsquo;^2 + y\u0026rsquo;^2}}$ 将两式平方并相加： $$ (x - C_2)^2 + (y - C_1)^2 = \\lambda^2 \\left( \\frac{y'^2}{x'^2 + y'^2} + \\frac{x'^2}{x'^2 + y'^2} \\right) $$ $$ (x - C_2)^2 + (y - C_1)^2 = \\lambda^2 \\left( \\frac{x'^2 + y'^2}{x'^2 + y'^2} \\right) $$最终得到： $$ (x - C_2)^2 + (y - C_1)^2 = \\lambda^2 $$结论 方程 $(x - C_2)^2 + (y - C_1)^2 = \\lambda^2$ 正是一个圆的方程。\n圆心坐标为 $(C_2, C_1)$ 半径为 $R = |\\lambda|$ 因此，在周长固定的闭合曲线中，围成面积最大的曲线是圆。\n","date":"2026-02-28T20:47:04+08:00","permalink":"https://charles-7777.github.io/p/%E5%8F%98%E5%88%86%E6%B3%95%E8%AF%81%E6%98%8E%E7%AD%89%E5%91%A8%E9%97%AE%E9%A2%98/","title":"变分法证明等周问题"},{"content":"变分法的由来 变分法（Calculus of Variations）的历史可以追溯到 17 世纪末的一个著名数学挑战。\n1696 年的挑战\n瑞士数学家约翰·伯努利 (Johann Bernoulli) 向全欧洲的数学家提出了著名的“最速降线问题 (Brachistochrone problem)”：\n设在垂直平面内有两点 $A$ 和 $B$（$A$ 高于 $B$，且不在同一铅垂线上）。一个质点只受重力作用，从 $A$ 点沿曲线下滑到 $B$ 点，问沿什么曲线下滑所需的时间最短？\n当时，伽利略曾错误地认为是圆弧，而约翰·伯努利期待着从他的同行那里得到解答。牛顿、莱布尼茨、洛必达和约翰的哥哥雅各布·伯努利都解决了这个问题（答案是旋轮线/摆线）。这个问题的解决标志着变分法的诞生——它不再是求函数的极值（如微积分中的 $dy/dx=0$），而是求泛函的极值（即寻找一个函数，使得某个积分量达到极值）。\n随后，欧拉和拉格朗日将这些特例推广为一般性的理论，形成了今天的变分法。\n变分法的基本原理 泛函 (Functional) 普通微积分处理的是函数 $y=f(x)$，输入一个数 $x$，输出一个数 $y$。\n变分法处理的是泛函 $J[y]$，输入一个函数 $y(x)$，输出一个数 $J$。\n[泛函]：Functional [算子]：Operator [变换]：Transformation [函数]：Function [映射]：Mapping\n函数 (Function) 是一种特殊的映射，要求定义域中的每一个元素都对应值域中的唯一一个元素。在现代数学中，函数和映射常常被视为同义词，可以互换使用。\n泛函 (Functional) 是一种特殊的函数，其定义域是一个函数空间（即元素是函数），值域是数域（实数或复数）。\neg:$F[f] = \\int_{0}^{1} f(x)\\ ; $ $L[y] = \\int_{x_1}^{x_2} \\sqrt{1 + (y\u0026rsquo;)^2} , dx$(弧长)\n算子 (Operator) 是一种特殊的函数或映射，其定义域和值域通常都是函数空间。\n变换 (Transformation) 这个词的含义非常广泛，通常指同一个空间内的映射，或者指一种具体的映射过程或操作。 它常常可以与映射和算子互换使用，但更强调“改变”或“转换”的动作本身。\n最常见的泛函形式是积分型泛函：\n$$ J[y] = \\int_{x_1}^{x_2} F(x, y, y') \\, dx $$其中：\n$x$ 是自变量。\n$y = y(x)$ 是我们要寻找的未知函数。\n$y\u0026rsquo; = \\frac{dy}{dx}$ 是 $y$ 的导数。\n$F(x, y, y\u0026rsquo;)$ 是已知的函数形式（称为拉格朗日量）。\n边界条件通常固定为 $y(x_1) = y_1$ 和 $y(x_2) = y_2$。\n欧拉-拉格朗日方程 (Euler-Lagrange Equation) 为了找到使泛函 $J[y]$ 取得极值（极大值或极小值）的函数 $y(x)$，该函数必须满足一个微分方程。这个方程被称为欧拉-拉格朗日方程：\n$$ \\frac{\\partial F}{\\partial y} - \\frac{d}{dx} \\left( \\frac{\\partial F}{\\partial y'} \\right) = 0 $$直观理解：这相当于微积分中求极值的条件 $f\u0026rsquo;(x)=0$ 在无限维函数空间中的推广。凡是能使作用量（积分）取极值的路径，都必须满足在这个方程。\n计算实例 例子一：两点之间直线最短 问题：在平面上，求连接两点 $(x_1, y_1)$ 和 $(x_2, y_2)$ 的曲线中，长度最短的那条。\n1. 建立泛函\n平面上微元长度为 $ds = \\sqrt{dx^2 + dy^2} = \\sqrt{1 + (y\u0026rsquo;)^2} , dx$。\n总长度 $L$ 为：\n$ L[y] = \\int_{x_1}^{x_2} \\sqrt{1 + (y\u0026rsquo;)^2} , dx $\n这里被积函数 $F(x, y, y\u0026rsquo;) = \\sqrt{1 + (y\u0026rsquo;)^2}$。\n2. 应用 E-L 方程\n$\\frac{\\partial F}{\\partial y} = 0$ （因为 $F$ 中不显含 $y$）\n$\\frac{\\partial F}{\\partial y\u0026rsquo;} = \\frac{1}{2\\sqrt{1 + (y\u0026rsquo;)^2}} \\cdot 2y\u0026rsquo; = \\frac{y\u0026rsquo;}{\\sqrt{1 + (y\u0026rsquo;)^2}}$\n代入方程 $\\frac{\\partial F}{\\partial y} - \\frac{d}{dx} (\\frac{\\partial F}{\\partial y\u0026rsquo;}) = 0$：\n$ 0 - \\frac{d}{dx} \\left( \\frac{y\u0026rsquo;}{\\sqrt{1 + (y\u0026rsquo;)^2}} \\right) = 0 $\n这意味着括号内的项是常数 $C$：\n$ \\frac{y\u0026rsquo;}{\\sqrt{1 + (y\u0026rsquo;)^2}} = C $\n这意味着 $y\u0026rsquo;$ 必须是常数（记为 $a$）。\n$ y\u0026rsquo; = a \\implies y = ax + b $\n这正是直线的方程。\n例子二：最速降线问题 (Brachistochrone) 问题：质点在重力作用下从 $(0,0)$ 滑到 $(x_2, y_2)$，求时间最短的路径。设向下为 $y$ 轴正方向。\n1. 建立泛函\n根据能量守恒 $\\frac{1}{2}mv^2 = mgy \\implies v = \\sqrt{2gy}$。\n时间 $t = \\int \\frac{ds}{v} = \\int \\frac{\\sqrt{1+(y\u0026rsquo;)^2}dx}{\\sqrt{2gy}}$。\n忽略常数 $\\frac{1}{\\sqrt{2g}}$，我们要极小化的泛函为：\n$ J[y] = \\int_{0}^{x_2} \\sqrt{\\frac{1 + (y\u0026rsquo;)^2}{y}} , dx $\n这里 $F = \\sqrt{\\frac{1 + (y\u0026rsquo;)^2}{y}}$。\n2. 应用 E-L 方程（贝尔特拉米恒等式）\n由于 $F$ 不显含 $x$（即 $\\frac{\\partial F}{\\partial x} = 0$），我们可以使用 E-L 方程的一个简化形式 —— 贝尔特拉米恒等式 (Beltrami Identity)：\n$ F - y\u0026rsquo; \\frac{\\partial F}{\\partial y\u0026rsquo;} = C $\n代入 $F$：\n$ \\sqrt{\\frac{1 + (y\u0026rsquo;)^2}{y}} - y\u0026rsquo; \\cdot \\left( \\frac{1}{\\sqrt{y(1 + (y\u0026rsquo;)^2)}} \\cdot y\u0026rsquo; \\right) = C $\n$ \\sqrt{\\frac{1 + (y\u0026rsquo;)^2}{y}} - \\frac{(y\u0026rsquo;)^2}{\\sqrt{y(1 + (y\u0026rsquo;)^2)}} = C $\n通分整理得到（分子变为 $1+(y\u0026rsquo;)^2 - (y\u0026rsquo;)^2 = 1$）：\n$ \\frac{1}{\\sqrt{y(1 + (y\u0026rsquo;)^2)}} = C $\n平方整理：\n$ y(1 + (y\u0026rsquo;)^2) = K \\quad (\\text{令 } K = 1/C^2) $\n3. 求解微分方程\n令 $y\u0026rsquo; = \\cot \\theta$（三角换元法）：\n$ y(1 + \\cot^2 \\theta) = K \\implies y = K \\sin^2 \\theta = \\frac{K}{2}(1 - \\cos 2\\theta) $\n由 $dy/dx = \\cot \\theta$，得 $dx = dy / \\cot \\theta = \\tan \\theta , dy$。\n将 $y$ 的表达式微分代入积分可得 $x$ 的参数方程。最终得到旋轮线 (Cycloid) 方程：\n$$ \\begin{cases} x = R (\\phi - \\sin \\phi) \\\\ y = R (1 - \\cos \\phi) \\end{cases} $$ 例子三：费马原理 (Fermat\u0026rsquo;s Principle - 光路最短) 原理：光线在两点间传播时，总是沿着光程 (Optical Path Length) 极值的路径传播。\n光程 $L = \\int n(x, y) , ds$。\n其中 $n(x,y)$ 是折射率。\n问题：证明在折射率 $n$ 随 $y$ 变化的介质中，光线遵循斯涅尔定律 (Snell\u0026rsquo;s Law)。\n1. 建立泛函\n$ J[y] = \\int n(y) , ds = \\int_{x_1}^{x_2} n(y) \\sqrt{1 + (y\u0026rsquo;)^2} , dx $\n这里 $F(y, y\u0026rsquo;) = n(y) \\sqrt{1 + (y\u0026rsquo;)^2}$。注意 $n$ 是 $y$ 的函数。\n2. 应用 E-L 方程\n因为 $F$ 不显含 $x$，再次使用贝尔特拉米恒等式：\n$ F - y\u0026rsquo; \\frac{\\partial F}{\\partial y\u0026rsquo;} = C $\n$ n(y)\\sqrt{1+(y\u0026rsquo;)^2} - y\u0026rsquo; \\left( n(y) \\frac{y\u0026rsquo;}{\\sqrt{1+(y\u0026rsquo;)^2}} \\right) = C $\n提取公因式 $\\frac{n(y)}{\\sqrt{1+(y\u0026rsquo;)^2}}$ 并整理：\n$ \\frac{n(y) [1+(y\u0026rsquo;)^2 - (y\u0026rsquo;)^2]}{\\sqrt{1+(y\u0026rsquo;)^2}} = C \\implies \\frac{n(y)}{\\sqrt{1+(y\u0026rsquo;)^2}} = C $\n3. 物理诠释\n在几何上，导数 $y\u0026rsquo; = \\tan \\phi$（切线斜率），其中 $\\phi$ 是切线与 x 轴夹角。\n则 $\\sqrt{1+(y\u0026rsquo;)^2} = \\sqrt{1+\\tan^2 \\phi} = \\sec \\phi = \\frac{1}{\\cos \\phi}$。\n代入上式：\n$ n(y) \\cos \\phi = C $\n如果我们定义入射角 $i$ 为光线与法线（垂直于 x 轴，即 y 轴方向）的夹角，那么 $\\phi = 90^\\circ - i$，此时 $\\cos\\phi = \\sin i$。\n方程变为：\n$ n(y) \\sin i = \\text{Constant} $\n这正是著名的斯涅尔折射定律 ($n_1 \\sin i_1 = n_2 \\sin i_2$) 的连续介质形式。\n","date":"2026-02-24T19:57:13+08:00","permalink":"https://charles-7777.github.io/p/%E5%8F%98%E5%88%86%E6%B3%95/","title":"变分法"},{"content":" Agent Skills 终极指南：入门、精通、预测\n","date":"2026-02-11T11:19:50+08:00","permalink":"https://charles-7777.github.io/p/agent-skill/","title":"Agent Skill"},{"content":"ctstate ctstate 是 conntrack 对“当前报文相对连接跟踪条目”的分类标签（用于规则匹配），主要依据：\n是否绕过跟踪（raw 表 NOTRACK / nftables bypass）\n是否命中 conntrack 条目（original/reply 方向）\n是否已见 reply（常见关键位：SEEN_REPLY）\n是否命中 expectation 或可被关联（helper / ICMP 错误嵌套头）\n协议跟踪器是否接受该报文（否则可能是 INVALID）\nctstate 含义（策略视角） 典型判定要点 NEW 新流量/新建条目 查表未命中但允许创建；或条目存在但尚未见 reply（UDP 常见） ESTABLISHED 已有双向迹象 命中条目且已见 reply（常见依据：SEEN_REPLY=1） RELATED 与既有连接相关 命中 expectation（helper 预告）或 ICMP 错误可关联回已有条目 INVALID 无法可靠追踪/归属 解析失败/状态机不接受/分片校验问题/不可匹配等 UNTRACKED 明确不走 conntrack raw 表命中 NOTRACK 或 nftables bypass iptables/ebtables 示例 iptables：\n1 2 3 4 5 iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -m conntrack --ctstate INVALID -j DROP iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT nftables：\n1 2 3 4 5 6 7 8 # 如果你的系统还没有 filter 表/INPUT 链 nft add table inet filter nft \u0026#39;add chain inet filter input { type filter hook input priority 0; policy accept; }\u0026#39; # 添加规则 nft add rule inet filter input ct state established,related accept nft add rule inet filter input ct state invalid drop nft add rule inet filter input tcp dport 22 ct state new accept 快速验证 conntrack -L：看是否存在条目/超时\nconntrack -E：实时看是否创建、何时出现 reply\n/proc/net/nf_conntrack：内核导出\nctstate vs ctstatus\nctstate：对当前报文的分类 ctstatus：条目内部标志位（如 SEEN_REPLY、EXPECTED 等），ctstate 通常由这些标志位综合得出\n","date":"2026-01-19T17:42:15+08:00","permalink":"https://charles-7777.github.io/p/nf_conntrack-%E7%9A%84-ctstate/","title":"nf_conntrack 的 ctstate"},{"content":"I/O 模型概览 一个输入操作通常包括两个阶段：\n等待数据准备好 从内核向进程复制数据 对于一个套接字上的输入操作，第一步通常涉及等待数据从网络中到达。当所等待数据到达时，它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区\nUnix 有五种 I/O 模型：\n阻塞 I/O：调用阻塞到数据复制完成。 非阻塞 I/O：立即返回，需要轮询。 I/O 复用：select/poll/epoll 等待多个 fd。 信号驱动 I/O：数据就绪时发 SIGIO，再读取。 异步 I/O：提交请求后由内核完成并通知。 实践侧重：\n阻塞模型简单，适合连接数少且延迟不敏感的 CLI/工具脚本； 非阻塞模型常与事件循环/定时器配合，否则轮询会耗 CPU； I/O 复用是高并发网络服务的基础； 信号驱动在 Linux 上使用较少，更多见于嵌入式/老项目； 异步 I/O 需要平台支持（如 Linux AIO/io_uring、Windows IOCP）。 阻塞 I/O 应用进程被阻塞，直到数据从内核缓冲区复制到应用进程缓冲区中才返回。 应该注意到，在阻塞的过程中，其它应用进程还可以执行，因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行，所以不消耗 CPU 时间，这种模型的 CPU 利用率会比较高。 例如，recvfrom() 用于接收 Socket 传来的数据，并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。\n1 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); sequenceDiagram participant App as 应用进程 participant Kernel as 内核 App-\u0026gt;\u0026gt;Kernel: recvfrom() Note left of App: 调用recvform()进程阻塞 Note right of Kernel: 等待数据到达并存入内核缓冲 Kernel--\u0026gt;\u0026gt;App: 数据复制完成后返回 特点：实现简单，CPU 利用率高（阻塞期间不占用 CPU），但吞吐受限。\n典型用法：短连接 RPC、小工具一次性读写； 隐患：若对端长时间不发数据或网络抖动，线程被长期占用，需要超时控制（如 SO_RCVTIMEO）； 在多线程服务器中，线程数与连接数线性相关，易受线程栈内存与调度开销限制。 非阻塞 I/O 应用进程执行系统调用之后，内核返回一个错误码。应用进程可以继续执行，但是需要不断的执行系统调用来获知 I/O 是否完成，这种方式称为轮询（polling）。 由于 CPU 要处理更多的系统调用，因此这种模型的 CPU 利用率比较低。\nsequenceDiagram participant App as 应用 participant Kernel as 内核 App-\u0026gt;\u0026gt;Kernel: recvfrom() (非阻塞) Kernel--\u0026gt;\u0026gt;App: EWOULDBLOCK App-\u0026gt;\u0026gt;Kernel: recvfrom() 轮询直至可读 Kernel--\u0026gt;\u0026gt;App: 数据复制完成 I/O 复用（事件驱动） 使用 select 或者 poll 等待数据，并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞，当某一个套接字可读时返回，之后再使用 recvfrom 把数据从内核复制到进程中。\n它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O，即事件驱动 I/O。\n如果一个 Web 服务器没有 I/O 复用，那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接，那么就需要创建相同数量的线程。相比于多进程和多线程技术，I/O 复用不需要进程线程创建和切换的开销，系统开销更小。\nsequenceDiagram participant App as 应用层 participant Kernel as 内核 App-\u0026gt;\u0026gt;Kernel: select Note left of App: 调用select()进程阻塞,等待某一个套接字可读 Kernel--\u0026gt;\u0026gt;App: 数据就绪 App-\u0026gt;\u0026gt;Kernel: recvfrom() Note left of App: 在数据拷贝到应用的buffer的过程中，进程阻塞 Kernel--\u0026gt;\u0026gt;App: 返回数据 Note right of Kernel: copy data form kernel to user App-\u0026gt;\u0026gt;App: 处理数据 适合单线程/单进程管理多连接，减少线程切换\n事件循环典型结构：注册 fd -\u0026gt; 等待 -\u0026gt; 处理就绪 -\u0026gt; 重新注册或继续等待； 需要把 fd 设置为非阻塞，避免单个 read/write 卡住事件循环； 回调里要尽量快，耗时任务应投递到工作线程池； 要关注粘包/拆包，事件驱动只告诉“可读/可写”，应用层需处理协议边界。 信号驱动 I/O 应用进程使用 sigaction 系统调用，内核立即返回，应用进程可以继续执行，也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号，应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。\n相比于非阻塞式 I/O 的轮询方式，信号驱动 I/O 的 CPU 利用率更高。\nsequenceDiagram participant App as 应用 participant Kernel as 内核 App-\u0026gt;\u0026gt;Kernel: sigaction(SIGIO) Kernel--\u0026gt;\u0026gt;App: return App--\u0026gt;\u0026gt;App: 继续执行其他逻辑 Kernel--\u0026gt;\u0026gt;App: 发送SIGIO App-\u0026gt;\u0026gt;Kernel: recvfrom() Note left of App: blocked Kernel--\u0026gt;\u0026gt;App: data copy 完成 减少轮询开销，但信号处理需谨慎。\n需设置 F_SETOWN/F_SETSIG 等，且信号处理函数中能做的事情有限（不可重入操作要避免）； 可与 signalfd 结合在 Linux 上统一为 fd 事件，但通用性较差； 在复杂网络服务中通常被 epoll 等方案取代。 异步 I/O（AIO） 应用进程执行 aio_read 系统调用会立即返回，应用进程可以继续执行，不会被阻塞，内核会在所有操作完成之后向应用进程发送信号。\n异步 I/O 与信号驱动 I/O 的区别在于，异步 I/O 的信号是通知应用进程 I/O 完成，而信号驱动 I/O 的信号是通知应用进程可以开始 I/O\nsequenceDiagram participant App as 应用 participant Kernel as 内核 App-\u0026gt;\u0026gt;Kernel: aio_read() Kernel--\u0026gt;\u0026gt;App: return App--\u0026gt;\u0026gt;App: 继续执行 Kernel--\u0026gt;\u0026gt;Kernel: copy data from kernel to user Kernel--\u0026gt;\u0026gt;App: deliver signal specified in aio_read App--\u0026gt;\u0026gt;App: signal handler处理data Linux 传统 AIO 限制较多，推荐现代接口如 io_uring； Windows IOCP、.NET async/await、Node.js libuv 都属于异步完成通知模型； 适合高吞吐文件 I/O 或网络 I/O，减少上下文切换和拷贝。 五大模型对比 同步 vs 异步：同步在“数据复制到用户缓冲”阶段会阻塞；异步不会。\n阻塞/非阻塞/I/O 复用/信号驱动：都属同步 I/O，本质差别在“等待数据”阶段是否阻塞。\nCPU 利用率：阻塞高、非阻塞低、复用和信号介于其间、AIO 最优但实现/平台支持差异大。\n拷贝次数：经典模型仍需用户态/内核态拷贝；部分异步框架支持零拷贝（如 sendfile, splice）。\n编程复杂度：阻塞 \u0026lt; 非阻塞 \u0026lt; I/O 复用 \u0026lt; 异步 AIO；\n适用连接规模：阻塞/非阻塞适合少量连接；复用/异步适合大量连接。\nI/O 复用详解 select/poll/epoll 都是 I/O 多路复用的具体实现，select 出现的最早，之后是 poll，再是 epoll\nselect 1 int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select 允许应用程序监视一组文件描述符，等待一个或者多个描述符成为就绪状态，从而完成 I/O 操作。\nfd_set 使用数组实现，数组大小使用 FD_SETSIZE 定义，所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型：readset、writeset、exceptset，分别对应读、写、异常条件的描述符集合。\ntimeout 为超时参数，调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。\n成功调用返回结果大于 0，出错返回结果为 -1，超时返回结果为 0\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 fd_set fd_in, fd_out; struct timeval tv; // Reset the sets FD_ZERO( \u0026amp;fd_in ); FD_ZERO( \u0026amp;fd_out ); // Monitor sock1 for input events FD_SET( sock1, \u0026amp;fd_in ); // Monitor sock2 for output events FD_SET( sock2, \u0026amp;fd_out ); // Find out which socket has the largest numeric value as select requires it int largest_sock = sock1 \u0026gt; sock2 ? sock1 : sock2; // Wait up to 10 seconds tv.tv_sec = 10; tv.tv_usec = 0; // Call the select int ret = select( largest_sock + 1, \u0026amp;fd_in, \u0026amp;fd_out, NULL, \u0026amp;tv ); // Check if select actually succeed if ( ret == -1 ) // report error and abort else if ( ret == 0 ) // timeout; no event detected else { if ( FD_ISSET( sock1, \u0026amp;fd_in ) ) // input event on sock1 if ( FD_ISSET( sock2, \u0026amp;fd_out ) ) // output event on sock2 } poll 1 int poll(struct pollfd *fds, unsigned int nfds, int timeout); poll 的功能与 select 类似，也是等待一组描述符中的一个成为就绪状态。 poll 中的描述符是 pollfd 类型的数组，pollfd 的定义如下：\n1 2 3 4 5 struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // The structure for two events struct pollfd fds[2]; // Monitor sock1 for input fds[0].fd = sock1; fds[0].events = POLLIN; // Monitor sock2 for output fds[1].fd = sock2; fds[1].events = POLLOUT; // Wait 10 seconds int ret = poll( \u0026amp;fds, 2, 10000 ); // Check if poll actually succeed if ( ret == -1 ) // report error and abort else if ( ret == 0 ) // timeout; no event detected else { // If we detect the event, zero it out so we can reuse the structure if ( fds[0].revents \u0026amp; POLLIN ) fds[0].revents = 0; // input event on sock1 if ( fds[1].revents \u0026amp; POLLOUT ) fds[1].revents = 0; // output event on sock2 } select vs poll 功能: select 和 poll 的功能基本相同，不过在一些实现细节上有所不同。 select 会修改描述符，而 poll 不会； select 的描述符类型使用数组实现，FD_SETSIZE 大小默认为 1024，因此默认只能监听少于 1024 个描述符。 如果要监听更描述符的话，需要修改 FD_SETSIZE 之后重新编译；而 poll 没有描述符数量的限制； poll 提供了更多的事件类型，并且对描述符的重复利用上比 select 高。 如果一个线程对某个描述符调用了 select 或者 poll，另一个线程关闭了该描述符，会导致调用结果不确定。 速度: select 和 poll 速度都比较慢，每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。 可移植性: 几乎所有的系统都支持 select，但是只有比较新的系统支持 poll。 epoll 1 2 3 int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)； int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上，通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理，进程调用 epoll_wait() 便可以得到事件完成的描述符。\n从上面的描述可以看出，epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次，并且进程不需要通过轮询来获得事件完成的描述符。\nepoll 仅适用于 Linux OS。\nepoll 比 select 和 poll 更加灵活而且没有描述符数量限制。\nepoll 对多线程编程更有友好，一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 // Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets. // The function argument is ignored (it was not before, but now it is), so put your favorite number here int pollingfd = epoll_create( 0xCAFE ); if ( pollingfd \u0026lt; 0 ) // report error // Initialize the epoll structure in case more members are added in future struct epoll_event ev = { 0 }; // Associate the connection class instance with the event. You can associate anything // you want, epoll does not use this information. We store a connection class pointer, pConnection1 ev.data.ptr = pConnection1; // Monitor for input, and do not automatically rearm the descriptor after the event ev.events = EPOLLIN | EPOLLONESHOT; // Add the descriptor into the monitoring list. We can do it even if another thread is // waiting in epoll_wait - the descriptor will be properly added if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1-\u0026gt;getSocket(), \u0026amp;ev ) != 0 ) // report error // Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen) struct epoll_event pevents[ 20 ]; // Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); // Check if epoll actually succeed if ( ret == -1 ) // report error and abort else if ( ret == 0 ) // timeout; no event detected else { // Check if any events detected for ( int i = 0; i \u0026lt; ready; i++ ) { if ( pevents[i].events \u0026amp; EPOLLIN ) { // Get back our connection pointer Connection * c = (Connection*) pevents[i].data.ptr; c-\u0026gt;handleReadEvent(); } } } epoll的工作模式 epoll 的描述符事件有两种触发模式：LT（level trigger）和 ET（edge trigger）。\nLT（Level Trigger，默认）：当 epoll_wait() 检测到描述符事件到达时，将此事件通知进程，进程可以不立即处理该事件，下次调用 epoll_wait() 会再次通知进程。是默认的一种模式，并且同时支持 Blocking 和 No-Blocking。 ET（Edge Trigger）：和 LT 模式不同的是，通知之后进程必须立即处理事件，下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数，因此效率要比 LT 模式高。只支持 No-Blocking，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。。 select vs poll vs epoll select: timeout 参数精度为微秒，而 poll 和 epoll 为毫秒，因此 select 更加适用于实时性要求比较高的场景，比如核 反应堆的控制。select 可移植性更好，几乎被所有主流平台所支持。\npoll: 没有最大描述符数量的限制，如果平台支持并且对实时性要求不高，应该使用 poll 而不是 select。\nepoll: 只需要运行在 Linux 平台上，有大量的描述符需要同时轮询，并且这些连接最好是长连接。 需要同时监控小于 1000 个描述符，就没有必要使用 epoll，因为这个应用场景下并不能体现 epoll 的优势。 需要监控的描述符状态变化多，而且都是非常短暂的，也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中，造成 每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用，频繁系统调用降低效率。并且 epoll 的描述符存储在内 核，不容易调试。\ncopy from\nhttps://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md\n","date":"2026-01-08T15:51:28+08:00","permalink":"https://charles-7777.github.io/p/socket-i/o/","title":"Socket I/O"},{"content":"均值与期望 离散随机变量 均值（期望） 设离散随机变量 $X$ 的概率分布函数为 $P(X = x_i) = p_i$，则其数学期望（均值）定义为： $$ \\mu = E[X] = \\sum_{i=1}^{\\infty} x_i p_i $$方差 离散随机变量 $X$ 的方差定义为： $$ \\sigma^2 = D(X) = E[(X - E[X])^2] = \\sum_{i} (x_i - E[X])^2 p_i $$ 也可等价表示为： $$ D(X) = E[X^2] - (E[X])^2 $$ 连续随机变量 均值（期望） 设连续随机变量 $X$ 的概率密度函数为 $f(x)$，则其期望为： $$ \\mu = E[X] = \\int_{-\\infty}^{\\infty} x f(x) \\, dx $$方差 连续随机变量 $X$ 的方差为： $$ \\sigma^2 = D(X) = E[(X - E[X])^2] = \\int_{-\\infty}^{\\infty} (x - E[X])^2 f(x) \\, dx $$ 同样有简化公式： $$ D(X) = E[X^2] - (E[X])^2 $$ 样本方差 给定一个容量为 $n$ 的样本 $x_1, x_2, \\dots, x_n$，其样本均值为： $$ \\bar{X} = \\frac{1}{n} \\sum_{i=1}^{n} x_i $$ 样本方差（无偏估计）定义为： $$ s^2 = \\frac{1}{n - 1} \\sum_{i=1}^{n} (x_i - \\bar{X})^2 $$ 注：分母使用 $n-1$（而非 $n$）是为了使 $s^2$ 成为总体方差 $\\sigma^2$ 的无偏估计量，这一修正称为 贝塞尔修正（Bessel\u0026rsquo;s correction）。\n无偏估计简介 在统计学中，我们常常无法获取整个总体的数据，只能通过样本来推断总体的某些特征（如均值、方差等）。用于推断总体参数的样本函数称为估计量。一个重要的评价标准是该估计量是否“无偏”。\n设 $\\theta$ 是某个总体参数（例如总体均值 $\\mu$ 或总体方差 $\\sigma^2$），$\\hat{\\theta}$ 是基于样本构造的估计量。如果满足： $$ E[\\hat{\\theta}] = \\theta $$ 则称 $\\hat{\\theta}$ 是 $\\theta$ 的无偏估计量（unbiased estimator）。\n换句话说：无偏估计的期望等于它所要估计的真实参数值。这意味着，如果我们反复从总体中抽样并计算该估计量，其平均值会趋近于真实参数。\n无偏估计常见例子 样本均值是总体均值的无偏估计 设总体均值为 $\\mu$，样本为 $x_1, x_2, \\dots, x_n$，样本均值为： $$ \\bar{x} = \\frac{1}{n} \\sum_{i=1}^{n} x_i $$ 可以证明： $$ E[\\bar{x}] = E[\\frac{1}{n} \\sum_{i=1}^{n} x_i] =\\frac{1}{n} \\sum_{i=1}^{n} E[x_i] = \\frac{1}{n} n \\mu = \\mu $$ 因此，$\\bar{x}$ 是 $\\mu$ 的无偏估计。\n样本方差是总体方差的无偏估计 总体方差定义为 $\\sigma^2 = E[(X - \\mu)^2]$。若使用： $$ s^2 = \\frac{1}{n - 1} \\sum_{i=1}^{n} (x_i - \\bar{x})^2 $$ 则有： $$ E[s^2] = \\sigma^2 $$ 所以 $s^2$ 是 $\\sigma^2$ 的无偏估计。\n为什么样本方差是总体方差的无偏估计 如果已知随机变量 $ X $ 的期望为 $ \\mu $，那么可以如下计算方差 $ \\sigma^2 $： $$ \\sigma^2 = E[(X - \\mu)^2] $$上面的式子需要知道 $ X $ 的具体分布是什么（在现实应用中往往不知道准确分布），计算起来也比较复杂。\n所以实践中常常采样之后，用下面这个 $ S^2 $ 来近似 $ \\sigma^2 $：\n$$ S^2 = \\frac{1}{n} \\sum_{i=1}^{n} (x_i - \\mu)^2 $$其实现实中，往往连 $ X $ 的期望 $ \\mu $ 也不清楚，只知道样本的均值：\n$$ \\bar{X} = \\frac{1}{n} \\sum_{i=1}^{n} x_i $$那可以这么来计算 $ s^2 $：\n$$ s^2 = \\frac{1}{n - 1} \\sum_{i=1}^{n} (x_i - \\bar{X})^2 $$$$ \\begin{aligned} E(s^2) \u0026= E\\left(\\frac{1}{n-1} \\sum_{i=1}^{n} (x_i - \\bar{X})^2\\right) \\\\ \u0026= E\\left[\\frac{1}{n-1} \\sum_{i=1}^{n} (x_i)^2 - \\frac{2}{n-1} \\sum_{i=1}^{n} (x_i)(\\bar{x}) + \\frac{1}{n-1} \\sum_{i=1}^{n} (\\bar{X})^2\\right] \\\\ \u0026= E\\left[\\frac{1}{n-1} \\sum_{i=1}^{n} \\left\\{ (x_i)^2 - 2(\\bar{X})^2 + (\\bar{X})^2 \\right\\} \\right] \\\\ \u0026= E\\left[\\frac{1}{n-1} \\sum_{i=1}^{n} \\left\\{ (x_i)^2 - (\\bar{X})^2 \\right\\} \\right] \\\\ \u0026= E\\left[\\frac{1}{n-1} \\sum_{i=1}^{n} \\left\\{ (x_i)^2 - E[(\\bar{X})^2]\\right\\} \\right] \\\\ \u0026= \\frac{1}{n-1} \\sum_{i=1}^{n} \\left\\{ E[(x_i)^2] - E[(\\bar{x})^2] \\right\\} \\\\ \u0026= \\frac{1}{n-1} \\sum_{i=1}^{n} \\left\\{ D[(x_i)] + (E[x_i])^2 - (D[\\bar{X}] + (E[\\bar{X}])^2) \\right\\} \\\\ \u0026= \\frac{1}{n-1} n \\left\\{ (\\sigma^2 + \\mu^2)- (\\frac{1}{n} \\sigma^2 + \\mu^2 ) \\right\\} \\\\ \u0026=\\sigma^2 \\end{aligned} $$ $D(X) = E[X^2] - (E[X])^2$ $D[\\bar{X}]=D[\\frac{1}{n} \\sum_{i=1}^{n} x_i]= \\frac{1}{n^2} \\sum_{i=1}^{n} D[x_i] = \\frac{1}{n^2} n \\sigma^2 = \\frac{1}{n} \\sigma^2 $ 故样本方差是总体方差的无偏估计，分母使用 $n-1$（而非 $n$）是为了使 $s^2$ 成为总体方差 $\\sigma^2$ 的无偏估计量，这一修正称为 贝塞尔修正（Bessel\u0026rsquo;s correction）\n从自由度的方面看，n 个样本$x_1$ ~ $x_n$ , 但加入了一个约束$\\bar{X} = \\frac{1}{n} \\sum_{i=1}^{n} x_i$, 在已知 $n-1$ 个样本和 $\\bar{X}$ 的情况下，第n个也就确定了(一个n维向量，若有一个线性约束$k_1x_1 +\u0026hellip;+k_nx_n=c$,则其中任意一个向量可以由其余向量线性表出，且这n个向量的秩维n-1),所以$s^2$的自由度就是n-1了，也正是因为除以了正确的自由度，才得到了无偏的方差统计量。\n","date":"2025-12-31T15:17:20+08:00","permalink":"https://charles-7777.github.io/p/%E6%A0%B7%E6%9C%AC%E6%96%B9%E5%B7%AE/","title":"样本方差"},{"content":"IPv6同一前缀地址通信 具有相同前缀的主机间互访的时候，不会查找路由表，只会发NS（Neighbor Solicitation）请求，获取目的地址对应的链路层地址，然后使用该链路层地址封装要发送的数据报文\nsequenceDiagram participant PC1 participant Switch as 交换机（透明） participant PC2 note over PC1,PC2: 统一前缀：2001:db8:1::/64 PC1-\u0026gt;\u0026gt;PC2: NS (请求 2001:db8:1::20 的 MAC) PC2-\u0026gt;\u0026gt;PC1: NA (回应自己的 MAC) PC1-\u0026gt;\u0026gt;Switch: 数据帧 {dst MAC = PC2 的 MAC}\u0026lt;br/\u0026gt;IP: src=2001:db8:1::10, dst=2001:db8:1::20 Switch-\u0026gt;\u0026gt;PC2: 转发数据帧 IPv6不同前缀地址通信 PC先查看目标地址，发现不在本地前缀中；查路由表 → 下一跳 = 默认网关。如果已有 默认网关的 MAC 地址， 直接封装帧发送；如果没有 ，先发 NS（Neighbor Solicitation）请求 默认网关的 MAC，要到网关的mac再封装帧发送\nsequenceDiagram participant PC1 participant RouterA as Router (接口A) participant RouterB as Router (接口B) participant PC2 note right of RouterA: 2001:db8:1::1/64 note right of RouterB: 2001:db8:2::1/64 PC1-\u0026gt;\u0026gt;RouterA: NS (请求网关 2001:db8:1::1 的 MAC) RouterA-\u0026gt;\u0026gt;PC1: NA (回应自己的 MAC) PC1-\u0026gt;\u0026gt;RouterA: IP 数据包 {src=2001:db8:1::10, dst=2001:db8:2::20} RouterA-\u0026gt;\u0026gt;RouterB: 路由查找，准备从接口B转发 RouterB-\u0026gt;\u0026gt;PC2: NS (请求 2001:db8:2::20 的 MAC) PC2-\u0026gt;\u0026gt;RouterB: NA RouterB-\u0026gt;\u0026gt;PC2: IP 数据包 {src=2001:db8:1::10, dst=2001:db8:2::20} IPv6 ND proxy 具有相同前缀的主机间互访的时候，不会查找路由表，只会发NS（Neighbor Solicitation）请求，获取目的地址对应的链路层地址，然后使用该链路层地址封装要发送的数据报文。\n在实际组网中，具有相同前缀的主机可能不在一个网络广播域当中，这种情况下NS（Neighbor Solicitation）报文不能发送到目的主机，导致主机间不能互访。\n为了避免此问题，可以开启路由器的ND Proxy功能，用于替代目的主机回应NA（Neighbor Advertise）报文，报文中会携带路由器的链路层地址。采用这种方式，两个主机可以通过路由器设备完成互访\nsequenceDiagram participant PC1 participant 路由器 as A-路由器-B participant PC2 Note over PC1, PC2: PC1 需要向 PC2 发送数据报文 PC1-\u0026gt;\u0026gt;路由器: NS(请求 1234::2/64 的 MAC 地址)\u0026lt;br\u0026gt;通过接口A 路由器--\u0026gt;\u0026gt;PC1: NA(回应 1234::2/64 的 MAC 地址是接口A的MAC地址) PC1-\u0026gt;\u0026gt;路由器: 目的是 1234::2/64 的数据报文\u0026lt;br\u0026gt;通过接口A 路由器-\u0026gt;\u0026gt;PC2: 目的是 1234::2/64 的数据报文\u0026lt;br\u0026gt;通过接口B Note over PC2, PC1: PC2 需要向 PC1 发送数据报文 PC2-\u0026gt;\u0026gt;路由器: NS(请求 1234::1/64 的 MAC 地址)\u0026lt;br\u0026gt;通过接口B 路由器--\u0026gt;\u0026gt;PC2: NA(回应 1234::1/64 的 MAC 地址是接口B的MAC地址) PC2-\u0026gt;\u0026gt;路由器: 目的是 1234::1/64 的数据报文\u0026lt;br\u0026gt;通过接口B 路由器-\u0026gt;\u0026gt;PC1: 目的是 1234::1/64 的数据报文\u0026lt;br\u0026gt;通过接口A ","date":"2025-12-29T16:28:14+08:00","permalink":"https://charles-7777.github.io/p/ipv6-nd-proxy/","title":"IPv6 ND proxy"},{"content":"DHCPv6 (Dynamic Host Configuration Protocol for IPv6) 为 IPv6 主机提供了一种有状态的地址配置机制。与 IPv4 DHCP 类似，DHCPv6 分配的地址也是有租期的，但其生命周期管理引入了更细致的状态和计时器机制。\n核心概念 在理解生命周期之前，需要了解以下几个关键参数，这些参数通常由 DHCPv6 服务器在 IA Address Option (IAADDR) 中下发：\nPreferred Lifetime (首选生命周期):\n地址处于“首选 (Preferred)”状态的时间长度。 在此期间，地址可以无限制地用于新的和现有的通信。 Valid Lifetime (有效生命周期):\n地址处于“有效 (Valid)”状态的总时间长度。 它总是大于或等于首选生命周期。 一旦超过这个时间，地址即变为“无效 (Invalid)”，不能再使用。 T1 (Renew Timer - 更新计时器):\n客户端向分配该地址的特定服务器发起续租请求的时间点。 通常建议设置为首选生命周期的 0.5 倍。 T2 (Rebind Timer - 重绑定计时器):\n如果 T1 时刻续租失败，客户端向任意可用的服务器发起续租请求的时间点。 通常建议设置为首选生命周期的 0.8 倍。 地址状态流转 IPv6 地址在生命周期中会经历以下几种状态：\nTentative (试探态): 客户端刚收到地址，正在进行重复地址检测 (DAD - Duplicate Address Detection)。 此时地址还不能用于通信。 如果 DAD 成功（无冲突），进入 Preferred 状态；如果失败，地址不可用。 Preferred (首选态): DAD 通过后，且当前时间 \u0026lt; Preferred Lifetime。 地址可以用于建立新的连接，也可以用于现有的连接。 这是地址正常工作的状态。 Deprecated (弃用态): Preferred Lifetime \u0026lt; 当前时间 \u0026lt; Valid Lifetime。 地址依然“有效”，现有的连接可以继续使用该地址（为了不中断正在进行的会话）。 但是，上层应用不应使用该地址建立新的连接（源地址选择算法会避免选它）。 Invalid (无效态): 当前时间 \u0026gt; Valid Lifetime。 地址从接口上删除，彻底不可用。Microsoft 365 生命周期流程详解 分配 (Allocation): 客户端通过 Solicit/Advertise/Request/Reply 四步交互（或两步 Rapid Commit）获取地址及 T1, T2, Lifetimes 参数。 DAD 检测: 客户端对新地址进行 DAD 检测。 使用 (Usage): 进入 Preferred 状态，T1, T2, Lifetime 倒计时开始。 更新 (Renew - T1): 当达到 T1 时间，客户端发送 Renew 报文给原服务器。 如果服务器回复 Reply 并刷新了时间，生命周期重置，回到初始 Preferred 状态。 重绑定 (Rebind - T2): 如果 T1 期间没收到回复，客户端继续使用地址。 当达到 T2 时间，客户端发送 Rebind 报文广播给所有服务器。 如果有服务器回复，生命周期重置。 弃用 (Deprecation): 如果 T2 也没成功，且时间超过了 Preferred Lifetime，地址进入 Deprecated 状态。 停止建立新连接。 过期 (Expiration): 时间超过 Valid Lifetime，地址被回收。 stateDiagram-v2 [*] --\u0026gt; Tentative Tentative --\u0026gt; Preferred : DAD 成功 Tentative --\u0026gt; [*] : DAD 失败 Preferred --\u0026gt; Deprecated : t \u0026gt; Preferred Lifetime Deprecated --\u0026gt; Invalid : t \u0026gt; Valid Lifetime Preferred --\u0026gt; Invalid : t \u0026gt; Valid Lifetime Preferred --\u0026gt; Preferred : Renew/Rebind 成功 ","date":"2025-12-24T11:39:28+08:00","permalink":"https://charles-7777.github.io/p/dhcpv6-address-lifecycle/","title":"DHCPv6 Address Lifecycle"},{"content":"本文介绍 Wi-Fi 物理层协商速率（PHY Rate）的计算方法，并提供 802.11ac (Wi-Fi 5) 和 802.11ax (Wi-Fi 6) 的 MCS (Modulation and Coding Scheme) 表。\n计算公式 Wi-Fi 的理论物理层速率可以通过以下公式计算：\n$$ \\text{Data Rate} = N_{ss} \\times \\frac{N_{sd} \\times N_{bps} \\times R}{T_{sym}} $$例如 ac 40Mhz 4x4 的最大速率 $ \\text{Rate} = 4 \\times \\frac{108 \\times 8 \\times (5/6)}{3.6 \\mu s} = 800 Mbps$\n其中各参数含义如下：\n参数 含义 说明 $N_{ss}$ Spatial Streams 空间流数量 (MIMO)，例如 1x1, 2x2, 3x3, 4x4。$Nss,max=min(N_{Tx},N_{Rx})$ $N_{sd}$ Data Subcarriers 数据子载波数量。取决于信道带宽 (20/40/80/160 MHz) 和协议标准 (11n/ac/ax)。 $N_{bps}$ Bits per Symbol 每个符号携带的比特数。由调制方式决定 (e.g., 64-QAM = 6 bits, 256-QAM = 8 bits, 1024-QAM = 10 bits)。 $R$ Coding Rate 编码率。有效数据位与总传输位（含纠错码）的比率 (e.g., 3/4, 5/6)。 $T_{sym}$ Symbol Duration 符号持续时间。$T_{sym} = T_{base} + T_{GI}$ (Guard Interval)。 简化理解 速率主要由以下几个维度决定：\n频宽 (Bandwidth): 车道越宽，通过的车越多 (20MHz vs 160MHz)。\n调制方式 (Modulation): 车装的货越多 (QAM 阶数越高)。\nMIMO (Spatial Streams): 高架桥层数越多 (天线数)。\n保护间隔 (Guard Interval): 车距越小，发车越快。 符号持续时间 标准 子载波间隔 基础符号时间 ($T_{base}$) 保护间隔 ($T_{GI}$) 总符号时间 ($T_{sym}$) 802.11n/ac 312.5 kHz 3.2 µs 0.4 µs (Short GI) 3.6 µs 802.11n/ac 312.5 kHz 3.2 µs 0.8 µs (Long GI) 4.0 µs 802.11ax 78.125 kHz 12.8 µs 0.8 µs 13.6 µs 802.11ax 78.125 kHz 12.8 µs 1.6 µs 14.4 µs 802.11ax 78.125 kHz 12.8 µs 3.2 µs 16.0 µs 数据子载波数量 频宽 (Bandwidth) 802.11ac (Wi-Fi 5) 802.11ax (Wi-Fi 6) 20 MHz 52 234 40 MHz 108 468 80 MHz 234 980 160 MHz 468 1960 802.11ac: 使用 312.5 kHz 的子载波间隔, 802.11ax: 使用更密集的 78.125 kHz 子载波间隔（是 ac 的 1/4） 20MHz/312.5kHz=64,这个 64 正是 Wi-Fi (802.11ac) 在 20MHz 频宽下的 FFT 大小 (总子载波数量)。但在实际传输中，并不是这 64 个子载波都能用来传数据。为了避免干扰和同步信号.它们被分配如下52(数据子载波)+4(导频子载波)+1(直流子载波)+7(保护子载波)=64;802.11ax的 234+8+3+11=256\n802.11ac (Wi-Fi 5) MCS 表 标准特性:\n最高支持 256-QAM (MCS 8, 9)。\n支持 20/40/80/160 MHz 频宽。\nGuard Interval (GI): Long (800ns), Short (400ns)。\n以下表格展示 单流 (1 Spatial Stream) 在 Short GI (400ns) 下的速率 (Mbps)。\n如果是多流 (如 2x2)，将下表速率乘以 2 即可。\nMCS Index Modulation Coding Rate 20 MHz 40 MHz 80 MHz 160 MHz 0 BPSK 1/2 7.2 15.0 32.5 65.0 1 QPSK 1/2 14.4 30.0 65.0 130.0 2 QPSK 3/4 21.7 45.0 97.5 195.0 3 16-QAM 1/2 28.9 60.0 130.0 260.0 4 16-QAM 3/4 43.3 90.0 195.0 390.0 5 64-QAM 2/3 57.8 120.0 260.0 520.0 6 64-QAM 3/4 65.0 135.0 292.5 585.0 7 64-QAM 5/6 72.2 150.0 325.0 650.0 8 256-QAM 3/4 86.7 180.0 390.0 780.0 9 256-QAM 5/6 N/A 200.0 433.3 866.7 注: 常见的 \u0026ldquo;433 Mbps\u0026rdquo; 就是 11ac 1x1 80MHz MCS9 的速率；\u0026ldquo;866 Mbps\u0026rdquo; 是 2x2 的速率。\n802.11ax (Wi-Fi 6) MCS 表 标准特性:\n引入 1024-QAM (MCS 10, 11)。\n更高效的子载波利用率 (更多的数据子载波)。\nGuard Interval (GI): 0.8us, 1.6us, 3.2us。通常使用 0.8us 获得最高速率。\n以下表格展示 单流 (1 Spatial Stream) 在 0.8us GI 下的速率 (Mbps)。\nMCS Index Modulation Coding Rate 20 MHz 40 MHz 80 MHz 160 MHz 0 BPSK 1/2 8.6 17.2 36.0 72.1 1 QPSK 1/2 17.2 34.4 72.1 144.1 2 QPSK 3/4 25.8 51.6 108.1 216.2 3 16-QAM 1/2 34.4 68.8 144.1 288.2 4 16-QAM 3/4 51.6 103.2 216.2 432.4 5 64-QAM 2/3 68.8 137.6 288.2 576.5 6 64-QAM 3/4 77.4 154.9 324.3 648.5 7 64-QAM 5/6 86.0 172.1 360.3 720.6 8 256-QAM 3/4 103.2 206.5 432.4 864.7 9 256-QAM 5/6 114.7 229.4 480.4 960.8 10 1024-QAM 3/4 129.0 258.1 540.4 1080.9 11 1024-QAM 5/6 143.4 286.8 600.5 1201.0 注: Wi-Fi 6 的 \u0026ldquo;1201 Mbps\u0026rdquo; 是 2x2 80MHz MCS11 的速率 (600.5 * 2)。\n参考链接\nhttps://zhuanlan.zhihu.com/p/1944204299568677330\nhttps://www.openwrt.pro/post-74.html\n","date":"2025-12-12T15:25:55+08:00","permalink":"https://charles-7777.github.io/p/wi-fi-%E9%80%9F%E7%8E%87%E8%AE%A1%E7%AE%97/","title":"Wi-Fi 速率计算"},{"content":" 聚类（clustering）是一种寻找数据之间内在结构的技术。聚类把全体数据实例组织成一些相似组，而这些相似组被称作簇。处于相同簇中的数据实例彼此相同，处于不同簇中的实例彼此不同。K-Means、AgglomerativeClustering、DBSCAN、MeanShift、SpectralClustering 等是常用的聚类方法。 K-Means 算法又名 K 均值算法，K-Means 算法中的 K 表示的是聚类为 K 个簇，Means 代表取每一个聚类中数据值的均值作为该簇的中心，或者称为质心，即用每一个类的质心对该簇进行描述。\nK-Means 算法原理 简介 K-Means 是一种广泛使用的无监督学习(没标准答案下去学习)算法，用于将数据点划分为 k 个簇（Cluster）。其目标是使同一簇内的数据点尽可能相似，而不同簇之间的数据点尽可能不同。\n算法原理 K-Means 算法是一个迭代过程，主要包含以下步骤：\n初始化 (Initialization): 随机选择 k 个数据点作为初始质心（Centroids）。 分配 (Assignment): 计算每个数据点到这 k 个质心的距离，并将每个数据点分配给最近的质心所代表的簇。 更新 (Update): 重新计算每个簇的质心。新的质心是该簇内所有数据点的平均值（均值）。 重复 (Repeat): 重复步骤 2 和 3，直到满足停止条件（例如：质心不再发生变化、达到最大迭代次数或误差平方和收敛） 数学公式 目标函数 K-Means 的目标是最小化簇内误差平方和(Within-Cluster Sum of Squares, WCSS)，也称为惯性（Inertia）。\n$$ J =SEE= \\sum_{i=1}^k\\sum_{x\\in C_i}||x-\\mu_i||^2 $$其中：\nJ 是目标函数值:整个数据集的误差平方和（Sum of Squared Error，SSE）。 k 是簇的数量。 $C_i$​ 是第 i 个簇。 X 是簇 $C_i$中的数据点。 $\\mu_i$是簇 $C_i$的质心。 $||x-\\mu_i||^2$ 是数据点 x 到质心 $\\mu_i$​ 的欧几里得距离的平方 质心更新公式 在第 t+1 次迭代中，簇$C_i$ 的新质心$\\mu_i^{(t+1)}$ 计算如下：\n$$ \\mu_i^{(t+1)} = \\frac{1}{|C_i^{(t)}|} \\sum_{x\\in C_i^{(t)}} x $$其中 $|C_i^{(t)}|$ 是第 t 次迭代中簇 $C_i$内数据点的数量。\n简单样例说明 假设我们有以下一维数据点：[2, 4, 10, 12, 3, 20, 30, 11, 25]，我们想将其分为 k=2 个簇。\n初始化: 随机选 2 和 30 为质心。 μ1​=2, μ2​=30 分配: 靠近 2 的点: {2, 3, 4, 10, 11, 12} -\u0026gt; 簇 1 靠近 30 的点: {20, 25, 30} -\u0026gt; 簇 2 更新: 新 μ1​=(2+3+4+10+11+12)/6=42/6=7 新 μ2​=(20+25+30)/3=75/3=25 迭代: 使用新质心 7 和 25 重新分配，直到质心稳定 Python 代码样例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from matplotlib.animation import FuncAnimation # ========================================== # 1. 数据准备 # ========================================== print(\u0026#34;Generating data...\u0026#34;) X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.60, random_state=0) # ========================================== # 2. K-Means 算法手动实现 (为了记录每一步) # ========================================== class KMeansManual: def __init__(self, X, k=4): self.X = X self.k = k # 随机选择k个点作为初始质心 indices = np.random.choice(X.shape[0], k, replace=False) self.centroids = X[indices] self.labels = None self.history = [] # 用于存储每一步的状态: (centroids, labels, title) def run(self, max_iters=10): # 记录初始状态 self.history.append((self.centroids.copy(), None, \u0026#34;Initialization\u0026#34;)) for i in range(max_iters): # --- 步骤 1: 分配 (Assignment) --- # 计算距离矩阵: (k, n_samples) # 利用广播机制计算每个点到每个质心的欧氏距离 distances = np.linalg.norm(self.X[:, np.newaxis] - self.centroids, axis=2) # 获取最近质心的索引 self.labels = np.argmin(distances, axis=1) # 记录分配后的状态 self.history.append((self.centroids.copy(), self.labels.copy(), f\u0026#34;Iteration {i+1}: Assignment\u0026#34;)) # --- 步骤 2: 更新 (Update) --- new_centroids = np.array([ self.X[self.labels == j].mean(axis=0) if np.sum(self.labels == j) \u0026gt; 0 else self.centroids[j] for j in range(self.k) ]) # 记录更新质心后的状态 self.history.append((new_centroids.copy(), self.labels.copy(), f\u0026#34;Iteration {i+1}: Update Centroids\u0026#34;)) # --- 检查收敛 --- if np.allclose(self.centroids, new_centroids): print(f\u0026#34;Converged at iteration {i+1}\u0026#34;) break self.centroids = new_centroids # 运行算法 k = 4 kmeans = KMeansManual(X, k=k) kmeans.run() # ========================================== # 3. 动画可视化 # ========================================== print(\u0026#34;Starting animation...\u0026#34;) fig, ax = plt.subplots(figsize=(10, 7)) def update(frame_idx): ax.clear() centroids, labels, title = kmeans.history[frame_idx] # 绘制数据点 if labels is None: # 初始状态，未分类，显示为灰色 ax.scatter(X[:, 0], X[:, 1], c=\u0026#39;gray\u0026#39;, s=50, alpha=0.5) else: # 已分类，按类别着色 ax.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap=\u0026#39;viridis\u0026#39;, alpha=0.6) # 绘制质心 (红色大叉) ax.scatter(centroids[:, 0], centroids[:, 1], c=\u0026#39;red\u0026#39;, s=200, marker=\u0026#39;X\u0026#39;, edgecolors=\u0026#39;white\u0026#39;, linewidths=2, label=\u0026#39;Centroids\u0026#39;) # 绘制质心移动轨迹 (可选，如果不是第一帧) if frame_idx \u0026gt; 0: prev_centroids, _, _ = kmeans.history[frame_idx-1] # 简单的绘制从上一个位置到当前位置的线 for i in range(k): ax.plot([prev_centroids[i, 0], centroids[i, 0]], [prev_centroids[i, 1], centroids[i, 1]], \u0026#39;k--\u0026#39;, alpha=0.5) ax.set_title(title, fontsize=14) ax.set_xlabel(\u0026#34;Feature 1\u0026#34;) ax.set_ylabel(\u0026#34;Feature 2\u0026#34;) ax.legend() ax.grid(True, alpha=0.3) # 创建动画 # interval=1000 表示每帧停留 1000ms (1秒) ani = FuncAnimation(fig, update, frames=len(kmeans.history), interval=2000, repeat=True) plt.show() K-Means算法的问题 K如何确定 手肘法 算法所需要预设的参数至少有2个：簇个数K和初始簇质心\n聚类的目标是使得每个样本点到距离其最近的聚类中心的总误差平方和(SSE)尽可能小。\n理论上随着K的增加，SSE会单调递减，因为类数的增加意味着总有一部分样本点会因为归属到新的类簇而节约下一段距离，直到K=N时，情况将演变为每个样本自成一类，此时SSE值就会降为0,这也就没意义了。\n根据学者们的长期实践经验，K值最大不应超过样本量的开平方根，即$K_{max}\u0026lt;= \\sqrt N$ 。而确定了范围后，最优K值又应该怎么判断？一种简单的思路是：试图找到某一个K值，要求当K大于该值时，SSE的下降变化幅度（或速度）明显变小。换句话说，当K超过某一个数后，每个类簇的聚合程度不再获得显著提升，此时我们就可以认为已找到最佳K的取值。这也是手肘法通过画出不同K值与SSE值的折线图，若SSE值下降过程中存在“肘点”（下降速度骤减的拐点处），该点所对应的K值即合适的聚类数。不过遗憾的是，若SSE的下降是均匀的，传统的肘部图法也就失灵了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.datasets import make_blobs # ========================================== # 1. 数据准备 # ========================================== print(\u0026#34;Generating data...\u0026#34;) # 生成稍微复杂一点的数据，以便观察肘部 X, y_true = make_blobs(n_samples=500, centers=5, cluster_std=0.8, random_state=42) # ========================================== # 2. 计算不同 K 值的 WCSS (Inertia) # ========================================== wcss = [] # Within-Cluster Sum of Squares (簇内误差平方和) k_range = range(1, 11) # 测试 k 从 1 到 10 print(\u0026#34;Calculating WCSS for different k values...\u0026#34;) for k in k_range: kmeans = KMeans(n_clusters=k, random_state=42) kmeans.fit(X) wcss.append(kmeans.inertia_) # inertia_ 属性就是 WCSS print(f\u0026#34;k={k}, WCSS={kmeans.inertia_:.2f}\u0026#34;) # ========================================== # 3. 绘制肘部图 (Elbow Plot) # ========================================== plt.figure(figsize=(10, 6)) plt.plot(k_range, wcss, marker=\u0026#39;o\u0026#39;, linestyle=\u0026#39;-\u0026#39;, color=\u0026#39;b\u0026#39;) # 标注轴和标题 plt.title(\u0026#39;Elbow Method For Optimal k (肘部法则)\u0026#39;, fontsize=16) plt.xlabel(\u0026#39;Number of clusters (k)\u0026#39;, fontsize=12) plt.ylabel(\u0026#39;WCSS (Inertia)\u0026#39;, fontsize=12) plt.xticks(k_range) plt.grid(True) # 标记可能的肘部点 (在这个例子中，我们知道真实中心是5，所以肘部应该在5附近) # 这里为了演示，我们简单标注一下 k=5 的位置 optimal_k = 5 plt.annotate(f\u0026#39;Possible Elbow Point (k={optimal_k})\u0026#39;, xy=(optimal_k, wcss[optimal_k-1]), xytext=(optimal_k+1, wcss[optimal_k-1]+1000), arrowprops=dict(facecolor=\u0026#39;red\u0026#39;, shrink=0.05), fontsize=12, color=\u0026#39;red\u0026#39;) plt.show() 轮廓系数（Silhouette Coefficient）法 是一种用于评估聚类效果并帮助确定最优聚类数 K 的方法。它结合了凝聚度（cohesion）和分离度（separation）两个指标\n轮廓系数的定义 对于数据集中的每一个样本点 $x_i$​，其轮廓系数 $s(i)$ 定义为：\n$$ s(i) = \\frac{b(i) - a(i)}{\\max\\{a(i), b(i)\\}} $$其中： a(i)：样本 xi​ 到同簇内其他点的平均距离（衡量凝聚度）。 b(i)：样本 xi​ 到最近的其他簇中所有点的平均距离（衡量分离度）。\n轮廓系数的取值范围为 [−1,1]： - 接近 1：表示样本聚类合理，簇内紧密、簇间分离良好。 - 接近 0：表示样本在两个簇边界上。 - 接近 -1：表示样本可能被分配到了错误的簇。\n使用轮廓系数确定 K 值的步骤 对不同的 K 值（如 K=2,3,\u0026hellip;,Kmax​）运行 K-means 聚类。 对每次聚类结果，计算每个样本的轮廓系数，再求所有样本的平均轮廓系数。 选择使平均轮廓系数最大的 K 值作为最优聚类数. 注意：轮廓系数法假设簇是凸形且大小相近的，对于非球形或密度差异大的簇可能不适用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.datasets import make_blobs from sklearn.metrics import silhouette_score # ========================================== # 1. 数据准备 # ========================================== print(\u0026#34;Generating data...\u0026#34;) # 生成与之前类似的模拟数据，预设中心为 5 X, y_true = make_blobs(n_samples=500, centers=5, cluster_std=0.8, random_state=42) # ========================================== # 2. 计算不同 K 值的轮廓系数 (Silhouette Score) # ========================================== silhouette_scores = [] # 注意：轮廓系数至少需要 2 个簇才能计算，所以从 k=2 开始 k_range = range(2, 11) print(\u0026#34;Calculating Silhouette Scores for different k values...\u0026#34;) for k in k_range: kmeans = KMeans(n_clusters=k, random_state=42) labels = kmeans.fit_predict(X) # 计算所有样本的平均轮廓系数 # 轮廓系数范围是 [-1, 1]，越接近 1 表示聚类效果越好 score = silhouette_score(X, labels) silhouette_scores.append(score) print(f\u0026#34;k={k}, Silhouette Score={score:.4f}\u0026#34;) # ========================================== # 3. 绘制轮廓系数图 # ========================================== plt.figure(figsize=(10, 6)) plt.plot(k_range, silhouette_scores, marker=\u0026#39;o\u0026#39;, linestyle=\u0026#39;-\u0026#39;, color=\u0026#39;g\u0026#39;, linewidth=2) # 标注轴和标题 plt.title(\u0026#39;Silhouette Method For Optimal k\u0026#39;, fontsize=16) plt.xlabel(\u0026#39;Number of clusters (k)\u0026#39;, fontsize=12) plt.ylabel(\u0026#39;Silhouette Score (Avg)\u0026#39;, fontsize=12) plt.xticks(k_range) plt.grid(True, alpha=0.3) # 标记最高分 (最佳 K 值) best_k_idx = np.argmax(silhouette_scores) best_k = k_range[best_k_idx] best_score = silhouette_scores[best_k_idx] plt.annotate(f\u0026#39;Best k={best_k}\\nScore={best_score:.3f}\u0026#39;, xy=(best_k, best_score), xytext=(best_k, best_score - 0.1), # 文本位置稍微往下一点 arrowprops=dict(facecolor=\u0026#39;red\u0026#39;, shrink=0.05), fontsize=12, color=\u0026#39;red\u0026#39;, ha=\u0026#39;center\u0026#39;) print(f\u0026#34;The optimal k according to Silhouette Score is {best_k}\u0026#34;) plt.show() 间隔统计法（gap statistic） Gap统计量基于以下假设：如果聚类是有意义的，那么数据集中的样本点应该比随机数据更紧密地聚集在一起。因此，Gap统计量计算了实际数据集的WCSS与随机数据集WCSS的期望值之间的差异。\n对于每一个K值，首先运行K-means算法，得到一个群内平方和。 然后，生成一组随机数据，并用相同的K值运行K-means算法。 比较真实数据的群内平方和和随机数据的结果，并计算他们之间的差距（称之为间隔值）。 对于多个K值，重复以上步骤，并选择拥有最大间隔值的K 初始的簇质心的选取 常用分析软件中的功能模块或函数包，基本上都已经代替使用者们自动预设了随机初始点，只需填入目标K值，就可以跑动算法。但实际上，K-Means对初始聚类中心的位置十分敏感，每次迭代，初始点的不同往往会导致不同的聚类结果。此外过于临近的初始中心点，有时还会导致模型的收敛时间变长（即Step4中迭代时间变长）。一种简单粗暴的解决方式是，选择不同的初始聚类中心，多次运行算法，挑出聚类效果更佳（SSE更小）、解释性更强的一组结果。\n当然了，我们或许更想知道算法上的改进手段。一种常见的优化方法是采用最大距离法，如：首先选取数据集中距离最大的两个点作为初始聚类中心，将剩余数据对象依据到聚类中心点距离的远近分配到相应的簇中，并更新聚类中心，然后继续寻找与聚类中心距离最远的点作为下一个中心点……\n与此类似地还有K-Means++ 算法，它是传统K-Means的改良版，同样是基于最大距离，这里结合加权概率的思想优化了对K个初始中心的选取，使得在选取第n+1(n+1\u0026lt;k)个聚类中心时，距离当前n个聚类中心越远的点会有更高的概率被选为第n+1个聚类中心。还有学者从点集密度的角度改进，又或者将优化搜索算法（如模拟退火、生物遗传算法等）运用到了聚类中心的选取中\u0026hellip;\u0026hellip;\n相似性与距离度量问题 特征量化后，不同个体的相似性反映在了向量之间的空间距离大小，常见的度量方法包括欧几里得距离、曼哈顿距离等等，有时我们还会用到余弦相似度等（如计算文档相似性）。而通常情况下，欧氏距离计算就可以满足我们对实现K-Means的需要。根据距离的度量方式容易发现，K-Means所划分出的类别是类球形的，换句话说，只有类球型分布的连续型样本数据，才能得到较好的聚类效果，而如果非数值型、样本类别极不平衡、非球形的分类，则聚类效果会受限。对于非理想情形的数据，有时我们就需要做一些灵活变通了。如，若数据为离散型，均值没有定义，我们可以采用K-众数(每个簇的质心不是均值，而是每个维度上出现频率最高的类别)的方法。如果样本类别极不平衡或者是非球型，则可能考虑更换聚类方法，如使用基于密度的聚类（经典的DBSCAN算法）、层次聚类法等等。\n聚类时间问题 上面的流程中提到，当聚类中心不再改变时（数学上即要求SSE函数收敛），我们认为聚类过程结束，但是这并不是唯一的结束信号。为了节省计算时间，有时我们也会通过设置迭代次数、设置簇内平方和或SSE下降阈值，又或者替换为“直到仅有1%的点改变簇”这样的弱条件，来控制算法的进程。出于对问题复杂度和计算量的合理预判，若聚类中心的更新超过了迭代次数上限，或者代价函数SSE已经小于所设定的阈值，我们都有理由提前终止。\n此外，为了提高收敛速度，还可以考虑采用二分K-Means法，将所有点作为一个簇，将该簇一分为二，然后选择能最大程度降低聚类代价函数的簇划分为两个簇，以此进行下去，直到簇的数目等于给定的个数K为止。值得一提的是，该法更突出的优点在于能够很好地解决K-Means收敛到局部最优的问题，帮助我们找到全局最优解\n标准化问题 考虑到我们所研究的对象通常包含多列数据，这些数据代表不同方面的属性值，在单位和数量级上可能存在较大的差别，因此为了避免这些差异可能引发的计算精度下降等问题，对于连续属性，可以先对数据进行规范化处理（如零均值规范化、最大最小值规范化等），再进行距离的计算\n","date":"2025-12-01T17:11:41+08:00","permalink":"https://charles-7777.github.io/p/k-means-%E8%81%9A%E7%B1%BB%E7%AE%97%E6%B3%95/","title":"K-Means 聚类算法"},{"content":" 智能体（Agent）与 MCP 简介 智能体（AI Agent） 发展脉络：\n2023 年：AutoGPT 等自治代理出现，将 GPT-4 与工具循环结合，标志着“让 LLM 自主执行复杂任务”的探索开始，被视为 智能体元年。 2024 年：LangChain、HuggingGPT、MetaGPT 等框架涌现，支持构建单/多智能体系统；产业界高度关注，IDC 预测 2026 年 50% 的大型企业数据团队将使用 AI Agent，Gartner 将 “Agentic AI” 列为 2025 年首要技术趋势。 核心机制：\n智能体赋予 LLM 自主决策与行动能力，不再只是被动回答。\n采用 “思考–行动–观察”循环机制：\n思考：LLM 接收目标后进行推理； 行动：决定调用工具（如搜索、执行代码）； 观察：获取环境反馈，继续迭代直至任务完成。 在大模型生态中的角色：\n智能体是 连接大模型与实际应用的桥梁。\n封装业务逻辑、工具调用（如 Function Calling、RAG），将企业知识库、数据库、第三方服务融入决策。\n扮演“大脑指挥官”，协调记忆、规划、工具接口等模块，驱动任务流程。\n例如：客服场景中，智能体可自主引导对话、查询数据库、转接人工，提升解决率。\nMCP（Model Context Protocol，模型上下文协议） 起源与概念：\n由 Anthropic 团队于 2024 年 11 月提出，受“语言服务器协议（LSP）”启发。 被誉为 AI 世界的“USB-C 接口”，提供模型与外部工具/数据交互的统一标准。 解决问题：此前各模型厂商的 Function Calling 实现五花八门，开发者需为每种模型+工具编写特定集成代码，繁琐且难扩展。 作用： 通过 标准化协议，让 LLM 能便捷接入数据库、搜索引擎、插件等外部资源。 支持 检索增强（RAG）、实时数据查询、API 调用等，突破训练语料限制。 提升回答的 准确性、实时性与行动能力。 LLM、智能体、MCP 定位与职责 组件 角色 职责 智能体（Agent） MCP 主机 / 用户交互入口 接收用户请求，调用 LLM，整合工具结果，返回最终答案 大模型（LLM） 智能体的“大脑” 理解用户意图，推理决策，判断是否调用工具，生成结构化调用指令或最终回复 MCP 客户端（Client） LLM 与外部资源的“信使” 解析 LLM 的工具调用请求，按 MCP 协议转发给对应服务器 MCP 服务器（Server） 工具/数据提供者 提供 Tools（可执行函数） 和 Resources（数据源），执行操作并返回结果 💡 补充：智能体应用通常内置 MCP 客户端，可同时连接多个 MCP 服务器。开发者只需将工具以 MCP Server 形式暴露，即可被任意兼容 MCP 的 LLM 调用。\n调用与通信顺序（完整交互流程） 以下以“用户询问：明天想在上海玩一天，看看天气，怎么安排行程？”为例：\n用户提问 → 在前端输入问题。\n智能体转发问题 → 将用户请求 + 上下文 + 可用工具列表发送给 LLM。\nLLM 理解与决策 → 判断是否需外部信息。若需（如查天气），进入工具调用流程。\nLLM 发出函数调用请求 → 输出结构化指令，例如：\n1 2 3 4 5 6 7 8 { \u0026#34;tool_calls\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;arguments\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34; } } ] } MCP 客户端解析并调用工具 → 识别调用请求，通过 MCP 协议（如 JSON-RPC 2.0）转发给对应 MCP 服务器。\nMCP 服务器执行操作 → 调用内部函数（如访问天气 API），获取实时数据。\n返回执行结果 → 服务器将结果（如 JSON 或文本）返回给客户端，客户端传回 LLM。\nLLM 生成最终答案 → 结合原始问题 + 外部数据，生成完整回复（如包含天气的游玩攻略）。\n结果返回用户 → 智能体将最终答案展示给用户，任务完成。\n✅ 整个流程形成闭环：用户 → Agent → LLM → MCP Client → MCP Server → LLM → Agent → 用户\n总结 LLM 是推理核心，但缺乏实时性和行动力；\nAgent 赋予 LLM 目标导向的自主性，是落地关键；\nMCP 提供标准化“插拔式”接口，极大降低工具集成成本；\n三者协作，使大模型从“聊天机器人”进化为“数字员工”，真正解决复杂业务问题。\n转载自 https://zhuanlan.zhihu.com/p/1910411281988588717\n","date":"2025-11-24T14:44:59+08:00","permalink":"https://charles-7777.github.io/p/agentmcp%E4%B8%8Ellm/","title":"Agent、MCP与LLM"},{"content":"修改/etc下的文件 通常/etc下的文件都是read-only的，但是可以通过临时 overlay 或 bind mount去绕过只读限制\n1 2 3 4 5 6 7 8 9 10 mkdir /tmp/etc # 挂载一个可写的 tmpfs 到 /tmp/etc mount -t tmpfs tmpfs /tmp/etc # 复制原 /etc 内容 cp -a /etc/* /tmp/etc/ # 重新挂载到 /etc（需要支持 bind mount） mount --bind /tmp/etc /etc 这样 /etc 就可写了，但重启后失效。适用于临时调试\n","date":"2025-09-28T16:37:56+08:00","permalink":"https://charles-7777.github.io/p/linux%E6%9D%82%E8%AE%B0/","title":"Linux杂记"},{"content":"Linux网桥的实现 linux内核实现虚拟的网桥设备，并绑定若干个以太网口（目前网桥只支持以太网接口）。 交换机对收到的数据包，只能丢弃或转发。但是linux内核设备不一样，他可能本身就是数据包的目的地。所以除了丢弃转发，他还会发往协议层自己消化。 网桥是在数据链路层实现的，他的上面是邻居子系统和网络层。\nlinux网桥数据结构 虚拟网桥也是一个网络设备，基本结构体为net_device，net_device –\u0026gt; priv下存储了网桥数据结构的私有变量net_bridge。上图中一些重要的数据结构如下：\nnet_bridge：网桥私有数据\nnet_bridge_port网桥要绑定的以太网端口的结构体\nnet_bridge_fdb_entry：单播转发数据库条目\nnet_bridge_mdb_entry：组播转发数据库条目\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int br_add_bridge(struct net *net, const char *name) { struct net_device *dev; int res; dev = alloc_netdev(sizeof(struct net_bridge), name, NET_NAME_UNKNOWN, br_dev_setup); ////// if (!dev) return -ENOMEM; dev_net_set(dev, net); dev-\u0026gt;rtnl_link_ops = \u0026amp;br_link_ops; res = register_netdevice(dev); if (res) free_netdev(dev); return res; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 void br_dev_setup(struct net_device *dev) { struct net_bridge *br = netdev_priv(dev); ///////// eth_hw_addr_random(dev); ether_setup(dev); dev-\u0026gt;netdev_ops = \u0026amp;br_netdev_ops; dev-\u0026gt;needs_free_netdev = true; dev-\u0026gt;ethtool_ops = \u0026amp;br_ethtool_ops; SET_NETDEV_DEVTYPE(dev, \u0026amp;br_type); dev-\u0026gt;priv_flags = IFF_EBRIDGE | IFF_NO_QUEUE; dev-\u0026gt;features = COMMON_FEATURES | NETIF_F_LLTX | NETIF_F_NETNS_LOCAL | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX; dev-\u0026gt;hw_features = COMMON_FEATURES | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX; dev-\u0026gt;vlan_features = COMMON_FEATURES; br-\u0026gt;dev = dev; ////////////////// spin_lock_init(\u0026amp;br-\u0026gt;lock); INIT_LIST_HEAD(\u0026amp;br-\u0026gt;port_list); //////////// INIT_HLIST_HEAD(\u0026amp;br-\u0026gt;fdb_list); //////////// INIT_HLIST_HEAD(\u0026amp;br-\u0026gt;frame_type_list); #if IS_ENABLED(CONFIG_BRIDGE_MRP) INIT_HLIST_HEAD(\u0026amp;br-\u0026gt;mrp_list); #endif #if IS_ENABLED(CONFIG_BRIDGE_CFM) INIT_HLIST_HEAD(\u0026amp;br-\u0026gt;mep_list); #endif spin_lock_init(\u0026amp;br-\u0026gt;hash_lock); br-\u0026gt;bridge_id.prio[0] = 0x80; br-\u0026gt;bridge_id.prio[1] = 0x00; ether_addr_copy(br-\u0026gt;group_addr, eth_stp_addr); br-\u0026gt;stp_enabled = BR_NO_STP; br-\u0026gt;group_fwd_mask = BR_GROUPFWD_DEFAULT; br-\u0026gt;group_fwd_mask_required = BR_GROUPFWD_DEFAULT; br-\u0026gt;designated_root = br-\u0026gt;bridge_id; br-\u0026gt;bridge_max_age = br-\u0026gt;max_age = 20 * HZ; br-\u0026gt;bridge_hello_time = br-\u0026gt;hello_time = 2 * HZ; br-\u0026gt;bridge_forward_delay = br-\u0026gt;forward_delay = 15 * HZ; br-\u0026gt;bridge_ageing_time = br-\u0026gt;ageing_time = BR_DEFAULT_AGEING_TIME; dev-\u0026gt;max_mtu = ETH_MAX_MTU; br_netfilter_rtable_init(br); br_stp_timer_init(br); br_multicast_init(br); INIT_DELAYED_WORK(\u0026amp;br-\u0026gt;gc_work, br_fdb_cleanup); } 1 2 3 4 static inline void *netdev_priv(const struct net_device *dev) { return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN); } 简单来说，struct net_device 是桥的“外壳”或“接口”，而 struct net_bridge 是桥的“大脑”或“核心逻辑”。它们通过一个关键的指针相互关联。下面我们进行详细分解。\n核心数据结构\nstruct net_device： 这个结构体代表一个网络接口，无论它是物理的（如 eth0）还是虚拟的（如 br-lan, veth0）。它包含了所有网络设备共有的信息，例如：\n接口名称（name，如 \u0026ldquo;br-lan\u0026rdquo;）\nMAC 地址（dev_addr）\n接口状态（UP/DOWN）\n发送和接收数据包的函数指针（netdev_ops）\n统计信息（包计数、错误计数等）\n一个非常重要的成员：priv_flags 和 rx_handler（后面会讲到）\n指向net_device私有数据的一个通用指针：priv（对于桥设备，它指向 struct net_bridge,通过 netdev_priv(dev) 宏来获取 priv 指针的地址\nstruct net_bridge： 这个结构体是专门为桥接功能定义的，它包含了管理一个桥所需的所有信息，例如：\n桥的 STP（生成树协议）状态和配置。\n桥的 MAC 地址（通常取自第一个被加入的端口）。\n端口列表（port_list）： 一个链表，包含了所有加入这个桥的端口（如 eth0, eth1）。每个端口由一个 struct net_bridge_port 表示。\n转发数据库（FDB）：一个哈希表，用于学习和管理 MAC 地址到端口的映射。\n指向其对应的 net_device 的指针。\n关键关联机制 关联是在创建桥设备（例如使用 brctl addbr br-lan 或 ip link add br-lan type bridge）时建立的。整个过程的核心是 netdev_priv() 机制。\n分配 net_device 并嵌入 net_bridge 当内核需要创建一个新的桥设备时，它并不是先分配一个 net_device，再分配一个 net_bridge，然后用指针连接它们。相反，它使用了一种更高效、更常见的“嵌入式”方法：\n一次性分配内存： 内核调用 alloc_netdev(sizeof(struct net_bridge), \u0026ldquo;br-lan\u0026rdquo;, NET_NAME_UNKNOWN, br_dev_setup)。 alloc_netdev 会分配一块足够大的内存，这块内存的大小 = sizeof(struct net_device) + sizeof(struct net_bridge) + 一些对齐填充。这样，net_bridge 结构体就直接内嵌在 net_device 结构体之后的内存中。\n关联指针： 在 br_dev_setup 初始化函数中，会进行关键设置:设置 net_device-\u0026gt;priv_flags 为 IFF_EBRIDGE，标记这是一个桥设备。设置 net_device-\u0026gt;netdev_ops 为 br_netdev_ops，这意味着所有针对 br-lan 这个网络设备的操作（如打开、关闭、发送数据包）都由桥接模块提供的函数处理。最关键的一步：通过 netdev_priv(dev) 宏来获取 priv 指针的地址。由于内存布局是内嵌的，也就是紧跟着 net_device 结构体的那块内存的起始地址——而这正好就是我们为 net_bridge 分配的地方。 br-\u0026gt;dev = dev; // net_bridge 也保存一个指回 net_device 的指针\n数据包流向（接收路径） 关联建立后，数据包如何被桥处理呢？这涉及到另一个关键机制：RX 处理程序（RX handler）。当一个网络接口（例如 eth0）被加入到桥 br-lan 时（使用 brctl addif br-lan eth0），内核会调用 br_add_if()。 在这个函数中，会为 eth0 这个 net_device 安装一个RX处理程序：dev-\u0026gt;rx_handler = br_handle_frame。从此以后，所有从 eth0 接收到的数据包，在进入网络协议栈的更上层（如IP层）之前，会先被 br_handle_frame 这个函数截获。br_handle_frame 函数可以判断这个数据包所属的桥（通过 eth0 对应的 struct net_bridge_port 找到其所属的 struct net_bridge），然后进行桥接逻辑（学习MAC地址、转发、过滤等）。\n网桥私有数据：net_bridge 网桥相关信息保存在这里：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 struct net_bridge { spinlock_t lock; spinlock_t hash_lock; struct hlist_head frame_type_list; struct net_device *dev; /////// unsigned long options; /* These fields are accessed on each packet */ #ifdef CONFIG_BRIDGE_VLAN_FILTERING __be16 vlan_proto; u16 default_pvid; struct net_bridge_vlan_group __rcu *vlgrp; #endif struct rhashtable fdb_hash_tbl; ///// struct list_head port_list; ///// #if IS_ENABLED(CONFIG_BRIDGE_NETFILTER) union { struct rtable fake_rtable; struct rt6_info fake_rt6_info; }; #endif u16 group_fwd_mask; u16 group_fwd_mask_required; /* STP */ bridge_id designated_root; bridge_id bridge_id; unsigned char topology_change; unsigned char topology_change_detected; u16 root_port; unsigned long max_age; unsigned long hello_time; unsigned long forward_delay; unsigned long ageing_time; unsigned long bridge_max_age; unsigned long bridge_hello_time; unsigned long bridge_forward_delay; unsigned long bridge_ageing_time; u32 root_path_cost; u8 group_addr[ETH_ALEN]; enum { BR_NO_STP, /* no spanning tree */ BR_KERNEL_STP, /* old STP in kernel */ BR_USER_STP, /* new RSTP in userspace */ } stp_enabled; struct net_bridge_mcast multicast_ctx; #ifdef CONFIG_BRIDGE_IGMP_SNOOPING struct bridge_mcast_stats __percpu *mcast_stats; u32 hash_max; spinlock_t multicast_lock; struct rhashtable mdb_hash_tbl; //////// struct rhashtable sg_port_tbl; struct hlist_head mcast_gc_list; struct hlist_head mdb_list; ////// struct work_struct mcast_gc_work; #endif struct timer_list hello_timer; struct timer_list tcn_timer; struct timer_list topology_change_timer; struct delayed_work gc_work; struct kobject *ifobj; u32 auto_cnt; #ifdef CONFIG_NET_SWITCHDEV /* Counter used to make sure that hardware domains get unique * identifiers in case a bridge spans multiple switchdev instances. */ int last_hwdom; /* Bit mask of hardware domain numbers in use */ unsigned long busy_hwdoms; #endif struct hlist_head fdb_list; ///// #if IS_ENABLED(CONFIG_BRIDGE_MRP) struct hlist_head mrp_list; #endif #if IS_ENABLED(CONFIG_BRIDGE_CFM) struct hlist_head mep_list; #endif }; 网桥端口：net_bridge_port 一个网桥可以和若干以太网端口绑定，每个端口信息保存在net_bridge_port结构中，并被net_bridge{} –\u0026gt;port_list链接。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 struct net_bridge_port { struct net_bridge *br; //// struct net_device *dev; //// netdevice_tracker dev_tracker; struct list_head list; unsigned long flags; #ifdef CONFIG_BRIDGE_VLAN_FILTERING struct net_bridge_vlan_group __rcu *vlgrp; #endif struct net_bridge_port __rcu *backup_port; /* STP */ u8 priority; u8 state; u16 port_no; unsigned char topology_change_ack; unsigned char config_pending; port_id port_id; port_id designated_port; bridge_id designated_root; bridge_id designated_bridge; u32 path_cost; u32 designated_cost; unsigned long designated_age; struct timer_list forward_delay_timer; struct timer_list hold_timer; struct timer_list message_age_timer; struct kobject kobj; struct rcu_head rcu; struct net_bridge_mcast_port multicast_ctx; #ifdef CONFIG_BRIDGE_IGMP_SNOOPING struct bridge_mcast_stats __percpu *mcast_stats; u32 multicast_eht_hosts_limit; u32 multicast_eht_hosts_cnt; struct hlist_head mglist; #endif #ifdef CONFIG_SYSFS char sysfs_name[IFNAMSIZ]; #endif #ifdef CONFIG_NET_POLL_CONTROLLER struct netpoll *np; #endif #ifdef CONFIG_NET_SWITCHDEV /* Identifier used to group ports that share the same switchdev * hardware domain. */ int hwdom; int offload_count; struct netdev_phys_item_id ppid; #endif u16 group_fwd_mask; u16 backup_redirected_cnt; struct bridge_stp_xstats stp_xstats; }; 单播转发数据库条目：net_bridge_fdb_entry net_bridge中有两个字段与该结构体相关，fdb_hash_tbl与fdb_list，函数fdb_create（）可以看到net_bridge_fdb_entry与这连个字段的关系。每一条新建的fdb表项会加入br-\u0026gt;fdb_hash_tbl这个哈希表中，准确的说是br-\u0026gt;fdb_hash_tbl-\u0026gt;tbl，对应的键值是fdb表项的key字段（addr与vlan）。插入成功，则将该fdb表项的fdb_node插入br-\u0026gt;fdb_hash_tbl中。 也就说fdb表项分别以哈希表与链表的形式存储在br中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct net_bridge_fdb_entry { struct rhash_head rhnode; struct net_bridge_port *dst; struct net_bridge_fdb_key key; struct hlist_node fdb_node; unsigned long flags; /* write-heavy members should not affect lookups */ unsigned long updated ____cacheline_aligned_in_smp; unsigned long used; struct rcu_head rcu; }; 组播转发数据库条目：net_bridge_mdb_entry 该条目描述一个多播组转发项，将多播形式的mac地址与一组端口对应，timer链表为这些端口的失效定时器链表，某一个端口定时器超时则将其移除\n1 2 3 4 5 6 7 8 9 10 11 12 13 struct net_bridge_mdb_entry { struct rhash_head rhnode; struct net_bridge *br; struct net_bridge_port_group __rcu *ports; struct br_ip addr; bool host_joined; struct timer_list timer; struct hlist_node mdb_node; struct net_bridge_mcast_gc mcast_gc; struct rcu_head rcu; }; 网桥设备对数据包处理 以收包为例。\n网络中有数据包到来，当网卡接受到数据时，网卡向cpu发中断，cpu调用驱动程序，将数据包放到对应的cpu输入队列。接着唤醒软中断，调用netif_receive_skb函数，在这里判断数据包需要转到上层协议栈，还是向网桥接口处理。数据流向如下，这里默认网桥设备为非disable状态\n具体流程：\n1 2 3 4 5 6 7 8 9 10 1. netif_receive_skb 2. --\u0026gt;netif_receive_skb_internal /* 记录收包时间, rps机制，将报文在多个cpu之间做负载均衡以及提高报文处理的缓存命中率 */ 3. --\u0026gt;__netif_receive_skb 4. --\u0026gt;__netif_receive_skb_core 5. --\u0026gt; skb_reset_network_header //重置network_header字段 6. --\u0026gt; skb_vlan_untag //802.1Q、802.1AD,剥除vxlan头 7. --\u0026gt; paket_type.func() //处理 ptype_all 上所有的 packet_type-\u0026gt;func() 8. --\u0026gt; vlan_do_receive 9. --\u0026gt; dev-\u0026gt;rx_handler () //实际执行函数br_handle_frame(),数据包逻辑分发，网桥处理转发就在这里进行 10. --\u0026gt; deliver_ptype_list_skb //向L3分发 __netif_receive_skb_core 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, struct packet_type **ppt_prev) { struct packet_type *ptype, *pt_prev; rx_handler_func_t *rx_handler; struct sk_buff *skb = *pskb; struct net_device *orig_dev; bool deliver_exact = false; int ret = NET_RX_DROP; __be16 type; net_timestamp_check(!READ_ONCE(netdev_tstamp_prequeue), skb); trace_netif_receive_skb(skb); orig_dev = skb-\u0026gt;dev; skb_reset_network_header(skb); if (!skb_transport_header_was_set(skb)) skb_reset_transport_header(skb); skb_reset_mac_len(skb); pt_prev = NULL; another_round: skb-\u0026gt;skb_iif = skb-\u0026gt;dev-\u0026gt;ifindex; __this_cpu_inc(softnet_data.processed); if (static_branch_unlikely(\u0026amp;generic_xdp_needed_key)) { int ret2; migrate_disable(); ret2 = do_xdp_generic(rcu_dereference(skb-\u0026gt;dev-\u0026gt;xdp_prog), skb); migrate_enable(); if (ret2 != XDP_PASS) { ret = NET_RX_DROP; goto out; } } if (eth_type_vlan(skb-\u0026gt;protocol)) { skb = skb_vlan_untag(skb); if (unlikely(!skb)) goto out; } if (skb_skip_tc_classify(skb)) goto skip_classify; if (pfmemalloc) goto skip_taps; list_for_each_entry_rcu(ptype, \u0026amp;ptype_all, list) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } list_for_each_entry_rcu(ptype, \u0026amp;skb-\u0026gt;dev-\u0026gt;ptype_all, list) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } skip_taps: #ifdef CONFIG_NET_INGRESS if (static_branch_unlikely(\u0026amp;ingress_needed_key)) { bool another = false; skb = sch_handle_ingress(skb, \u0026amp;pt_prev, \u0026amp;ret, orig_dev, \u0026amp;another); if (another) goto another_round; if (!skb) goto out; if (nf_ingress(skb, \u0026amp;pt_prev, \u0026amp;ret, orig_dev) \u0026lt; 0) goto out; } #endif skb_reset_redirect(skb); skip_classify: if (pfmemalloc \u0026amp;\u0026amp; !skb_pfmemalloc_protocol(skb)) goto drop; if (skb_vlan_tag_present(skb)) { if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; } if (vlan_do_receive(\u0026amp;skb)) goto another_round; else if (unlikely(!skb)) goto out; } rx_handler = rcu_dereference(skb-\u0026gt;dev-\u0026gt;rx_handler); if (rx_handler) { if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; } switch (rx_handler(\u0026amp;skb)) { case RX_HANDLER_CONSUMED: ret = NET_RX_SUCCESS; goto out; case RX_HANDLER_ANOTHER: goto another_round; case RX_HANDLER_EXACT: deliver_exact = true; break; case RX_HANDLER_PASS: break; default: BUG(); } } if (unlikely(skb_vlan_tag_present(skb)) \u0026amp;\u0026amp; !netdev_uses_dsa(skb-\u0026gt;dev)) { check_vlan_id: if (skb_vlan_tag_get_id(skb)) { /* Vlan id is non 0 and vlan_do_receive() above couldn\u0026#39;t * find vlan device. */ skb-\u0026gt;pkt_type = PACKET_OTHERHOST; } else if (eth_type_vlan(skb-\u0026gt;protocol)) { /* Outer header is 802.1P with vlan 0, inner header is * 802.1Q or 802.1AD and vlan_do_receive() above could * not find vlan dev for vlan id 0. */ __vlan_hwaccel_clear_tag(skb); skb = skb_vlan_untag(skb); if (unlikely(!skb)) goto out; if (vlan_do_receive(\u0026amp;skb)) /* After stripping off 802.1P header with vlan 0 * vlan dev is found for inner header. */ goto another_round; else if (unlikely(!skb)) goto out; else /* We have stripped outer 802.1P vlan 0 header. * But could not find vlan dev. * check again for vlan id to set OTHERHOST. */ goto check_vlan_id; } /* Note: we might in the future use prio bits * and set skb-\u0026gt;priority like in vlan_do_receive() * For the time being, just ignore Priority Code Point */ __vlan_hwaccel_clear_tag(skb); } type = skb-\u0026gt;protocol; /* deliver only exact match when indicated */ if (likely(!deliver_exact)) { deliver_ptype_list_skb(skb, \u0026amp;pt_prev, orig_dev, type, \u0026amp;ptype_base[ntohs(type) \u0026amp; PTYPE_HASH_MASK]); } deliver_ptype_list_skb(skb, \u0026amp;pt_prev, orig_dev, type, \u0026amp;orig_dev-\u0026gt;ptype_specific); if (unlikely(skb-\u0026gt;dev != orig_dev)) { deliver_ptype_list_skb(skb, \u0026amp;pt_prev, orig_dev, type, \u0026amp;skb-\u0026gt;dev-\u0026gt;ptype_specific); } if (pt_prev) { if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC))) goto drop; *ppt_prev = pt_prev; } else { drop: if (!deliver_exact) atomic_long_inc(\u0026amp;skb-\u0026gt;dev-\u0026gt;rx_dropped); else atomic_long_inc(\u0026amp;skb-\u0026gt;dev-\u0026gt;rx_nohandler); kfree_skb(skb); /* Jamal, now you will not able to escape explaining * me how you were going to use this. :-) */ ret = NET_RX_DROP; } out: /* The invariant here is that if *ppt_prev is not NULL * then skb should also be non-NULL. * * Apparently *ppt_prev assignment above holds this invariant due to * skb dereferencing near it. */ *pskb = skb; return ret; } CONFIG_NET_INGRESS 是 Linux 内核的一个配置选项，用于启用或禁用网络设备的 Ingress 功能。启用 CONFIG_NET_INGRESS 配置选项后，Linux 内核将支持对网络设备的入口流量进行控制和处理。网络设备的 Ingress 功能可以用于实现以下功能：\nØ 数据包过滤：根据预先设置的过滤规则，判断数据包是否符合要求。如果数据包被过滤，可以根据策略进行丢弃或处理。\nØ 数据包处理：根据网络设备的配置，对数据包进行处理。例如，进行 QoS 处理、修改数据包头部、更改目标端口等操作。\nØ 数据包转发：根据策略和路由表，决定数据包的转发目的地，并将数据包发送到相应的网络接口。\n上述功能通过函数sch_handle_ingress实现\n对vlan的处理 判断skb协议是否为8021Q 8021AD，若是vlan包，则调用skb_vlan_untag()函数，该函数读出数据流中的vlan_id，并填写入skb-\u0026gt;vlan_tci中，然后删除vlan_head，从而实现对上层的透明。注意这里的skb-\u0026gt;vlan_tci标志仅是为了保存skb数据的vlan头信息，而skb中的数据是透明的以太网包.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static inline bool eth_type_vlan(__be16 ethertype) { switch (ethertype) { case htons(ETH_P_8021Q): case htons(ETH_P_8021AD): return true; default: return false; } } if (eth_type_vlan(skb-\u0026gt;protocol)) { skb = skb_vlan_untag(skb); if (unlikely(!skb)) goto out; } vlan信息转移到skb结构中后，会获取数据包真实协议类型（三层协议类型），更新到skb的protocol字段，替换了之前的ETH_P_8021Q或者ETH_P_8021AD。将vlan信息存在skb字段后，调用vlan_do_receive，该函数由skb-\u0026gt;vlan_tci得到该skb包所要发往的vlan_dev，并且重定向skb-\u0026gt;dev为该vlan_dev，最后消除skb中的vlan_tci标志。\n1 2 3 4 5 6 7 8 9 10 11 12 13 if (skb_vlan_tag_present(skb)) { if (pt_prev) { ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = NULL; } if (vlan_do_receive(\u0026amp;skb)) goto another_round; else if (unlikely(!skb)) goto out; } rx_handler = rcu_dereference(skb-\u0026gt;dev-\u0026gt;rx_handler); if (rx_handler) { 样之后vlan_do_receive()返回，不过此时的skb已经是一个普通的数据包了（实现了对上层的透明），且它看起来就像是由vlan_dev接收的数据包。\n接下来是关键处理dev-\u0026gt;rx_handler，这个函数在之前将网口添加到网桥设备的过程，调用br_add_if函数中定义\n1 2 3 4 5 br_add_if () --\u0026gt;netdev_rx_handler_register --\u0026gt;netdev_rx_handler_register(dev, br_get_rx_handler(dev), p); // {rcu_assign_pointer(dev-\u0026gt;rx_handler_data, p) rcu_assign_pointer(dev-\u0026gt;rx_handler, br_get_rx_handler)} 看到在netdev_rx_handler_register函数：\n将dev-\u0026gt;rx_handle与br_get_rx_handler绑定\ndev-\u0026gt;rx_handler_data与以太网端口p绑定\n所以这里网桥操作skb包，主要在br_get_rx_handler函数中进行。\n经过dev-\u0026gt;rx_handler函数处理，返回值有四种类型：\nØ RX_HANDLER_CONSUMED：数据包已处理消化，无需进一步处理。（什么场景） 直接跳转到out标签退出。\nØ RX_HANDLER_ANOTHER：修改了skb-\u0026gt;dev，再处理一次，比如数据包设备是网桥设备，但经过判断，需要经过网桥设备转发到另一个网桥关联的端口，这时就从bridge_dev转换到了port_dev\nØ RX_HANDLER_EXACT：精确指定要传递到ptype-\u0026gt;dev == skb-\u0026gt;dev（什么场景）\nØ RX_HANDLER_PASS：1.数据包类型是回环类型PACKET_LOOPBACK\n后两种情况会继续往下执行，最终将数据包提交到L3层协议栈\n网桥设备处理skb: br_handle_frame 从这里开始才是对数据包的处理分流转发，之前可以看作netif_receive_skb函数对数据包的预处理。br_get_rx_handler被设置为br_handle_frame，具体路径：net/bridge/br_input.c。\nbr_handle_frame函数根据数据包的类型，对数据的处理进行分流.\nØ 网桥处于disable状态：\n1.数据包目的地址是网桥端口地址：将pkt_type设为PACKET_HOST\n1 2 if (ether_addr_equal(p-\u0026gt;br-\u0026gt;dev-\u0026gt;dev_addr, dest)) skb-\u0026gt;pkt_type = PACKET_HOST; 2.否则，br_handle_local_finish函数设置到网桥预处理节点，该函数在网桥状态部位disable时，会学习mac与端口信息并记录fdb表中\n1 2 3 4 5 if (NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, dev_net(skb-\u0026gt;dev), NULL, skb, skb-\u0026gt;dev, NULL, br_handle_local_finish) == 1) { return RX_HANDLER_PASS; } Ø 网桥处于learning或forwarding状态：\n调用函数nf_hook_bridge_pre-\u0026gt; br_handle_frame_finish\nbr_handle_frame_finish br_handle_frame_finish：决策将不同类别的数据包做不同的分发路径。具体处理逻辑如下图.\n首先这里有一个比较重要的特性，arp proxy，接口如果是能了此功能，对于收到的arp request，通过查询本地的arp表构造arp reply报文回应。\n根据目标地址skb-\u0026gt;hdr-\u0026gt;h_dest判断是单播、广播、多播的哪一种：\nØ 单播：调用br_fdb_find_rcu查找fdb表项， （1）若命中，调用br_forward()将数据包转发， （2）若表项为空，调用函数br_flood() br_flood(br, skb, pkt_type, local_rcv, false); 注意，如果发现目标地址是本地地址（判断方法目标表项dst-\u0026gt;flags为BR_FDB_LOCAL），则直接调用br_pass_frame_up()发往本地，该函数会再次进入netif_receive_skb函数。\nØ 广播：由于广播目标地址包含本机，所以会再次调用br_pass_frame_up()，并调用br_flood()\nØ 多播：因为当目的mac地址是0x01开头时，既可以是igmp类型的多播协议控制报文，也可以是多播数据流报文。先调用br_multicast_rcv处理igmp类型数据包，多播数据表也通过该类型数据表维护。接着查看多播数据表项， （1）如果命中，调用br_multicast_flood()， （2）如果未命中也会调用br_flood()。 最后多播也会判断本机是否加入多播组，加入的话也会调用函数br_pass_frame_up()将数据发往本地。\n这里看到单播，多播，广播都会调用br_flood()函数，分别在以下情况：\n单播，fdb表项未命中\n广播下都会调用\n多播，mdb表未命中\n不同情况，通过函数参数pkt_type进行的区分。\n单播：pkt_type = BR_PKT_UNICAST\n广播：pkt_type = BR_PKT_BROADCAST\n多播：pkt_type = BR_PKT_MULTICAST\n数据包发往本地：br_pass_frame_up 数据进入br_pass_frame_up，是打算经由Bridge设备，输入到本地Host的。数据包从网桥端口设备进入，经过网桥设备，然后再进入协议栈，其实是“两次经过net_device”，一次是端口设备，另一次是网桥设备。现在数据包离开网桥端口进入网桥设备，需要修改skb-\u0026gt;dev字段。\n1 2 indev = skb-\u0026gt;dev; skb-\u0026gt;dev = brdev 递交的最后一步是经过NF_BR_LOCAL_IN钩子点，然后是我们熟悉的netif_receive_skb，只不过这次进入该函数的时候skb-\u0026gt;dev已经被换成了Bridge设备。这可以理解为进入了Bridge设备的处理。它的skb-\u0026gt;dev-\u0026gt;rx_handler 为空，所以不会再次进入br_handler_frame，而是会进上层协议栈。\n1 2 3 return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, dev_net(indev), NULL, skb, indev, NULL, br_netif_receive_skb); 数据被修改skb-\u0026gt;dev后再次进入netif_receive_skb，上次执行的netif_receive_skb因为rx_handler返回CONSUMED而结束。\n数据包转发 单端口转发：br_forward\nbr_forward经过一系列的检查与前期准备，最终调用dev_queue_xmit(skb)将数据包放到网卡输出队列中发送出去。之后对应具体的网卡驱动函数。\n多播：br_multicast_flood\n桥的多播功能需要使能igmp snooping功能\n转发端口获取：\n从mdb中获取多播组的端口p1， 从br-\u0026gt;router_list获取桥接端口p2 p = p1\u0026gt;p2? p1: p2 （为什么这样比较） 若p=p1时，如果该端口支持多播对单播转发， 则以单播形式转发（也是调用br_forward，与普通多播貌似没有不同） 复制skb数据，调用br_forward从p端口转发数据 下面br_flood一样每次转发的端口都是上次遍历得到的端口，最后一次端口使用原始skb数据，这样减少一次clone操作\nflood到各端口br_flood\n如果没有查找到对应的表项（单播、组播），或者要进行广播模式，系统调用br_flood，向网桥的每个端口转发。br_flood函数遍历网桥下的每个端口，根据允许的flags条件，调用deliver_clone或__br_forward将sbk从该端口转发出去\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void br_flood(struct net_bridge *br, struct sk_buff *skb, enum br_pkt_type pkt_type, bool local_rcv, bool local_orig) { struct net_bridge_port *prev = NULL; struct net_bridge_port *p; list_for_each_entry_rcu(p, \u0026amp;br-\u0026gt;port_list, list) { /* Do not flood unicast traffic to ports that turn it off, nor * other traffic if flood off, except for traffic we originate */ switch (pkt_type) { case BR_PKT_UNICAST: if (!(p-\u0026gt;flags \u0026amp; BR_FLOOD)) continue; break; case BR_PKT_MULTICAST: if (!(p-\u0026gt;flags \u0026amp; BR_MCAST_FLOOD) \u0026amp;\u0026amp; skb-\u0026gt;dev != br-\u0026gt;dev) continue; break; case BR_PKT_BROADCAST: if (!(p-\u0026gt;flags \u0026amp; BR_BCAST_FLOOD) \u0026amp;\u0026amp; skb-\u0026gt;dev != br-\u0026gt;dev) continue; break; } /* Do not flood to ports that enable proxy ARP */ if (p-\u0026gt;flags \u0026amp; BR_PROXYARP) continue; if ((p-\u0026gt;flags \u0026amp; (BR_PROXYARP_WIFI | BR_NEIGH_SUPPRESS)) \u0026amp;\u0026amp; BR_INPUT_SKB_CB(skb)-\u0026gt;proxyarp_replied) continue; prev = maybe_deliver(prev, p, skb, local_orig); if (IS_ERR(prev)) goto out; } if (!prev) goto out; if (local_rcv) deliver_clone(prev, skb, local_orig); else __br_forward(prev, skb, local_orig); return; out: if (!local_rcv) kfree_skb(skb); } 在向每个端口转发时，都会复制一份新的skb数据。 maybe_deliver是deliver_clone的包装，每次遍历端口后，会在下一个遍历周期，将数 据从本次遍历端口发送，最后一个端口的数据则是直接使用原始的skb，这样可以少一次数据复制开销。\n参考链接 linux网桥驱动与二层对数据包分发 - 知乎\n","date":"2025-09-26T16:36:08+08:00","permalink":"https://charles-7777.github.io/p/linux-%E7%BD%91%E6%A1%A5/","title":"Linux 网桥"},{"content":"\n光滑的地面上放着大小两个滑块，左边是墙。大滑块的质量是小滑块的N倍。给大滑块一个向左的初速度，两个滑块之间会发生多次碰撞。假设碰撞没有能量损失，问一共会发生多少次碰撞？\n能量守恒 $$ \\frac{1}{2}mv^2 + \\frac{1}{2}MV^2 = \\frac{1}{2}MV_0^2 $$$$ \\frac{v^2}{M/m} + V^2 = V_0^2 $$由M=Nm\n$$ \\frac{v^2}{N}+V^2=V_0^2 $$$$ \\frac{v^2}{NV_0^2}+\\frac{V^2}{V_0^2}=1 $$ 碰撞过程满足动量守恒\n设向右为正\n第一次碰撞\n$$ mv_1+MV_1=MV_0 $$$$ V_1=-\\frac{1}{N}v_1+V_0 $$初始$V_0$向左为负，此时$V_1$ 为负，$v_1$为负\n第二次碰撞\n小滑块m 撞墙以后，被墙反弹，速度反向变为 $-v_1$\n$$ mv_2+MV_2=m(-v_1)+MV_1 $$ $$ V_2=-\\frac{1}{N}(v_2+v_1)+V_1 $$ 相当于$V_2=-\\frac{1}{N}v_2+V_1$向右平移了$|v_1|$\n上述过程可以重复下去，直到 $V\\geq v \\gt 0$时为止\n$\\frac{v^2}{NV_0^2}+\\frac{V^2}{V_0^2}=1$ 变量代换$x=\\frac{v}{\\sqrt N |V_0|}$ $y=\\frac {V}{|V_0|}$ ===\u0026gt;$x^2+y^2=1$； $ V_1=-\\frac{1}{N}v_1+V_0$ ==\u0026gt;$y=-\\frac {1}{\\sqrt N}x-1$\n$\\tan \\theta = -\\frac {1}{\\sqrt N} $\n滑块的每一次碰撞，都可以看成是从单位圆上切下两段长度为 $l=\\theta R$\n的圆弧，即每次切下$s=2 l= 2 \\times \\theta \\times R = 2\\arctan \\frac {1}{\\sqrt N} $\n整个单位圆的周长为$2 \\pi$\n直到剩下的圆弧不足$s= 2\\arctan \\frac {1}{\\sqrt N}$时不会再碰撞\n故总的碰撞次数为\n$$ \\lceil \\frac {2 \\pi}{2\\arctan \\frac {1}{\\sqrt N}} \\rceil -1 $$由上式可以算出，当两个滑块质量相等时，$s= \\arctan \\frac {1}{\\sqrt N}$ 为$\\frac {\\pi}{4}$，碰撞总次数为 3。而当两个滑块质量悬殊时， $\\frac {1}{\\sqrt N}$就很小，由$\\arctan (x) = x - \\frac {x^3} {3}+\\frac {x^5} {5}-\\frac {x^7} {7}-\u0026hellip;$此时就可以直接用 $\\frac {1}{\\sqrt N}$来近似$\\arctan \\frac {1}{\\sqrt N}$，于是碰撞总次数就约为$\\lfloor \\sqrt N \\pi \\rfloor$ 。当两个滑块的质量之比 N是 100 的幂时，$\\sqrt N$就是 10 的幂，这就难怪碰撞总次数会恰好是$\\pi$的前若干位了\n","date":"2025-09-19T14:10:10+08:00","image":"https://cdn.jsdelivr.net/gh/charles-7777/ImageBed@main/blog/2025-09-19-13-54-10-image.png","permalink":"https://charles-7777.github.io/p/%E5%BC%B9%E6%80%A7%E7%A2%B0%E6%92%9E%E4%B8%8Epi/","title":"弹性碰撞与pi"},{"content":"什么是 UPnP？ UPnP（Universal Plug and Play，通用即插即用）是一种网络协议标准，旨在让网络设备（如计算机、打印机、摄像头、智能电视、路由器、IoT设备等）在接入网络时能够自动发现彼此并建立功能性的网络服务，无需用户手动配置。其设计目标是实现“零配置”网络体验。 UPnP 基于开放的互联网标准和技术，如 TCP/IP、HTTP、XML、SOAP 和 SSDP，使其能在多种操作系统和硬件平台上运行。\nUPnP 基本原理 UPnP 网络通常包含三种角色：\n设备 (Device)：提供一项或多项服务的网络实体（如智能灯泡、网络摄像头）。\n服务 (Service)：设备提供的最小功能控制单元（如开关服务、亮度调节服务）。\n控制点 (Control Point)：发现并控制设备的控制器（如手机App、电脑上的软件）。\nUPnP 的工作流程可分为以下几个核心阶段：\n寻址（Addressing） 设备接入网络后，首先通过 DHCP 获取 IP 地址；若 DHCP 不可用，则使用 Auto-IP（如 169.254.x.x）自行分配一个局域网内可用的 IP。\n发现（Discovery） 设备使用 SSDP（Simple Service Discovery Protocol，简单服务发现协议）向局域网广播自己的存在：\n广播消息：设备发送 M-SEARCH 请求或 NOTIFY 通知。\n响应机制：控制点（如手机App或PC）监听这些广播，并向感兴趣的设备发送请求获取详细信息。\n使用组播地址 239.255.255.250:1900 进行通信。\n描述（Description） 控制点通过 HTTP GET 请求从设备提供的 URL 获取 XML 描述文件，该文件包含：\n设备类型、厂商信息、型号、序列号\n支持的服务列表（如端口映射、音量控制等）\n每个服务的控制URL、事件URL、描述URL\n控制（Control） 控制点通过 SOAP（Simple Object Access Protocol）协议向设备发送控制指令，调用其服务方法。例如：\n调用“AddPortMapping”在路由器上添加端口转发规则\n调用“SetVolume”调节音响音量 请求和响应均使用 XML 格式封装，通过 HTTP POST 发送。\n事件通知（Eventing） 设备状态变化时（如播放状态改变、端口映射成功），可通过 GENA（General Event Notification Architecture）协议向订阅的控制点推送事件通知（也是 XML 格式），实现状态同步。\n展示（Presentation，可选） 设备可提供一个内置的 Web 界面（URL 包含在描述文件中），用户可通过浏览器访问进行图形化控制或查看状态。\nUPnP 主要功能 自动设备发现与互操作 局域网内设备自动广播身份，无需手动输入 IP 或配置。\n不同厂商设备只要遵循 UPnP 标准即可互通（如智能灯泡 + 手机App）。\n自动端口映射（NAT 穿透） 最重要功能之一：允许内网设备（如游戏主机、P2P软件、监控摄像头）自动在路由器上创建端口转发规则，实现从外网访问。\n应用场景：在线游戏联机、远程访问家庭摄像头、BT下载加速等。\n无需用户登录路由器后台手动设置端口转发。\n设备控制与状态同步 可远程控制设备功能（播放/暂停、开关机、调节参数等）。\n实时接收设备状态变更通知（如“播放结束”、“电量低”）。\n零配置网络体验 用户无需了解网络知识，插上网线或连上WiFi即可使用设备功能。\n特别适合家庭用户和 IoT 设备。\n支持多种设备类型 路由器、打印机、NAS、媒体服务器（DLNA）、智能家电、安防设备等均可支持 UPnP。 UPnP 的优缺点 ✅ 优点： 简单易用：真正实现“即插即用”。\n跨平台兼容：基于标准互联网协议，支持 Windows、Linux、macOS、Android、iOS 等。\n自动化程度高：减少人工配置错误。\n❌ 缺点与风险： 安全风险：恶意软件可能利用 UPnP 自动在路由器上开洞，暴露内网服务（如勒索软件、僵尸网络）。\n缺乏认证机制：默认无用户权限控制，局域网内任何设备都可操作。\n稳定性问题：部分设备实现不规范，可能导致冲突或失效。\n🛡️ 安全建议：\n家庭用户如无远程访问需求，建议在路由器中关闭 UPnP 功能。 企业网络应严格禁用 UPnP。 使用支持 UPnP-IGD v2 的设备，其安全性有所增强（支持 PIN 验证等）。 典型应用场景 场景 说明 在线游戏主机联网 Xbox/PS 自动请求端口映射，实现 NAT 开放或中等。 P2P 下载（如 BitTorrent） 客户端自动映射端口，提高连接数和下载速度。 远程访问家庭摄像头/NAS 自动在路由器上映射端口，便于外网访问。 DLNA 媒体共享 电视自动发现局域网内的媒体服务器并播放影片。 智能家居控制 手机 App 自动发现并控制智能插座、灯泡等。 相关协议与标准组织 标准组织：UPnP Forum（现由 Open Connectivity Foundation 接管） 核心协议： SSDP（发现） HTTP + XML（描述） SOAP（控制） GENA（事件） 扩展标准： UPnP-IGD（Internet Gateway Device）：专用于路由器端口映射。 DLNA（Digital Living Network Alliance）：基于 UPnP 的媒体共享标准。 总结 UPnP 是一项旨在简化家庭和小型办公网络设备互联的技术，通过标准化的发现、描述、控制和事件机制，实现了真正的“零配置”体验。尤其在自动端口映射方面极大地方便了普通用户。然而，其安全机制的缺失也带来潜在风险，用户应根据实际需求权衡是否启用。 随着 IoT 和智能家居的普及，UPnP 仍在广泛使用，但正逐步被更安全的替代方案（如 NAT-PMP、PCP、mDNS + DNS-SD）所补充或取代。\n📚 参考资料：UPnP Device Architecture 2.0, IETF RFC 6970, OCF Specifications\n","date":"2025-09-15T14:53:06+08:00","permalink":"https://charles-7777.github.io/p/upnp%E9%80%9A%E7%94%A8%E5%8D%B3%E6%8F%92%E5%8D%B3%E7%94%A8%E5%8D%8F%E8%AE%AE%E4%BB%8B%E7%BB%8D/","title":"upnp(通用即插即用)协议介绍"},{"content":"docker 搭建 hwdsl2/docker-ipsec-vpn-server: Docker image to run an IPsec VPN server, with IPsec/L2TP, Cisco IPsec and IKEv2\n1 2 3 4 5 6 7 8 9 10 docker run \\ --name ipsec-vpn-server \\ --restart=always \\ --env-file ./vpn.env \\ -v ikev2-vpn-data:/etc/ipsec.d \\ -v /lib/modules:/lib/modules:ro \\ -p 500:500/udp \\ -p 4500:4500/udp \\ -d --privileged \\ hwdsl2/ipsec-vpn-server vpn.env\n1 2 3 4 5 6 VPN_IPSEC_PSK=ipsec VPN_USER=vpnuser VPN_PASSWORD=123456 VPN_PUBLIC_IP=x.x.x.x VPN_ADDL_USERS=vpnuser1 vpnuser2 VPN_ADDL_PASSWORDS=123456 123456 查看docker启动log\ndocker logs ipsec-vpn-server\n一些client 连接可能遇到的问题\nsetup-ipsec-vpn/docs/clients.md at master · hwdsl2/setup-ipsec-vpn\n连接ipsec/l2tp 可能遇到的问题 Windows error 809\nError 809: The network connection between your computer and the VPN server could not be established because the remote server is not responding. This could be because one of the network devices (e.g, firewalls, NAT, routers, etc) between your computer and the remote server is not configured to allow VPN connections. Please contact your Administrator or your service provider to determine which device may be causing the problem.\n1 REG ADD HKLM\\SYSTEM\\CurrentControlSet\\Services\\PolicyAgent /v AssumeUDPEncapsulationContextOnSendRule /t REG_DWORD /d 0x2 /f 配置Ikev2 可参考docker-ipsec-vpn-server/README-zh.md at master · hwdsl2/docker-ipsec-vpn-server\ndocker logs ipsec-vpn-server查看IKEv2 相关配置信息\n1 2 3 4 5 6 7 IKEv2 is already set up. Details for IKEv2 mode: VPN server address: 208.216.217.35 VPN client name: vpnclient Client configuration is available inside the Docker container at: 首次启动时，可能使用的是默认配置，需要手动更改\n进入dockerdocker exec -it ipsec-vpn-server /bin/sh\n1 2 wget https://get.vpnsetup.net/ikev2addr -O ikev2addr.sh bash ikev2addr.sh 执行ikev2addr.sh去手动更改vpnserver address\n配置IKEv2 client（windows) 1 2 3 4 # 查看容器内的 /etc/ipsec.d 目录的文件 docker exec -it ipsec-vpn-server ls -l /etc/ipsec.d # 示例：将一个客户端配置文件从容器复制到 Docker 主机当前目录 docker cp ipsec-vpn-server:/etc/ipsec.d/vpnclient.p12 ./ 在创建IKEv2连接之前，需要在windows 客户端 导入证书\n1 2 # Import .p12 file (replace with your own value) certutil -f -importpfx \u0026#34;\\path\\to\\your\\file.p12\u0026#34; NoExport 可能遇到的问题 IKE authentication credentials are unacceptable\n可能是IKEv2 vpn server中 server address配置不对，可以尝试更改VPN server address\nPolicy match error\n1 REG ADD HKLM\\SYSTEM\\CurrentControlSet\\Services\\RasMan\\Parameters /v NegotiateDH2048_AES256 /t REG_DWORD /d 0x1 /f 在windows server 上搭建vpn server 可参考Windows Server 2012 R2 安装SSTP/L2TP/PPTP – Noname\n在windows 上安装并使用openvpn 参考链接\nopenvpn安装配置说明(windows系统) OpenVPN Windows 平台安装部署教程 安装openvpn https://github.com/OpenVPN/openvpn\nhttps://openvpn.net/community-downloads/\nopenvpn软件服务端和客户端都是同一个安装包 安装服务端的时候要选择Customize，勾选openvpn service和EasyRSA3 安装，用于服务端配置和证书生成使用。 安装客户端时可直接点击Install Now进行安装 证书生成 安装服务端的时候已经安装了证书生成工具**EasyRSA3，**使用此工具即可生成所需证书\n进入EasyRSA shell 环境dos窗口\n进入C:\\Program Files\\OpenVPN\\easy-rsa目录，双击EasyRSA-Start.bat 进入EasyRSA shell 环境dos窗口中\n初始化证书生成程序\n弹出的dos窗口中输入./easyrsa init-pki 初始化证书生成程序，初始化成功后会在C:\\Program Files\\OpenVPN\\easy-rsa目录下新建文件夹kpi\n生成ca证书\n在dos窗口中输入./easyrsa build-ca nopass生成无密码CA证书，生成过程中会要求输入证书名称，随意输入即可，生成结束后会打印出证书所在目录easy-rsa\\pki\\ca.crt\n生成服务端证书\n输入./easyrsa build-server-full server nopass 生成无密码服务端证书,生成后证书文件在C:\\Program Files\\OpenVPN\\easy-rsa\\pki\\issued文件夹\n生成客户端证书\n输入./easyrsa build-client-full client nopass生成无密码客户端证书,生成后证书在C:\\Program Files\\OpenVPN\\easy-rsa\\pki\\issued文件夹\n生成DH密钥交换协议\n输入./easyrsa gen-dh生成DH密钥交换协议文件,生成文件在C:\\Program Files\\OpenVPN\\easy-rsa\\pki目录下\n目录C:\\Program Files\\OpenVPN\\easy-rsa\\pki\\private下为证书key\nopenvpn --genkey tls-auth ta.keyto help block DoS attacks and UDP port flooding.\n配置文件 服务端配置文件模板为server.ovpn，客户端配置文件client.ovpn，在 C:\\Program Files\\OpenVPN\\sample-config目录下有。在服务端或客户端的windows主机上复制对应配置文件模板到C:\\Program Files\\OpenVPN\\config目录下\n配置文件中 ； # 都可以用作注释，但是不支持在配置指令的同一行后面添加注释\n例如 cipher AES-256-CBC #选择加密算法 这样写是错误的，解析时会将cipher 后的AES-256-CBC #选择加密算法整个字符串当成参数值，从而导致错误\n服务端server.ovpn 修改\n端口：（公网需要对应开通此端口）port udp 1194 协议文件名：dh dh2048.pem修改为dh dh.pem 运行多用户使用同一客户端证书：;duplicate-cn取消注释（前面;号删除）修改为duplicate-cn 客户端client.ovpn修改\n修改连接服务器地址: remote my-server-1 1194修改为remote 服务器公网ip 1194 取消注释掉此行: ;tls-auth ta.key 1修改为 tls-auth ta.key 1（前面;号删除） 证书复制 配置文件server.ovpn client.ovpn 中的证书 和 key 都是只有文件名 ，没有规定具体路径\n需要将其copy 到C:\\Program Files\\OpenVPN\\config 下\n将服务端证书，服务端key，ca证书，dh文件, ta.key复制到文件夹C:\\Program Files\\OpenVPN\\config下\n将客户端证书，客户端key，ca证书 , ta.key复制到目录C:\\Program Files\\OpenVPN\\config下\n连接 右键点击任务栏带锁小电脑图标，点击连接,连接成功后系统分配ip，小电脑变绿\n其它 tls-auth ta.key 配置为防御 DoS,UDP 淹没等恶意攻击行为的选项，如配置需要生成ta.key证书，生成方式：跳转到openvpn软件的bin目录下，在dos窗口中输入：openvpn.exe --genkey --secret ta.key ，便在bin目录下生成ta.key证书了，复制到配置文件目录即可。 证书时间等参数可以在vars文件中设置，设置后重新启动EasyRSA-Start即可加载新配置。 ;client-to-client为客户端之间是否能直接访问的配置，去掉注释后生效。 ;push \u0026ldquo;redirect-gateway def1 bypass-dhcp\u0026rdquo; 配置开启后客户端所有流量将路由至服务器，需要在访问端服务器上配置路由转发后才可以访问公网。 开启第4步后，可在客户端配置文件中增加route 192.168.1.0 255.255.255.0 net_gateway配置，使该网段不通过openvpn路由 ","date":"2025-09-01T19:31:02+08:00","permalink":"https://charles-7777.github.io/p/%E6%90%AD%E5%BB%BAvpn-server/","title":"搭建vpn server"},{"content":"官方文档\nCORE (Common Open Research Emulator) 是一个强大的、开源的网络模拟和仿真工具，广泛应用于网络研究、教学、协议开发和网络应用测试\n主题 描述 Architecture 体系结构概述，介绍如何使用Python、gRPC直接控制Core Installation CORE的安装方法及要求 GUI 如何使用GUI Node Types CORE支持的节点类型概述 (BETA) Python GUI 如何使用基于Python的BETA GUI Python API 介绍如何使用Python直接控制Core (自己实现core-daemon) gRPC API 介绍如何使用gRPC控制Core (连接core-daemon 调用其api) Distributed 在多个服务器上运行CORE的分布式细节 CTRLNET 如何控制网络从主机与节点通信 Services 概述所提供的服务并创建自定义服务 Performance 使用CORE时的性能说明 Developers Guide 概述如何对CORE开发 Core Emane CORE中运行和使用EMANE的高级主题和示例 Emane开发手册 Emane的架构介绍 开发相关 ","date":"2025-08-18T20:01:30+08:00","permalink":"https://charles-7777.github.io/p/corecommon-open-research-emulator%E7%BD%91%E7%BB%9C%E4%BB%BF%E7%9C%9F%E5%B7%A5%E5%85%B7/","title":"CORE(Common Open Research Emulator)网络仿真工具"},{"content":"IPv6 地址结构 一个 IPv6 地址由 128 个比特（bits）组成，通常分为 8 个 16 比特的块，每个块用冒号（:）分隔，并以十六进制表示。\n例如：2001:0db8:85a3:0000:0000:8a2e:0370:7334\n地址压缩规则 为了简化书写，IPv6 地址可以被压缩：\n省略前导零 每个块中开头的\n1 0 可以省略。\n0db8 -\u0026gt; db8 0000 -\u0026gt; 0 0370 -\u0026gt; 370 压缩连续的零: 可以使用双冒号 :: 来替代地址中任意一段连续的、全为 0 的块。注意：在一个地址中 :: 只能使用一次。\n应用上述规则后，上面的地址可以被简化为：\n1 2001:db8:85a3::8a2e:370:7334 IPv6 地址类型 IPv6 地址主要分为三种类型：单播（Unicast）、多播（Multicast）和任播（Anycast）。\n单播地址 (Unicast) 单播地址标识一个唯一的网络接口，发往单播地址的数据包将被送到该地址所标识的唯一接口。\n全球单播地址 (Global Unicast Address - GUA)\n范围: 全球唯一，可在公网路由。\n前缀: 通常以 2000::/3 开头。\n结构:\n全局路由前缀 (Global Routing Prefix): 通常为 48 位，由 ISP 分配给组织。 子网 ID (Subnet ID): 通常为 16 位，由组织内部规划子网。 接口 ID (Interface ID): 64 位，用于标识子网中的具体设备接口。 链路本地地址 (Link-Local Address)\n范围: 仅在同一物理或逻辑链路上有效，不能跨路由器路由。\n前缀: fe80::/10。\n用途: 用于邻居发现、自动地址配置等链路内部通信。设备启动后会自动生成。\n唯一本地地址 (Unique Local Address - ULA)\n范围: 仅在有限范围内（如一个组织或站点内部）使用，功能类似 IPv4 的私有地址。它不会在公网路由。\n前缀: fc00::/7。\n特殊地址\n环回地址: ::1/128，相当于 IPv4 的 127.0.0.1。\n多播地址 (Multicast) 多播地址标识一组网络接口，发往多播地址的数据包将被送到该组中的所有接口。\n前缀: ff00::/8。\n常见示例:\nff02::1: 链路本地范围内的所有节点。 ff02::2: 链路本地范围内的所有路由器。 任播地址 (Anycast) 任播地址也标识一组网络接口，但发往任播地址的数据包只会被送到这组接口中距离最近（根据路由协议的度量）的一个。任播地址的格式与单播地址相同，但通过路由配置实现。\nIPv6 地址分配方法 设备获取 IPv6 地址主要有以下几种方式：\n无状态地址自动配置 (SLAAC) SLAAC 是 IPv6 的核心特性之一，允许设备在没有 DHCP 服务器的情况下自动配置地址。\n过程:\n设备启动后，首先为自己生成一个链路本地地址。 设备向本地链路发送一个路由器请求 (Router Solicitation - RS) 多播消息。 链路上的路由器收到 RS 后，会回复一个路由器通告 (Router Advertisement - RA) 消息。RA 消息中包含了网络前缀（如 GUA 前缀）和其他网络信息。 设备使用 RA 中通告的前缀，并结合自己的接口 ID（通常通过 EUI-64 算法或随机生成）来构成一个完整的全球单播地址。 有状态 DHCPv6 (Stateful) 工作方式类似于 IPv4 的 DHCP，由 DHCPv6 服务器集中管理和分配 IPv6 地址。\n优点: 便于集中管理和审计，可以精确控制地址分配。 适用场景: 需要严格管理地址分配的企业网络。 无状态 DHCPv6 (Stateless) 这是一种混合模式。\n设备通过 SLAAC 获取 IPv6 地址。 同时，设备通过 DHCPv6 获取其他网络配置信息，例如 DNS 服务器地址、域名等。 这种方式既利用了 SLAAC 的便捷性，又弥补了其无法提供 DNS 等附加信息的不足。 静态配置 (Manual) 管理员手动为设备配置静态的 IPv6 地址、网关和 DNS 等信息。适用于服务器、路由器等需要固定地址的设备。\nDHCPv6 交互过程 DHCPv6 客户端和服务器之间的通信过程，用于获取 IPv6 地址和/或其他网络配置参数。标准的交互过程涉及四次消息交换，这确保了客户端可以选择最合适的服务器。\n四步交换 (Four-Way Exchange):\nSolicit (请求): 客户端在本地链路上发送一个 Solicit 多播消息，以发现可用的 DHCPv6 服务器。 消息发送到所有 DHCP 中继代理和服务器的多播地址 ff02::1:2。 Advertise (通告): 收到 Solicit 消息的 DHCPv6 服务器会回复一个 Advertise 单播消息。 此消息中包含了服务器可以为客户端提供的地址和/或配置信息。一个客户端可能会收到多个 Advertise 消息。 Request (请求): 客户端从收到的一个或多个 Advertise 消息中选择一个服务器，并向其发送一个 Request 单播消息，正式请求分配地址和/或配置参数。 消息中会包含它所选择的服务器的标识符。 Reply (回复): 被选中的服务器收到 Request 消息后，会发送一个 Reply 单播消息，确认地址分配和配置信息。 此时，客户端完成配置，可以开始使用获取到的地址。 两步交换 (Two-Way Exchange) - 快速提交 (Rapid Commit): 为了加速地址分配过程，DHCPv6 引入了快速提交选项。 Solicit (请求): 客户端在 Solicit 消息中加入 \u0026ldquo;Rapid Commit\u0026rdquo; 选项，表示希望立即完成地址分配。 Reply (回复): 支持快速提交的服务器如果愿意立即分配地址，会直接回复一个 Reply 消息，跳过 Advertise 和 Request 步骤。 这种两步交换减少了延迟，在许多场景下（如网络中只有一个 DHCPv6 服务器）非常高效 关键协议与技术 在一个部署 IPv6 的广播域中，相比 IPv4 一个最明显的区别就是 IPv6 路由通告；在 IPv4 时代，一个广播域中的 ipv4 网段并没有显式说明，或者说并没有一个 ipv4 网段宣称占据了这个广播域，仅使用这个广播域的广播/组播能力，多个ipv4网段共用一个广播域的情况也不鲜见；而在ipv6中，运行ipv6协议的路由器会主动发送名为路由通告 Router Advertisement的 ICMPv6 报文，在广播域中宣告/广播*此 ipv6 网段占据了该广播域；\n*（准确的讲应该是组播，IPv6 已经没有广播概念了，这里使用广播这个说法为了说明是公开发送的，在一个广播域中，广播和组播达成的功能类似，且在低端交换机中，广播和组播的转发方式是一致的）\nRA（路由通告 Router Advertisement）使用 ICMPv6 报文（type 134），属于 NDP 协议的一部分\nIPv6 的功能实现严重依赖于一些关键的底层协议，特别是 ICMPv6 和在其基础上构建的邻居发现协议 (NDP)，以及用于动态分配网络前缀的 DHCPv6-PD。\nICMPv6 IPv6 时代的ip层控制协议（类比ipv4中的arp、icmp等）均使用icmpv6报文，取消了IP层的广播，转而广泛使用组播。ICMPv6 中的 type 133 ~ 137 报文由NDP协议使用，用于发现网关、邻居、地址配置等等。\nICMPv6 ICMPv6 是 IPv6 的一个核心组成部分，其功能远超 IPv4 中的 ICMP。它不仅用于传输网络错误和诊断信息，还承载了许多关键的网络控制功能。如果防火墙错误地完全阻止 ICMPv6，将导致 IPv6 网络中断。\n主要功能:\n错误报告: 如 \u0026ldquo;目标不可达\u0026rdquo;、\u0026ldquo;数据包过大\u0026rdquo;、\u0026ldquo;超时\u0026rdquo; 等。 诊断查询: 如 ping 命令所使用的 \u0026ldquo;Echo Request\u0026rdquo; 和 \u0026ldquo;Echo Reply\u0026rdquo;。 网络控制: 为 NDP、MLD (多播侦听发现) 等协议提供消息支持。 NDP (邻居发现协议) NDP 是 IPv6 中一个至关重要的协议，它取代了 IPv4 中的 ARP、ICMP 路由器发现和 ICMP 重定向等多个协议的功能。NDP 基于 ICMPv6 实现，是实现即插即用和 SLAAC 的基础。\nNDP 的核心功能:\n地址解析: 将 IPv6 地址解析为链路层地址（如 MAC 地址），取代了 ARP。 路由器发现: 主机可以发现本地链路上的路由器。 前缀发现: 主机可以发现用于自动配置的地址前缀。 重复地址检测 (DAD): 节点可以确认其想要使用的地址在链路上是唯一的。 可达性跟踪: 判断邻居节点是否仍然可达。 NDP 使用的五种 ICMPv6 消息类型:\n路由器请求 (Router Solicitation - RS): 通常由终端主动发送，用于在未收到周期性 RA 的情况下，请求 RA 路由器通告 (Router Advertisement - RA):由路由器周期性发送，也可以响应 RS 发送，用于宣告本广播域中 IPv6 相关信息。比如前缀、网关、DNS等 邻居请求 (Neighbor Solicitation - NS): 用于解析邻居的链路层地址或执行 DAD,和 IPv4 的 ARP request 功能一致。 邻居通告 (Neighbor Advertisement - NA): 对 NS 消息的响应，或在链路层地址变化时主动发送和 ,IPv4 的 ARP reply功能一致。 重定向 (Redirect): 路由器告知主机有更优的下一跳路径,类似于 IPv4 的icmp redirect功能。 RS/RA RS/RA 是IPv6 环境下独有的控制信令，此信令明确了一个广播域中各节点所代表的角色。\n广播域中发送RA的为路由器\n广播域中可以存在多个路由器\nRA 组播周期性发送\nRS 用于在未收到 RA 情况下请求 RA\nRA 中的Flags表明当前广播域中 IPv6 地址的配置方式\n这些 Flags 分布在两个位置\u0026mdash;-ICMPv6 报头和 prefix option，后者称为 PIO(Prefix Information Option) flag。\nM(Managed address configuration):位于报头，M=1 表明终端应该使用 DHCPv6 协议配置地址和其他网络参数；如果M=1，则忽略 O Flag；此flag用于DHCPv6 有状态模式。 O(Other configuration):位于报头，O=1且M=0 表明终端应该使用 DHCPv6 设置除了 IPv6 地址之外的其他（dns/ntp等），此 flag 用于 DHCPv6 无状态模式。 A(Autononous address-configuration flag):位于PIO中，A=1 表明终端应该使用 SLAAC 配置 IPv6 地址；PIO可以包含多个 Prefix，这也就意味着一个广播域中可以存在多个 IPv6 网段。 这几种flag是可以复合使用的，以满足不同场景需求,除了手动静态配置ipv6地址，动态/自动配置或者半自动配置ipv6地址的方式都需要从ndp协议的 RA 开始，就好像 RA 引导了IPv6自动配置的开始；RA中的不同flag叠加影响地址自动配置；\nDHCPv6-PD DHCPv6前缀代理DHCPv6-PD(PrefixDelegation)是一种前缀分配机制，通过DHCPv6前缀代理机制，下游 网络设备不需要再手工指定用户侧链路的IPv6地址前缀，它只需要向上游网络设备提出前缀分配申请，上游网 络设备便可以分配合适的地址前缀给下游设备，下游设备把获得的前缀再通过路由通告(RA)至与IPv6主机直连 的用户链路上，实现IPv6主机的地址自动配置，完成整个系统层次的地址布局。\nDHCP-PD 技术最早在RFC3633中提出，经过几次更新目前最新的是RFC8415，其主要思想是把DHCPv6的地址分配方式划分为多层，dhcp client不再是仅仅获取地址用于终端通信，而是可以作为次级路由器身份把地址层层分配下去；这样的好处是便于快速和统一部署大量地址，尤其是在大规模动态地址的情况下，此种地址分配方式在家庭宽带中已经大量部署；光猫从其上级获取一段地址用于本地网络终端的地址分配，甚至从光猫拿到的地址还能再往下一层分配.\n工作原理:\n请求: 家庭路由器（客户端）向其上游 ISP 的路由器（服务器）发送 DHCPv6 请求，希望获得一个 IPv6 地址前缀（例如一个 /56 或 /48 的地址块），而不仅仅是一个地址。 代理: ISP 路由器从其地址池中分配一个前缀块，并通过 DHCPv6 回复将其“代理”或“授权”给家庭路由器。 分配: 家庭路由器获得该前缀后，就拥有了管理这个地址块的权限。它可以将这个前缀进一步划分为更小的子网（例如多个 /64 子网），并为连接到其 LAN 口的各个内部网络（如家庭网络、访客网络）分配这些子网前缀。 通告: 家庭路由器在其内部网络上发送 RA 消息，通告这些 /64 的子网前缀，使得内部网络中的设备可以通过 SLAAC 自动配置 IPv6 地址。 优势:\n自动化: 无需手动配置下游路由器的子网，实现了网络部署的自动化。 结构化: 使得家庭或小型办公室可以轻松地拥有多个独立的 IPv6 子网，便于网络隔离和管理。 高效性: 相比于 NAT，每个设备都能获得全球唯一的公网地址，实现了真正的端到端连接。 link-local 对比IPv4环境，IPv6 地址有所谓的 link-local 地址而且充当比较重要作用；由于一个接口可以配置很多IPv6地址，当这些地址作为下一跳使用的时候会出现混乱，这样不利于管理也不利于设备性能开销,使用Link Local地址唯一标识链路上的一个节点就避免了这个问题。而且在网络重新编址过程中，Link Local并不会发生变化，更利于快速更改编址\nDAD DAD 基于 ICMPv6 Neighbor Solicitation（NS，邻居请求） 和 Neighbor Advertisement（NA，邻居通告） 消息实现，步骤如下：\n节点分配一个 IPv6 地址（如通过 SLAAC 或手动配置），但该地址尚未正式启用（处于 \u0026ldquo;tentative\u0026rdquo; 状态）。 发送 Neighbor Solicitation（NS）： 目标地址（Target Address）设为待检测的 IPv6 地址。 源地址设为 未指定地址（::）（因为该地址尚未正式使用）。 发送到 \u0026ldquo;请求节点组播地址\u0026rdquo;（Solicited-Node Multicast Address，FF02::1:FFXX:XXXX）。 等待响应： 如果没有收到 Neighbor Advertisement（NA），说明地址未被占用，可以正常使用。 如果收到 NA，说明地址已被占用，节点必须放弃该地址并重新配置（如 SLAAC 会生成新地址）。 DAD 完成： 成功通过检测后，地址变为 \u0026ldquo;Preferred\u0026rdquo; 或 \u0026ldquo;Valid\u0026rdquo; 状态，可以正常通信 邻居表 邻居表，在IPv4环境下就是ARP表，系统维护Arp表内容由系统自己决定，比如端口、更新时间等；\n到了IPv6环境，ND表代替了arp表，而且协议规定了5种邻居状态，IPv6 邻居状态，分别是： Incomplete、Reachable、Stale、Delay、Probe，其中只有 Stale 状态是稳定状态。\nIncomplete （未完成状态）：表⽰正在解 析地址，但邻居链路层地址尚未确定。 Reachable （可达状态）：表⽰地址解析 成功，该邻居可达。 Stale（失效状态）：表⽰可达时间耗尽，未确定邻居是否可达。 Delay（延迟状态）：表⽰未确定邻居是否可达。Delay 状态不是⼀个稳定的状态，⽽是⼀个延时等待状态。 Probe （探测状态）：节点会向处于 Probe 状态的邻居持续发送 NS 报⽂。 不同状态之间迁移如下图 1 2 ip neigh ip -6 neigh ","date":"2025-08-15T19:58:16+08:00","permalink":"https://charles-7777.github.io/p/ipv6-%E5%9C%B0%E5%9D%80%E5%88%86%E9%85%8D%E8%AF%A6%E8%A7%A3/","title":"IPv6 地址分配详解"},{"content":"一般情况下如果我们想使用其他文件的函数时，常规操作是将该函数在头文件中声明，然后在需要使用的文件里引用这个头文件 但若是没声明呢？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 11.c #include \u0026lt;stdio.h\u0026gt; extern char* b; int main() { printf(\u0026#34;aaa--d:%x----%s--p\\n\u0026#34;, aaa(),b,b); char*p = aaa(); printf(\u0026#34;p = %p\\n\u0026#34;, p); if(p) printf(\u0026#34;---\\n\u0026#34;); printf(\u0026#34;value = %s\\n\u0026#34;，p);// 崩溃! return 0; } 2.c char *b=\u0026#34;aaa\u0026#34;; char* aaa(){ return b;} 1 2 3 4 5 6 7 8 9 10 11 12 13 gcc 11.c 2.c -o test 11.c: In function ‘main\u0026#39; 11.c:4:37: warning:: implicit declaration of function ‘aaa\u0026#39;[-Wimplicit-function-declaration] 4| printf(\u0026#34;aaa--d:%x----%s--%p\\n\u0026#34;,aaa(),b,b); | ^~~ 11.c:5:15: warning:： initialization of ‘char *’ fromn‘int\u0026#39; makes pointer from integer without a cast [-Wint-conversion] 5| char *p = aaa(); | ^~~ ./test aaa--d:4b1c032----aaaa--0x560604b1c032 p=0x4b1c032 --- Segmentation fault (core dumped) 这里并没有引用这个头文件，为什么依然可以使用呢？\n这就引出了一个概念：隐式声明。\n简单来讲就是当前文件使用没有声明的函数时，编译器会自动声明一个，但返回值是int类型\n那为什么会出现段错误呢? 上面编译运行的环境是64bit的:\nchar* aaa() 返回的是一个指针8B(64bit) 但是在编译时 已经确定了是返回值类型(由于是隐式声名,编译器默认是int) 是int 4B(32bit) ,所以就会把指针的低32bit 取出来 赋给char* *p 指针p 的值就是一个截断的值, 这个指针就变成了一个非法的了,如果取访问其指向的内存,就会出现 段错误.\n如果是32bit 环境呢\nint 是4B 指针也是4B ,指针不会被截断,还是原来的值,不会出现非法访问.\n如何避免呢: 在Makefile 里加上 -Wall -Werror ,编译时会检测出这种错误\n","date":"2025-08-15T16:22:16+08:00","permalink":"https://charles-7777.github.io/p/implicit-function-declaration/","title":"implicit-function-declaration"},{"content":"什么是虚拟网卡？ 虚拟网卡是软件实现的网络接口，与物理网卡不同，它没有物理硬件，只存在于操作系统的内存中。虚拟网卡可以用来模拟网络环境，进行数据包的捕获、分析和处理。 TUN 和 TAP 的基本概念 Tun （Network TUNnel） TUN 是三层（网络层）的虚拟网络设备，主要用于 IP 数据包的处理。TUN 设备会模拟一个网络层接口，接收到的数据包会被传递给用户空间的程序进行处理，处理完的数据包会被发送回内核网络栈。\nTAP（Network TAP） TAP 是二层（数据链路层）的虚拟网络设备，主要用于以太网帧的处理。TAP 设备可以模拟一个以太网接口，能够接收和发送原始的以太网帧。这使得 TAP 设备非常适合用于桥接不同的网络环境，或者在虚拟机中模拟物理网卡。TAP是数据链路层的虚拟网络设备。\nTun和Tap的异同 Tun是三层的设备，该设备没有MAC地址，从字符设备上读取IP数据包，写入的也是IP数据包，因此不能进行二层的操作，例如发ARP包和以太网广播 Tab是二层的设备，该设备有MAC地址，处理的是数据链路层的数据帧，从字符设备上读取的是数据链路层的数据帧，写入的也是数据。\n在使用上面，两者都是通过字符设备的方式进行读取和写入，Tun是三层网络设备，而Tab是二层网络设备，Tun常用于VPN等技术，由于工作在IP层，无法与物理网卡做bridge，但可以通过三层交换（如 ip_forward）与物理网卡连通，Tab设备工作在第二层，收发的是MAC层数据包，拥有MAC层的功能，可以与物理网卡做bridge，支持MAC层广播 Tun和Tap应用场景 Tun是一个网络层设备，支持点到点的网络通信，常用于tunnel隧道和VPN的构建，tunnel技术是网络设备把网络层数据包封装到另一个协议中以跨过网络传送到另一个网络设备的处理过程，主要用于公网主机和私有网络互联互通。在Linux系统中支持多种隧道技术，其底层实现原理都是基于Tun设备。 TAP接口的典型应用场景是在虚拟化网络中。例如，我们通过KVM创建的多个VM（虚拟机），以LinuxBridge（桥接网络）互通；实际上即是通过像vnet0这样的TAP接口来接入LinuxBridge的。在这种场景下，KVM程序就是向TAP接口读写数据的用户空间程序。当VM0向本机的eth0接口发送数据，KVM会将数据发送到TAP接口vnet0，再通过LinuxBridge将数据转发到vnet1上。然后，KVM将数据发送到VM1的eth0口。 Tun 配置 1 2 3 4 5 6 7 8 \\# 创建网卡并配置IP ip tuntap add dev tun0 mode tun ip link set dev tun0 up ip addr add 10.0.0.1/24 dev tun0 ip route add 10.0.0.0/24 via 10.0.0.1 \\# 清除网卡 ip link set dev tun0 down ip tuntap del dev tun0 mode tun 转载自\nLinux虚拟网卡TUN和TAP - 心若向阳花自开 - 博客园\n参考资料 Universal TUN/TAP device driver Universal TUN/TAP device driver Frequently Asked Question Tun/Tap interface tutorial A simplistic, simple-minded, naive tunnelling program using tun/tap interfaces and TCP ","date":"2025-08-07T15:17:36+08:00","permalink":"https://charles-7777.github.io/p/linux%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1tun%E5%92%8Ctap/","title":"Linux虚拟网卡TUN和TAP"},{"content":"作业控制 (Job Control) 作业控制是大多数 shell 提供的一个功能，允许用户在单个终端上同时运行和管理多个命令（作业）。\n前台作业 (Foreground Job): 在终端中启动，它会独占终端的输入和输出。在它运行期间，shell 会被挂起，等待该作业完成。\n后台作业 (Background Job): 通过在命令后添加 \u0026amp; 符号启动。它不会占用终端，你可以在它运行时继续在 shell 中输入其他命令。\nCtrl+Z(SIGTSTP）暂停前台进程Ctrl+C(SIGINT）终止当前前台进程Ctrl+D（EOF）\n常用命令: jobs, fg, bg, kill %\u0026lt;job_id\u0026gt;。\n进程组 (Process Group) 进程组是一个或多个进程的集合。系统中的每个进程都属于一个进程组。\n目的: 主要用于作业控制，方便将信号（如 SIGINT, SIGTSTP）发送到一组相关的进程。例如，当你在终端按下 Ctrl+C 时，SIGINT 信号会被发送到当前前台作业的整个进程组。\n进程组ID (PGID): 每个进程组都有一个唯一的 ID。\n进程组领导 (Process Group Leader): 进程组中第一个创建的进程，其 PID 通常就是该进程组的 PGID。\n一个管道（pipeline）中的所有进程（例如 cat file | grep \u0026quot;text\u0026quot; | wc -l）通常属于同一个进程组。\n会话 (Session) 会话是一个或多个进程组的集合。它提供了一个更高层次的进程组织方式。\n目的: 将一个用户登录到退出期间创建的所有进程组织在一起。\n会话ID (SID): 每个会话都有一个唯一的 ID。\n会话领导 (Session Leader): 创建该会话的进程。通常，这是用户登录时启动的 shell 进程。\n控制终端 (Controlling Terminal): 一个会话通常与一个控制终端相关联。当控制终端断开连接时（例如关闭终端窗口或网络断开），内核会向会话领导发送 SIGHUP 信号，后者通常会将其传播给会话中的所有进程，导致它们终止。\nsetsid() 系统调用 setsid() 是一个关键的系统调用，用于创建一个新的会话。\n功能: 1. 调用 setsid() 的进程会成为一个新会话的会话领导。\n2. 该进程会成为一个新进程组的进程组领导。\n3. 该进程会脱离它之前的控制终端。\n前提: 调用 setsid() 的进程不能是某个现有进程组的领导者。为了确保这一点，通常的做法是 fork() 一个子进程，然后让父进程退出，子进程再调用 setsid()。\n用途: 这是创建守护进程（Daemon）的标准方法。通过创建一个没有控制终端的新会hs话，守护进程可以确保自己不会因为终端的关闭而意外终止。\nlxc-attach 后台进程导致退出卡住的原因分析 问题场景 用户在终端中执行 lxc-attach \u0026lt;container_name\u0026gt;。\n在 lxc-attach 创建的 shell 中，启动一个后台作业。\n输入 exit 尝试退出 lxc-attach 的 shell。\n此时，终端卡住，用telnet 连接然后kill 掉才可以回到host 主机的shell\n原因剖析 这个问题的核心在于进程关系和信号处理。\n进程结构: - 当你运行 lxc-attach 时，它会在容器内启动一个新的 shell 进程（如 bash）。\n- 这个新的 shell 是 lxc-attach 进程的子进程。\n- 重要的是，这个新 shell 没有成为新的会话领导。它与 lxc-attach 进程、以及你最初的登录 shell 位于同一个会话中，并共享同一个控制终端。\n启动后台进程: - 当你在 lxc-attach 的 shell 中运行一个后台进程。\n- 这个 后台进程与 lxc-attach 的 shell 属于同一个进程组。\n执行 exit: - 你输入 exit，lxc-attach 的 shell 进程开始退出流程。\n- shell 进程本身会终止。\n- lxc-attach 进程在等待其子进程（即那个 shell）完全终止。\n卡住的根源: - shell 进程虽然终止了，但它启动的后台进程仍然在运行。\n- 这个后台进程仍然是前台进程组的一部分（相对于控制终端而言），或者说它仍然与控制终端关联。\n- 控制终端的驱动程序会发现，虽然前台的 shell 退出了，但该进程组里还有其他进程 在运行。此时，终端会处于一种“挂起”或“等待”状态，因为它需要处理这个仍在运行的后台进程的标准输入/输出（即使它不读写）。\n- lxc-attach 进程本身也在等待，因为它可能需要清理与子进程相关的所有资源。只要 这个后台进程还在运行，整个进程链就无法干净地结束。\n- 只有当 这个后台进程结束后，整个进程组才算完全终结，控制终端的锁定状态被解除，lxc-attach 进程也随之退出，你才能回到原来的 shell 提示符。\n解决办法: nohup setsid disown\n参考链接 Linux session和进程组概述 - Linux程序员 - SegmentFault 思否\nLinux TTY/PTS概述 - Linux程序员 - SegmentFault 思否\n","date":"2025-08-06T15:36:36+08:00","permalink":"https://charles-7777.github.io/p/linux-%E4%BD%9C%E4%B8%9A%E6%8E%A7%E5%88%B6%E8%BF%9B%E7%A8%8B%E7%BB%84%E5%92%8C%E4%BC%9A%E8%AF%9D/","title":"Linux 作业控制、进程组和会话"},{"content":"frok 一个运行的进程,它调用了fork()函数，然后就产生了子进程，原来的进程叫父进程。这个子进程也是进程，但凡是进程，都有自己的虚拟地址空间（让每个进程自己看起来是独占内存，通过段页内存管理映射到不同的物理地址空间）。虚拟地址空间是从0到4G的大小，其中3-4G是属于内核的（32位系统）。创建完子进程后，父进程继续运行程序（即原来的进程）的代码，刚创建出来的子进程拥有和父进程完全一样的代码段，数据段，也就是说完完全全拷贝了一份父进程，和父进程完全一样。即clone父进程0-3G的内容，而3-4G的kernel只需要重新映射一下到物理地址的kernel即可。但是操作系统要如何区分这两个进程呢？答案就是进程ID即pid。pid是存储在PCB当中的类似身份证的东西. kernel会创建子进程自己的PCB,然后clone父进程的PCB(task_struct)的绝大部分信息，如内存映射(mm_struct，采用 COW 机制copy on write),文件描述符表,调度信息（优先级、CPU 时间等）,但某些关键字段会被修改,如pid,ppid等.\nkernel会设置 fork() 的返回值.在子进程的 task_struct 中，kernel会预先设置 eax/rax 寄存器（存储返回值）为 0，因此子进程看到的 fork() 返回 0。父进程的 fork() 返回子进程的 pid。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; int q_val = 100; int main() { printf(\u0026#34;father is running, pid: %d, ppid: %d\\n\u0026#34;, getpid(), getppid()); sleep(2); pid_t id = fork(); int cnt = 3; while(1) { if(id == 0) { // Child process printf(\u0026#34;I am child process, pid: %d, ppid: %d, q_val: %d, \u0026amp;q_val: %p\\n\u0026#34;, getpid(), getppid(), q_val, \u0026amp;q_val); sleep(1); cnt--; if(cnt == 0) { q_val = 300; printf(\u0026#34;I am child, q_val is changed, 100 -\u0026gt; 300\\n\u0026#34;); } } else { // Parent process printf(\u0026#34;I am father process, pid: %d, ppid: %d, q_val: %d, \u0026amp;q_val: %p\\n\u0026#34;, getpid(), getppid(), q_val, \u0026amp;q_val); sleep(1); } } return 0; } fork底层是调用了内核的函数来实现fork的功能的，即先create()先创建进程，此时进程内容为空，然后clone()复制父进程的内容到子进程中，此时子进程就诞生了，接着父进程就return返回了。而子进程诞生后，是直接运行return返回的，然后接着执行后面的程序，这里注意：子进程是不会执行前面父进程已经执行过的程序了得，因为PCB中记录了当前进程运行到哪里，而子进程又是完全拷贝过来的，所以PCB的程序计数器也是和父进程相同的，所以是从fork()后面的程序继续执行。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 father is running, pid: 2193928, ppid: 784260 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child, q_val is changed, 100 -\u0026gt; 300 I am child process, pid: 2193930, ppid: 2193928, q_val: 300, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 300, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid: 2193930, ppid: 2193928, q_val: 300, \u0026amp;q_val: 0x55e878c7f010 I am father process, pid: 2193928, ppid: 784260, q_val: 100, \u0026amp;q_val: 0x55e878c7f010 I am child process, pid exec族 用fork创建子进程后执行的是和父进程相同的程序（但有可能执行不同的代码分支）， 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时，该进程的 用户空间代码和数据完全被新程序替换，从新程序的启动例程开始执行。调用exec并不创建 新进程，所以调用exec前后该进程的id并未改变\n1 2 3 4 5 6 7 #include int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]); 这些函数如果调用成功则加载新的程序从启动代码开始执行，不再返回，如果调用出错 则返回-1，所以exec函数只有出错的返回值而没有成功的返回值\nwait/waitpid 僵尸进程: 子进程退出，父进程没有回收子进程资源（PCB），则子进程变成僵尸进程 孤儿进程: 父进程先于子进程结束，则子进程成为孤儿进程,子进程的父进程成为1号 进程init进程，称为init进程领养孤儿进程\n一个进程在终止时会关闭所有文件描述符，释放在用户空间分配的内存，但它的PCB还 保留着，内核在其中保存了一些信息：如果是正常终止则保存着退出状态，如果是异常终止 则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这 些信息，然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变 量$?查看，因为Shell是它的父进程，当它终止时Shell调用wait或waitpid得到它的退出状 态同时彻底清除掉这个进程。但是，如果父进程先于子进程结束，则子进程成为孤儿进程。孤儿进程将被 init 进程（进程号为1）领养，并由 init 进程对孤儿进程完成状态收集工作。而如果子进程先于父进程退出，同时父进程太忙了，无瑕回收子进程的资源，子进程残留资源（PCB）存放于内核中，变成僵尸。任何进程在刚终止时都是僵尸进程，正常情况下，僵 尸进程都立刻被父进程清理了.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int main(void) { pid_t pid; pid = fork(); if (pid == 0) { printf(\u0026#34;I am child, my parent= %d, going to sleep 3s\\n\u0026#34;, getppid()); sleep(3); printf(\u0026#34;-------------child die--------------\\n\u0026#34;); } else if (pid \u0026gt; 0) { printf(\u0026#34;I am parent, pid = %d, myson = %d, going to sleep 5s\\n\u0026#34;, getpid(), pid); sleep(5); system(\u0026#34;ps -o pid,ppid,state,tty,command\u0026#34;); } else { perror(\u0026#34;fork\u0026#34;); return 1; } return 0; } 在这个程序里，父进程创建子进程之后，就休眠 5 秒钟。而子进程只休眠 3 秒钟就退出，在它退出之后，父进程还未苏醒，因此没人给子进程「收尸」，所以它就变成了僵尸进程。\n僵尸进程其实已经就是退出的进程，因此无法再利用kill命令杀死僵尸进程。僵尸进程的罪魁祸首是父进程没有回收它的资源，那我们可以想办法它其它进程去回收僵尸进程的资源，这个进程就是 init 进程。因此，我们可以直接杀死父进程，init 进程就会很善良地把那些僵尸进程领养过来，并合理的回收它们的资源，那些僵尸进程就得到了妥善的处理了\n","date":"2025-07-31T17:22:16+08:00","permalink":"https://charles-7777.github.io/p/%E8%BF%9B%E7%A8%8B%E5%8E%9F%E8%AF%ADfork-exceve-waitpid/","title":"进程原语fork exceve waitpid"},{"content":"openwrt procd启动流程分析 kernel_init Linux内核执行start_kernel函数时会调用kernel_init来启动init进程，流程如下图\nstart_kernel\u0026ndash;\u0026gt;rest_init\u0026ndash;\u0026gt;kernel_init\u0026ndash;\u0026gt;try_to_run_init_process\nkernel_init()(位于 linux-4.1.52/init/main.c)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic(\u0026#34;Requested init %s failed (error %d).\u0026#34;, execute_command, ret); } if (!try_to_run_init_process(\u0026#34;/sbin/init\u0026#34;) || !try_to_run_init_process(\u0026#34;/etc/init\u0026#34;) || !try_to_run_init_process(\u0026#34;/bin/init\u0026#34;) || !try_to_run_init_process(\u0026#34;/bin/sh\u0026#34;)) return 0; panic(\u0026#34;No working init found. Try passing init= option to kernel. \u0026#34; \u0026#34;See Linux Documentation/init.txt for guidance.\u0026#34;); } /sbin/init openwrt 源码 openwrt/package/system/procd/Makefile\n1 2 3 4 5 6 7 8 9 10 define Package/procd/install $(INSTALL_DIR) $(1)/sbin $(1)/etc $(1)/lib/functions $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/{init,procd,askfirst,udevtrigger,upgraded} $(1)/sbin/ $(INSTALL_DATA) $(PKG_INSTALL_DIR)/usr/lib/libsetlbf.so $(1)/lib $(INSTALL_BIN) ./files/reload_config $(1)/sbin/ $(INSTALL_CONF) ./files/hotplug*.json $(1)/etc/ $(INSTALL_DATA) ./files/procd.sh $(1)/lib/functions/ $(INSTALL_BIN) ./files/service $(1)/sbin/service endef procd源码procd/CMakeList.txt\n1 2 3 4 5 6 7 8 9 10 IF(DISABLE_INIT) ADD_DEFINITIONS(-DDISABLE_INIT) ELSE() ADD_EXECUTABLE(init initd/init.c initd/early.c initd/preinit.c initd/mkdev.c sysupgrade.c watchdog.c utils/utils.c) TARGET_INCLUDE_DIRECTORIES(init PUBLIC ${SELINUX_INCLUDE_DIRS}) TARGET_LINK_LIBRARIES(init ${LIBS} ${SELINUX_LIBRARIES}) INSTALL(TARGETS init RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} ) procd 启动流程 /sbin/init main 函数入口位于 procd/initd/init.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int main(int argc, char **argv) { pid_t pid; ulog_open(ULOG_KMSG, LOG_DAEMON, \u0026#34;init\u0026#34;); sigaction(SIGTERM, \u0026amp;sa_shutdown, NULL); sigaction(SIGUSR1, \u0026amp;sa_shutdown, NULL); sigaction(SIGUSR2, \u0026amp;sa_shutdown, NULL); sigaction(SIGPWR, \u0026amp;sa_shutdown, NULL); if (selinux(argv)) exit(-1); early(); cmdline(); watchdog_init(1); pid = fork(); if (!pid) { char *kmod[] = { \u0026#34;/sbin/kmodloader\u0026#34;, \u0026#34;/etc/modules-boot.d/\u0026#34;, NULL }; if (debug \u0026lt; 3) patch_stdio(\u0026#34;/dev/null\u0026#34;); execvp(kmod[0], kmod); ERROR(\u0026#34;Failed to start kmodloader: %m\\n\u0026#34;); exit(EXIT_FAILURE); } if (pid \u0026lt;= 0) { ERROR(\u0026#34;Failed to start kmodloader instance: %m\\n\u0026#34;); } else { const struct timespec req = {0, 10 * 1000 * 1000}; int i; for (i = 0; i \u0026lt; 1200; i++) { if (waitpid(pid, NULL, WNOHANG) \u0026gt; 0) break; nanosleep(\u0026amp;req, NULL); watchdog_ping(); } } uloop_init(); preinit(); uloop_run(); return 0; } kmodloader 先启动的是kmodloader(实现于openwrt/ubox/kmodloader.c),会insmod位于/etc/modules.d/下的kernel module list\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 static int main_loader(int argc, char **argv) { int gl_flags = GLOB_NOESCAPE | GLOB_MARK; char *dir = \u0026#34;/etc/modules.d/\u0026#34;; struct module_node *mn; struct module *m; glob_t gl; char *path; int ret = 0, fail, j; if (argc \u0026gt; 1) dir = argv[1]; path = malloc(strlen(dir) + 2); if (!path) { ULOG_ERR(\u0026#34;out of memory\\n\u0026#34;); return -1; } strcpy(path, dir); strcat(path, \u0026#34;*\u0026#34;); if (scan_module_folders()) { ret = -1; goto free_path; } if (scan_loaded_modules()) { ret = -1; goto free_path; } ULOG_INFO(\u0026#34;loading kernel modules from %s\\n\u0026#34;, path); ...... } int main(int argc, char **argv) { char *exec = basename(*argv); avl_init(\u0026amp;modules, avl_modcmp, true, NULL); if (!strcmp(exec, \u0026#34;insmod\u0026#34;)) return main_insmod(argc, argv); if (!strcmp(exec, \u0026#34;rmmod\u0026#34;)) return main_rmmod(argc, argv); if (!strcmp(exec, \u0026#34;lsmod\u0026#34;)) return main_lsmod(argc, argv); if (!strcmp(exec, \u0026#34;modinfo\u0026#34;)) return main_modinfo(argc, argv); load_options(); if (!strcmp(exec, \u0026#34;modprobe\u0026#34;)) return main_modprobe(argc, argv); ulog_open(ULOG_KMSG, LOG_USER, \u0026#34;kmodloader\u0026#34;); return main_loader(argc, argv); } uloop_init实现位于libubox源码uloop.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int uloop_init(void) { if (uloop_init_pollfd() \u0026lt; 0) return -1; if (waker_init() \u0026lt; 0) { uloop_done(); return -1; } return 0; } static int uloop_init_pollfd(void) { if (poll_fd \u0026gt;= 0) return 0; poll_fd = epoll_create(32); if (poll_fd \u0026lt; 0) return -1; fcntl(poll_fd, F_SETFD, fcntl(poll_fd, F_GETFD) | FD_CLOEXEC); return 0; } preinit实现位于procd源码文件initd/preinit.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static struct uloop_process preinit_proc; static struct uloop_process plugd_proc; void preinit(void) { char *init[] = { \u0026#34;/bin/sh\u0026#34;, \u0026#34;/etc/preinit\u0026#34;, NULL }; char *plug[] = { \u0026#34;/sbin/procd\u0026#34;, \u0026#34;-h\u0026#34;, \u0026#34;/etc/hotplug-preinit.json\u0026#34;, NULL }; int fd; LOG(\u0026#34;- preinit -\\n\u0026#34;); plugd_proc.cb = plugd_proc_cb; plugd_proc.pid = fork(); if (!plugd_proc.pid) { execvp(plug[0], plug); ERROR(\u0026#34;Failed to start plugd: %m\\n\u0026#34;); exit(EXIT_FAILURE); } if (plugd_proc.pid \u0026lt;= 0) { ERROR(\u0026#34;Failed to start new plugd instance: %m\\n\u0026#34;); return; } uloop_process_add(\u0026amp;plugd_proc); setenv(\u0026#34;PREINIT\u0026#34;, \u0026#34;1\u0026#34;, 1); fd = creat(\u0026#34;/tmp/.preinit\u0026#34;, 0600); if (fd \u0026lt; 0) ERROR(\u0026#34;Failed to create sentinel file: %m\\n\u0026#34;); else close(fd); preinit_proc.cb = spawn_procd; preinit_proc.pid = fork(); if (!preinit_proc.pid) { execvp(init[0], init); ERROR(\u0026#34;Failed to start preinit: %m\\n\u0026#34;); exit(EXIT_FAILURE); } if (preinit_proc.pid \u0026lt;= 0) { ERROR(\u0026#34;Failed to start new preinit instance: %m\\n\u0026#34;); return; } uloop_process_add(\u0026amp;preinit_proc); DEBUG(4, \u0026#34;Launched preinit instance, pid=%d\\n\u0026#34;, (int) preinit_proc.pid); } 创建子进程执行 /sbin/procd -h /etc/hotplug-preinit.json ，主进程同时使用 uloop_process_add()把 /sbin/procd 子进程加入 uloop 进行监控，当 /sbin/procd 进程结束时回调 plugd_proc_cb 函数。 创建子进程执行 /etc/preinit 脚本，此时 PREINIT环境变量被设置为1，主进程同时使用 uloop_process_add() 把/etc/preinit 子进程加入 uloop 进行监控，当 /etc/preinit 执行结束时回调 spawn_procd函数 spawn_procd()函数繁行后继真正使用的 /sbin/procd 进程，从 /tmp/debuglevel 读出 debug 级别并设置到环境变量 DBGLVL 中，把 watchdog fd 设置到环境变量 WDTFD 中，最后调用 execvp()繁行 /sbin/procd 进程 首先看procd，因为带有参数“-h /etc/hotplug-preinit.json”，所以会执行hotplug_run函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int main(int argc, char **argv) { int ch; char *dbglvl = getenv(\u0026#34;DBGLVL\u0026#34;); int ulog_channels = ULOG_KMSG; if (dbglvl) { debug = atoi(dbglvl); unsetenv(\u0026#34;DBGLVL\u0026#34;); } while ((ch = getopt(argc, argv, \u0026#34;d:s:h:S\u0026#34;)) != -1) { switch (ch) { case \u0026#39;h\u0026#39;: return hotplug_run(optarg); // hotplug case \u0026#39;s\u0026#39;: ubus_socket = optarg; break; case \u0026#39;d\u0026#39;: debug = atoi(optarg); break; case \u0026#39;S\u0026#39;: ulog_channels = ULOG_STDIO; break; default: return usage(argv[0]); } } ulog_open(ulog_channels, LOG_DAEMON, \u0026#34;procd\u0026#34;); ulog_threshold(LOG_DEBUG + 1); setsid(); uloop_init(); procd_signal(); procd_udebug_set_enabled(true); if (getpid() != 1) procd_connect_ubus(); else procd_state_next(); uloop_run(); uloop_done(); return 0; } hotplug实现如下，这里是建立netlink通信机制，完成用户层和内核的交互，监听内核的uevent事件。\nprocd/plug/hotplug.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void hotplug(char *rules) { struct sockaddr_nl nls; int nlbufsize = 512 * 1024; rule_file = strdup(rules); memset(\u0026amp;nls,0,sizeof(struct sockaddr_nl)); nls.nl_family = AF_NETLINK; nls.nl_pid = getpid(); nls.nl_groups = -1; if ((hotplug_fd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT)) == -1) { ERROR(\u0026#34;Failed to open hotplug socket: %s\\n\u0026#34;, strerror(errno)); exit(1); } if (bind(hotplug_fd.fd, (void *)\u0026amp;nls, sizeof(struct sockaddr_nl))) { ERROR(\u0026#34;Failed to bind hotplug socket: %s\\n\u0026#34;, strerror(errno)); exit(1); } if (setsockopt(hotplug_fd.fd, SOL_SOCKET, SO_RCVBUFFORCE, \u0026amp;nlbufsize, sizeof(nlbufsize))) ERROR(\u0026#34;Failed to resize receive buffer: %s\\n\u0026#34;, strerror(errno)); json_script_init(\u0026amp;jctx); queue_proc.cb = queue_proc_cb; uloop_fd_add(\u0026amp;hotplug_fd, ULOOP_READ); } int hotplug_run(char *rules) { uloop_init(); hotplug(rules); uloop_run(); return 0; } 内核发出uevent事件 内核使用 uevent 事件通知用户空间， uevent 首先在内核中调用 netink_kemel_create() 函数创建一个 socket 套接字，该函数原型在 netink.h 中定义。这是一种特殊类型的 socket ，专门用于内核空间与用户空间的异步通信。kobject_uevent()产生uevent 事件 (/lib/kobject_uevent.c)，事件的部分信息通过环境变量传递，如$ACTION,$DEVPATH,$SUBSYSTEM 等，产生的 uevent 先由 netlink_broadcast_filtered()发出，最后调用uevent helper 所指定的程序来处理。在linux 中，uevent_helper 里默认指定\u0026quot;/sbin/hotplug”，但可以通过 /sys/kemel/uevent helper (kernel/ksysfs.c) /proc/kernel/uevent_elper(kernel/sysctl.c)来修改成指定的程序。在新 OpenWRT 中，并不使用 user helper 指定程序来处理 uevent(/sbin/hotplug 不存在，在以前版本中存在)，而是通过PF_NETLINK套接字来获取来自内核空间的 uevent 。 用户空间监听uevent 在 procd/plug/hotplug.c 中，创建一个 PF_NETLINK 套接字来监听内核 netlink_broadcast_fitered() 发出的 uevent 。收到uevent 之后，在根据 /etc/hotplug.json 里的描述，定位到对应的执行函数来处理.通常情况下， /etc/hotplug.json 会调用 /sbin/hotplug-call 来处理 uevent ，它根据 uevent 的 $SUBSYSTEM 变量来分别调用 /etc/hotplug.d 下不同目录中的脚本。 /etc/preinit脚本大致内容如下，先调用另外的shell脚本，获取函数定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 . /lib/functions.sh . /lib/functions/preinit.sh . /lib/functions/system.sh # 初始化hook链 boot_hook_init preinit_essential boot_hook_init preinit_main boot_hook_init failsafe boot_hook_init initramfs # 依次执行/lib/preinit目录中的脚本，将函数调用添加到hook链中 for pi_source_file in /lib/preinit/*; do . $pi_source_file done # 执行preinit_essential注册的hook链的所有函数 boot_run_hook preinit_essential # 执行preinit_main注册的hook链的所有函数 boot_run_hook preinit_main /etc/preinit脚本执行完成后，调用spawn_procd,spawn_procd会调用 execvp()执行 /sbin/procd进程\nprocd/initd/preinit.c\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spawn_procd(struct uloop_process *proc, int ret) { char *wdt_fd = watchdog_fd(); char *argv[] = { \u0026#34;/sbin/procd\u0026#34;, NULL}; char dbg[2]; if (plugd_proc.pid \u0026gt; 0) kill(plugd_proc.pid, SIGKILL); unsetenv(\u0026#34;PREINIT\u0026#34;); unlink(\u0026#34;/tmp/.preinit\u0026#34;); check_sysupgrade(); DEBUG(2, \u0026#34;Exec to real procd now\\n\u0026#34;); if (wdt_fd) setenv(\u0026#34;WDTFD\u0026#34;, wdt_fd, 1); check_dbglvl(); if (debug \u0026gt; 0) { snprintf(dbg, 2, \u0026#34;%d\u0026#34;, debug); setenv(\u0026#34;DBGLVL\u0026#34;, dbg, 1); } execvp(argv[0], argv); } procd/procd.c中\n1 2 3 4 5 6 7 8 9 10 setsid(); uloop_init(); procd_signal(); procd_udebug_set_enabled(true); if (getpid() != 1) procd_connect_ubus(); else procd_state_next(); uloop_run(); uloop_done(); 此时getpid()等于1，所以调用procd_state_next，进入到状态机处理中。\nprocd_state不断迁移，包括STATE_EARLY，STATE_UBUS，STATE_INIT等。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [ 3.161338@3] init: Console is alive [ 3.173921@3] init: Ping [ 3.184207@3] init: Ping [ 3.192558@1] kmodloader: loading kernel modules from /etc/modules-boot.d/* [ 3.194447@3] init: Ping [ 3.196209@1] kmodloader: done loading kernel modules from /etc/modules-boot.d/* [ 3.204716@3] init: Ping [ 3.206180@3] init: - preinit - [ 3.208671@3] init: Launched preinit instance, pid=1308 [ 3.302967@3] init: Exec to real procd now [ 3.308865@3] procd: - early - [ 3.524654@2] procd: Finished udevtrigger [ 4.024929@2] procd: Coldplug complete [ 4.028061@2] procd: - ubus - [ 4.029198@2] procd: Create service ubus [ 4.030829@2] procd: Create instance ubus::instance1 [ 4.032109@2] procd: Started instance ubus::instance1[1554] [ 4.098895@2] procd: Connected to ubus, id=459ede6c [ 4.099092@2] procd: - init - [ 4.102474@2] procd: Launched new askconsole action, pid=1555 [ 4.104142@2] procd: Launched new askfirst action, pid=1556 以STATE_INIT为例，执行procd_inittab_run(\u0026ldquo;xxx\u0026rdquo;)会调用对应handlers的callback，对应所有的init_action是在procd_inittab()中添加的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 case STATE_INIT: LOG(\u0026#34;- init -\\n\u0026#34;); procd_inittab(); procd_inittab_run(\u0026#34;respawn\u0026#34;); procd_inittab_run(\u0026#34;askconsole\u0026#34;); procd_inittab_run(\u0026#34;askfirst\u0026#34;); procd_inittab_run(\u0026#34;sysinit\u0026#34;); static struct init_handler handlers[] = { { .name = \u0026#34;sysinit\u0026#34;, .cb = runrc, }, { .name = \u0026#34;shutdown\u0026#34;, .cb = runrc, }, { .name = \u0026#34;askfirst\u0026#34;, .cb = askfirst, .multi = 1, }, { .name = \u0026#34;askconsole\u0026#34;, .cb = askconsole, .multi = 1, }, { .name = \u0026#34;respawn\u0026#34;, .cb = rcrespawn, .multi = 1, } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 static const char *tab = \u0026#34;/etc/inittab\u0026#34;; static char *ask = \u0026#34;/sbin/askfirst\u0026#34;; static int add_action(struct init_action *a, const char *name) { int i; for (i = 0; i \u0026lt; ARRAY_SIZE(handlers); i++) if (!strcmp(handlers[i].name, name)) { a-\u0026gt;handler = \u0026amp;handlers[i]; list_add_tail(\u0026amp;a-\u0026gt;list, \u0026amp;actions); return 0; } ERROR(\u0026#34;Unknown init handler %s\\n\u0026#34;, name); return -1; } void procd_inittab(void) { #define LINE_LEN 128 FILE *fp = fopen(tab, \u0026#34;r\u0026#34;); struct init_action *a; regex_t pat_inittab; regmatch_t matches[5]; char *line; if (!fp) { ERROR(\u0026#34;Failed to open %s: %m\\n\u0026#34;, tab); return; } regcomp(\u0026amp;pat_inittab, \u0026#34;([a-zA-Z0-9]*):([a-zA-Z0-9]*):([a-zA-Z0-9]*):(.*)\u0026#34;, REG_EXTENDED); line = malloc(LINE_LEN); a = calloc(1, sizeof(struct init_action)); while (fgets(line, LINE_LEN, fp)) { char *tags[TAG_PROCESS + 1]; char *tok; int i; int len = strlen(line); while (isspace(line[len - 1])) len--; line[len] = 0; if (*line == \u0026#39;#\u0026#39;) continue; if (regexec(\u0026amp;pat_inittab, line, 5, matches, 0)) continue; DEBUG(4, \u0026#34;Parsing inittab - %s\\n\u0026#34;, line); for (i = TAG_ID; i \u0026lt;= TAG_PROCESS; i++) { line[matches[i].rm_eo] = \u0026#39;\\0\u0026#39;; tags[i] = \u0026amp;line[matches[i + 1].rm_so]; }; tok = strtok(tags[TAG_PROCESS], \u0026#34; \u0026#34;); for (i = 0; i \u0026lt; (MAX_ARGS - 1) \u0026amp;\u0026amp; tok; i++) { a-\u0026gt;argv[i] = tok; tok = strtok(NULL, \u0026#34; \u0026#34;); } a-\u0026gt;argv[i] = NULL; a-\u0026gt;id = tags[TAG_ID]; a-\u0026gt;line = line; if (add_action(a, tags[TAG_ACTION])) continue; line = malloc(LINE_LEN); a = calloc(1, sizeof(struct init_action)); } fclose(fp); free(line); free(a); regfree(\u0026amp;pat_inittab); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void procd_inittab_run(const char *handler) { struct init_action *a; list_for_each_entry(a, \u0026amp;actions, list) { if (!strcmp(a-\u0026gt;handler-\u0026gt;name, handler)) { if (a-\u0026gt;handler-\u0026gt;multi) { a-\u0026gt;handler-\u0026gt;cb(a); continue; } a-\u0026gt;handler-\u0026gt;cb(a); break; } } } /etc/inittab\n1 2 3 ::sysinit:/etc/init.d/rcS S boot ::shutdown:/etc/init.d/rcS K shutdown ::askconsole:/usr/libexec/login.sh 这里来看runrc的实现，代码位于inittab.c\n1 2 3 4 5 6 7 8 9 10 11 static void runrc(struct init_action *a) { if (!a-\u0026gt;argv[1] || !a-\u0026gt;argv[2]) { ERROR(\u0026#34;valid format is rcS \u0026lt;S|K\u0026gt; \u0026lt;param\u0026gt;\\n\u0026#34;); return; } /* proceed even if no init or shutdown scripts run */ if (rcS(a-\u0026gt;argv[1], a-\u0026gt;argv[2], rcdone)) rcdone(NULL); } rcS.c\n1 2 3 4 5 6 7 8 int rcS(char *pattern, char *param, void (*q_empty)(struct runqueue *)) { runqueue_init(\u0026amp;q); q.empty_cb = q_empty; q.max_running_tasks = 1; return _rc(\u0026amp;q, \u0026#34;/etc/rc.d\u0026#34;, pattern, \u0026#34;*\u0026#34;, param); } 执行/etc/rc.d目录下S开头的脚本\n","date":"2025-07-30T18:28:23+08:00","permalink":"https://charles-7777.github.io/p/openwrt-procd%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/","title":"openwrt procd启动流程分析"},{"content":"OverlayFS 简介 OverlayFS (Overlay Filesystem) 是一种联合文件系统（Union Filesystem），它允许用户将一个文件系统（通常是可写的）“覆盖”在另一个文件系统（通常是只读的）之上。对于用户来说，这两个文件系统看起来像是一个合并后的单一目录树。\nOverlayFS 在 Linux 内核 3.18 中被合并进主线，因其设计简洁、性能高效而被广泛应用于容器技术（如 Docker）和嵌入式系统（如 OpenWrt）。\n核心概念 OverlayFS 主要涉及四个目录层级：\nLower Dir (底层目录): 通常是只读的。 在 OpenWrt 中，这通常对应于压缩的只读固件部分（SquashFS）。 可以有多个 Lower 层。\nUpper Dir (上层目录): 是可写的。 所有对文件系统的修改（新建、修改、删除）都会记录在这里。 在 OpenWrt 中，这通常对应于 JFFS2 或 UBIFS 分区。\nWork Dir (工作目录): 一个对用户不可见的空目录，OverlayFS 使用它来准备文件操作（如原子性保证）。 必须与 Upper Dir 在同一个文件系统下。\nMerged Dir (合并目录): 这是用户最终看到的挂载点。 它展示了 Lower 和 Upper 合并后的视图。\n工作原理 读取 (Read): 如果文件在 Upper 层存在，直接读取 Upper 层的文件。 如果文件仅在 Lower 层存在，读取 Lower 层的文件。 如果两层都有，Upper 层的文件会“遮盖”住 Lower 层的文件。\n写入 (Write):\n修改文件: 当尝试修改一个位于 Lower 层的文件时，OverlayFS 会触发 Copy-Up 操作，将文件从 Lower 层复制到 Upper 层，然后对 Upper 层的文件进行修改。 新建文件: 直接在 Upper 层创建。 删除 (Delete): 如果删除的是 Upper 层的文件，直接删除。 如果删除的是 Lower 层的文件，OverlayFS 会在 Upper 层创建一个特殊的 Whiteout 文件（通常是字符设备 0/0），用于标记该文件已被“遮盖”（即删除），从而在 Merged 视图中不可见。\nOpenWrt 中的 OverlayFS 应用 OpenWrt 是 OverlayFS 最经典的落地场景之一。它利用 OverlayFS 完美解决了嵌入式设备存储空间有限且需要系统恢复能力的矛盾。\nOpenWrt 的文件系统架构 OpenWrt 的固件通常包含两个主要部分：\n/rom (Read-Only): 这是一个 SquashFS 文件系统，包含了系统启动所需的所有基础文件、内核模块和默认配置。 它是高度压缩的，且在运行时是只读的。这保证了系统核心文件的安全性，无论用户如何误操作，都可以恢复出厂设置。\n/overlay (Read-Write): 这是一个 JFFS2 (对于 NOR Flash) 或 UBIFS (对于 NAND Flash) 文件系统，或者是 ext4 (对于 SD 卡/硬盘)。 它用于存储用户的配置更改、安装的软件包等。\n挂载机制 当 OpenWrt 启动时，它会执行以下逻辑（简化版）：\n挂载 SquashFS 分区到 /rom。 挂载可写分区（如 JFFS2）到 /overlay。 使用 OverlayFS 将 /overlay（Upper）覆盖在 /rom（Lower）之上，并将结果挂载到根目录 /。 挂载命令示例:\n1 mount -t overlay overlay -o lowerdir=/rom,upperdir=/overlay/upper,workdir=/overlay/work / (注：OpenWrt 的实际挂载过程由 mount_root 和 preinit 脚本处理，细节可能因版本而异，早期版本使用 mini_fo，现代版本均使用 overlayfs)*\n实际操作与管理 查看挂载状态 在 OpenWrt 终端中输入 df -h 或 mount 可以看到 OverlayFS 的结构：\n1 2 3 4 5 6 root@OpenWrt:~# df -h Filesystem Size Used Available Use% Mounted on /dev/root 2.5M 2.5M 0 100% /rom tmpfs 61.8M 168.0K 61.6M 0% /tmp /dev/mtdblock3 5.0M 424.0K 4.6M 8% /overlay overlayfs:/overlay 5.0M 424.0K 4.6M 8% / 可以看到 / 的类型是 overlayfs:/overlay，它的大小等于 /overlay 分区的大小。\n恢复出厂设置 (Firstboot) 由于 OverlayFS 的特性，恢复出厂设置变得非常简单：只需要清空 /overlay 分区即可。\n当 /overlay 被清空后，Upper 层没有了任何文件，系统重启后，OverlayFS 呈现的完全是 Lower 层（即 /rom）的内容，系统就回到了初始状态。\n命令：\n1 2 firstboot -y reboot 扩容 (Extroot) OpenWrt 设备的内置 Flash 通常很小（如 16MB）。利用 OverlayFS，我们可以轻松实现扩容（Extroot）：\n插入一个 USB U盘。 将 U盘格式化为 ext4。 将 U盘挂载为 /overlay。 这样，系统的可用空间就变成了 U盘的大小，可以安装大量的软件包。 常见问题 空间不足: 如果 /overlay 满了，系统可能无法保存配置或安装软件。此时删除文件实际上是在 Upper 层操作。 文件被遮挡: 如果你在 /rom 中修改了源码重新编译固件，但刷机后发现文件没变，可能是因为 /overlay 中存在旧的修改版本遮挡了新的 /rom 文件。刷机时通常建议选择“不保留配置”来清空 /overlay。\n总结 OverlayFS 是 OpenWrt 灵活性的基石。它通过分层机制，既保证了系统底层的稳定性（只读的 /rom），又提供了用户所需的灵活性（可写的 /overlay），并且实现了极低成本的“恢复出厂设置”功能。理解 OverlayFS 对于深入玩转 OpenWrt（如定制固件、扩容、调试）至关重要。\n","date":"2025-07-29T09:52:54+08:00","permalink":"https://charles-7777.github.io/p/overlayfs/","title":"OverlayFS"},{"content":"LXC (Linux 容器) 工作原理详解 LXC (Linux Containers) 是一种操作系统级别的虚拟化技术，它允许在单个 Linux 内核上运行多个隔离的 Linux 系统（容器）。与虚拟机（VM）不同，LXC 不需要模拟硬件，因此它非常轻量级且启动速度快。\nLXC 的核心是利用 Linux 内核的两个关键特性：命名空间 (Namespaces) 和 控制组 (Cgroups)。\nNamespaces：负责隔离，确保一个容器中的进程看不到或影响到另一个容器或宿主机的进程、网络、文件系统等。 Cgroups：负责资源限制和审计，确保每个容器只能使用分配给它的 CPU、内存、I/O 等资源。 本文将重点详细介绍几个关键的命名空间。\n1. PID 命名空间 (PID Namespace) PID (Process ID) 命名空间用于隔离进程 ID。\n原理 隔离进程树：每个 PID 命名空间都有一套独立的进程 ID，从 1 开始。 容器的 init 进程：在一个新的 PID 命名空间中创建的第一个进程会成为该空间的 \u0026ldquo;init\u0026rdquo; 进程，其 PID 为 1。这个进程负责管理容器内的所有其他进程（例如，处理孤儿进程）。如果这个 PID 为 1 的进程终止，内核将终止该命名空间中的所有其他进程。 内外 PID 映射：容器内的进程在容器内部有自己的 PID（例如，PID 1, 2, 3\u0026hellip;），同时在宿主机上也有一个全局唯一的 PID。这意味着从宿主机看，所有容器的进程都是普通的进程，只是被 PID 命名空间隔离开来。 示例 假设我们在宿主机上启动一个 LXC 容器，并在容器内运行 bash。\n容器内视角:\n1 2 3 4 5 6 # 在容器内执行 ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.0 2384 1596 ? Ss 10:00 0:00 /sbin/init root 15 0.0 0.0 4372 3480 pts/0 Ss 10:01 0:00 bash root 25 0.0 0.0 5924 1788 pts/0 R+ 10:02 0:00 ps aux 在这里，init 进程的 PID 是 1，bash 的 PID 是 15。\n宿主机视角:\n1 2 3 4 # 在宿主机执行 ps aux | grep bash # 输出可能像这样 root 12345 0.0 0.0 4372 3480 pts/0 Ss 10:01 0:00 bash 在宿主机上，同一个 bash 进程的 PID 可能是 12345。这种隔离使得容器内的进程管理与宿主机完全分离。\n2. 网络命名空间 (Network Namespace) 网络命名空间为每个容器提供了一个完全独立的网络协议栈。\n原理 独立网络栈：每个网络命名空间都有自己独立的网络设备（如 lo, eth0）、IP 地址、路由表、iptables 防火墙规则、端口号等。一个容器默认无法访问另一个容器或宿主机的网络。 veth 设备对：为了让容器能与外部通信，LXC 通常使用 veth (Virtual Ethernet) 设备对。veth 设备总是成对出现，像一根虚拟网线。数据从一端进入，会从另一端出来。 连接过程: 创建一对 veth 设备，例如 veth_host 和 veth_container。 将 veth_host 留在宿主机的网络命名空间中。 将 veth_container \u0026ldquo;移动\u0026rdquo; 到容器的网络命名空间中，并将其重命名为 eth0。 在宿主机上，通常会创建一个网桥（例如 lxcbr0），并将 veth_host 端连接到这个网桥上。 为容器内的 eth0 分配 IP 地址。 数据流: 容器内 eth0 发出的网络包 -\u0026gt; 通过 veth 对到达宿主机的 veth_host -\u0026gt; 进入宿主机的网桥 lxcbr0 -\u0026gt; 通过宿主机的物理网卡和路由规则与外部网络通信。 这种结构使得每个容器都像一台独立的机器连接到了一个虚拟交换机（网桥）上，实现了网络隔离和互联。\n3. 文件系统命名空间 (Mount Namespace) 文件系统（挂载）命名空间允许每个容器拥有自己独立的文件系统视图，尤其是独立的根目录 (/)。\n原理 隔离挂载点：每个挂载命名空间维护着一个独立的挂载点列表。在一个命名空间中的 mount() 和 umount() 操作不会影响到其他命名空间。 独立的根文件系统 (rootfs)：LXC 利用这个特性为每个容器创建一个独立的根文件系统。容器进程看到的文件系统层次结构与宿主机完全不同。 实现方式: 准备 rootfs：首先，需要为容器准备一个目录，其中包含一个完整的 Linux 系统所需的文件和目录（如 /bin, /etc, /lib, /usr 等）。这通常通过复制一个最小化的系统模板来完成。 pivot_root 系统调用：为了将容器的根目录切换到准备好的 rootfs，LXC 使用 pivot_root 系统调用（或者在某些情况下使用 chroot，但 pivot_root 更强大、更安全）。pivot_root 会将当前进程的根文件系统切换到一个新的挂载点，同时将旧的根文件系统挂载到新根下的一个指定目录中，之后可以将其卸载。 隔离挂载：在容器启动后，它可以在自己的文件系统命名空间内自由地挂载其他设备或文件系统（如 proc, sysfs, tmpfs），而这些挂载对宿主机是不可见的。 通过这种方式，容器内的进程被\u0026quot;囚禁\u0026quot;在其自己的文件系统视图中，无法访问或修改宿主机的文件系统（除非特别配置了绑定挂载 bind mount）。\n其他命名空间 除了以上三个，LXC 还使用了其他命名空间来实现全方位隔离：\nUTS Namespace: 隔离主机名和域名。 IPC Namespace: 隔离进程间通信资源，如 System V IPC 和 POSIX 消息队列。 User Namespace: 隔离用户和组 ID。允许容器内的 root 用户（UID 0）映射为宿主机上的一个非特权用户，极大地提升了安全性。 Cgroup Namespace: 隔离控制组视图。 总结 LXC 通过精巧地组合使用 Linux 内核的 Namespaces 和 Cgroups 特性，为用户提供了一个轻量级、高效且隔离性良好的容器环境。PID、网络和文件系统命名空间是实现这种隔离的基础，它们分别创建了独立的进程树、网络协议栈和文件系统视图，使得容器内的环境看起来就像一个独立的操作系统。\n","date":"2025-07-28T20:00:08+08:00","permalink":"https://charles-7777.github.io/p/lxc%E8%AF%A6%E8%A7%A3/","title":"LXC详解"},{"content":"对称加密 加密前的原始数据，叫做 原文（ original text ），或者 明文（ plain text ）； 加密后的不规则（ scrambled ）数据，通常叫做 密文（ cipher text ）； 加密所用的密码，通常叫做 密钥（ secret key ）； 对称加密（symmetric encryption）算法最大的特点是，它只有一把密钥，加密和解密过程用的都是同一把密钥，这也符合大众对加密算法的认知，用密码对数据进行加密之后，必须用同一个密码才能将数据解密出来\nAES ，高级加密标准，新一代加密算法标准，速度快，安全级别高； DES ，数据加密标准，速度较快，适用于加密大量数据，但安全性较弱； Blowfish ，使用变长密钥，运行速度很快，非专利算法，没有使用限制； etc 非对称加密 从前有一个黑帮，老大和手下们之间的通信必须加密确保安全。于此同时，老大希望手下发给他的信息，不能被其他手下知晓。若采用对称加密算法，只能给每个手下都分配一个独立的密钥，但老大觉得太麻烦了。该怎么办呢？\n黑帮老大希望他只维护一份密钥，就能达到这样的效果，有办法做到吗？\n可以用非对称加密（asymmetric encryption）\n非对称加密，顾明思议，在加密和解密环节用的密钥是不同的。\n非对称加密算法需要两把不同的密钥，这两把密钥组成一对：\n公钥（ public key ），公钥用来对数据进行 加密 ； 私钥（ private key ），也称为 密钥（ secret key ），用来对数据进行 解密 ； 公钥和私钥总是成对出现，用公钥加密后得到的密文，必须用对应的私钥才能解密； 这套加密机制完美解决了黑帮老大的难题，他只需要生成一对密钥：公钥分发给手下们，他们先用公钥加密信息，再发老大；老大接到密文，就用自己保管的私钥来解密；手下们就算拿到别人发的密文也解不开，因为私钥只有他们老大才有。\n数学原理 公钥和私钥的加密机制看起来非常不可思议，这一切其实来一个神奇的数学原理。\n我们来做一个数字游戏，您随便写下一个整数 m （1\u0026lt;m\u0026lt;7387 ），然后计算m ^3 mod 7387 并把结果告诉我，我就知道您写下的整数 m 是什么\n1 2 3 4 5 6 7 8 9 10 11 # 520 \u0026gt;\u0026gt;\u0026gt; 520 ** 3 % 7387 3842 \u0026gt;\u0026gt;\u0026gt; 3842 ** 4811 % 7387 520 # 1314 \u0026gt;\u0026gt;\u0026gt; 1314 ** 3 % 7387 7382 \u0026gt;\u0026gt;\u0026gt; 7382 ** 4811 % 7387 1314 公钥参数 3 和 7387 ，私钥参数 4811 和 7387 又是怎么生成的呢？ 第一步，随机选择两个质数 p 和 q ： p = 83 q = 89 第二步，计算 p 和 q 的乘积 n ： n = p * q = 7387 第三步，计算 n 的欧拉函数 ，记为 phi ： φ(n) = (p-1) * (q-1) = 7216 第四步，随机选择一个整数 e ，满足1\u0026lt;e\u0026lt;φ(n)，且 e 和φ(n)互质： 选一个质数 e ，使得不能被 e 整除即可。 第五步，计算 e 对φ(n)的模反元素 d ，即找到一个数 d 使得 ed 除以φ(n)的余数为 1 ： ed ≡ 1 mod(φ ( n )) \u0026lt;=\u0026gt; ed = kφ(n) + 1 可以找到一个d :4811\n安全性分析 那么，有无可能在已知n和e的情况下，推导出d？\n1 2 3 ed≡1 (mod φ(n))。只有知道e和φ(n)，才能算出d。 φ(n)=(p-1)(q-1)。只有知道p和q，才能算出φ(n)。 n=pq。只有将n因数分解，才能算出p和q。 大整数的因数分解，是一件非常困难的事情\n目前，能够被破解的 n 最大位数是 768 位(这里提到的位是指二进制位)，因此有人开始质疑 1024 位密钥的安全性。现在推荐的密钥长度至少要 2048 位，只要长度足够，安全性完全不用担心\n应用场景 加密 公钥加密私钥解密是非对称加密算法最典型的应用场景，特别适用于密钥需要公开的场景，比如 传输层安全协议 TLS ，它为通讯双方提供可靠的加密连接\n如果没有非对称加密算法，TLS 将无法实现。因为对称加密算法要求双方使用同一密钥，加密连接建立之前，只能明文协商密钥。试想浏览器想跟服务器建立安全连接，无论是它选定密钥然后发给服务器，还是服务器选定密钥发给它，只要密钥经过明文传输，加密就失去意义。\n有了非对称加密算法，服务器可以生成一对密钥，私钥自己保管，公钥可以公开。当浏览器请求建立加密连接时，服务器可以将公钥发给浏览器，因为公钥是可以公开的。浏览器将敏感信息用公钥加密后再发给浏览器，只有掌握私钥的服务器才能解密，他人便无法知晓。\n同理，服务器想发敏感信息给客户端，必须由客户端生成的公钥加密。换句话讲，每对密钥解决一个方向的加密问题，通讯双方都需要生成自己的密钥对，负责加密对方发来的数据。\n由于非对称加密算法运算复杂，加密效率不高，通常只是用来加密少量的关键信息，比如协商密钥。回到 TLS 这个例子，其实可以借助非对称加密算法协商密钥，从而直接使用更高效的对称加密算法来加密数据：\n服务器生成公钥和私钥 对； 客户端（浏览器）连接上来后，服务器将公钥发给客户端； 客户端随机生成一个用于对称加密（ AES ）的密钥； 客户端用公钥对生成的密钥进行加密，然后后发给服务器； 服务器收到客户端用公钥加密的密钥后，用自己的私钥解密，至此密钥协商完毕； 由于私钥只有服务器才有，因此第三方无法知晓客户端选定的密钥是啥； 此后通信双方采用对称加密，以该密钥加密数据； 签名 实际上，私钥也可以用来加密数据，加密后的密文只有公钥才能解密。尽管如此，由于公钥是公开的，因此这个机制不能来加密数据，但可以对用来对数据进行签名防伪。\n数字证书(certificate) 信息摘要（ digest ），数据经过哈希算法得到的一串哈希值，代表数据的特征，也称为数据指纹； 摘要算法 ，可以把任意长度的数据，映射成一个定长的字符串（哈希值）； 由于哈希冲突的存在，两份不同的数据，有可能算出相同的摘要值； 摘要算法无法用于数据加密，通常用来校验数据完整性，即 数据防伪 ； 常见的摘要算法有：MD5 、SHA1 、SHA256 、SHA512 。 数字签名（ signature ），摘要由私钥加密后，得到的摘要密文就是数字签名； 签名，由数据发送方生成，这是一个 加密 过程（使用私钥）； 验签，由数据接收方校验，这是一个 解密 过程（使用公钥）； 数字签名只能由私钥生成，因此第三方无法伪造； 在介绍密钥协商时，我们提到服务器先将公钥发给客户端，用公钥保护对称加密密钥，确保通信内容不会被第三方获悉。但如果客户端连接的服务器是假的呢？如果用户对假网站信以为真，输入了账号密码，那么这些敏感信息都会被假网站窃取！\n上节我们也讨论了数字签名，通过它可以实现数据防伪。那么，我们是不是可以利用这项技术来甄别仿冒站点呢\n权威机构生成一对密钥，并提供站点认证审核和数字签名颁发服务； 站点管理员将站点信息，包括域名、运营单位、公钥等信息发给权威机构审核； 权威机构对提交上来的站点信息进行审核，审核通过则用私钥签名后返回给站点管理员； 客户端（浏览器）连接站点服务器，服务器将站点信息以及对应的数字签名发给客户端； 客户端用权威机构提供的公钥来校验数字签名，即可判断站点信息的真伪性； 签名验证通过，客户端从站点信息中取出公钥，与服务端协商密钥，发起加密通信； 由于签名用的私钥只有权威机构掌握，黑客无法伪造数字签名，也就无法架设仿冒站点； 权威机构必须由可信的单位运营； 你可能会觉得，黑客直接盗用站点信息和签名不就可以伪造原站点了嘛？此言差矣！因为公钥属于站点信息\n的一 部分，也会参与签名！客户端和服务端协商密钥时，会使用这个公钥加密密钥。由于黑客不掌握站点私 钥，因此 加密连接无法建立！黑客把公钥替换成自己的吧，签名就不对，肯定会被验出来！\n证书签发实验 CA权威机构 首先，权威机构需要生成一对密钥，cakey.pem 是私钥,\n1 openssl genrsa -out cakey.pem 2048 然后，生成根证书签发申请文件（ csr 文件）：\n1 openssl req -new -key cakey.pem -out ca.csr -subj \u0026#39;/C=CN/ST=Guangdong/L=Guangzhou/O=coding-fans/OU=CA/CN=ca.fasionchan.com\u0026#39; 证书申请文件包含权威机构的信息，包括机构信息(Subject )和公钥(Public Key 部分)可以用下面命令查看：\n1 openssl req -in ca.csr -text -noout 最后，自签根证书（ cer 文件 ）：\n1 openssl x509 -req -days 3650 -sha1 -extensions v3_ca -signkey cakey.pem -in ca.csr -out ca.cer 这一步生成的 cer 文件就是根证书文件，它的主要作用是承载权威机构公钥，以便预装在操作系统或者其他终端。它同样会包含权威机构的信息，公钥，以及对应的签名。\n商业站点（服务端） 首先，站点管理员生成一对密钥\n1 openssl genrsa -out sitekey.pem 2048 然后，生成证书签发申请文件（ csr 文件）：\n1 openssl req -new -key sitekey.pem -out site.csr -subj \u0026#39;/C=CN/ST=Guangdong/L=Guangzhou/O=fasionchan/OU=website/CN=fasionchan.co 证书申请文件包含站点信息和公钥，站点管理员将证书申请文件发给权威机构审核，\n权威机构对申请进行审核，审核通过则用自己的私钥对它进行签名，生成证书(cer 文件):\n1 openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey cakey.pem -CAserial ca.srl -CAcreateserial -in site.csr -out site.cer 证书中保存着包括公钥在内的站点信息，以及权威机构对这些信息的签名。管理员接到权威机构颁发的证书，就可以部署网站了\n浏览器（客户端） 客户端浏览器访问站点，服务端会将其证书发给客户端。客户端先对证书签名进行验证，步骤如下：\n重新对证书中的站点信息计算 摘要值 ； 用公钥对证书中的签名进行解密，得到证书的原始摘要值； 公钥通常由根证书提供，根证书通常预装在系统里； 对比两个摘要值看是否一致； 调用 openssl 工具，一行命令即可完成签名验证\n1 openssl verify -CAfile ca.cer site.cer 总结 数字证书是支撑互联网身份认证的重要技术手段，可以简单理解成经过 CA 权威结构签名认证过的站点信息。由于经过 CA 签名，第三方无法通过伪造手段冒充身份。\n证书由站点信息和 CA 签名组成，站点信息包含站点公钥，公钥用于协商对称加密密钥； 证书由 CA 权威机构审核签发，签名用的是 CA 的私钥； CA 公钥通常以根证书形式预装在系统内，客户端通过它来验证证书签名； 有了数字签名，黑客无法对证书进行篡改，也无法伪造证书，因此无法部署仿冒站点； 若只窃取原站点证书，不做篡改，客户端使用真实站点的公钥，而黑客无法掌握站点私钥，因此加密连接无法建立； 如果篡改原站点证书，换上自己的公钥，但因为没有 CA 私钥无法生成合法签名，也会被识别出来； 转载自数字证书身份认证原理与签发步骤详解 | 小菜学网络\n","date":"2025-07-28T16:36:08+08:00","permalink":"https://charles-7777.github.io/p/%E8%AF%81%E4%B9%A6%E8%AF%A6%E8%A7%A3/","title":"证书详解"},{"content":"RESTful API 一、RESTful API概述 1.1 定义 REST（Representational State Transfer）即表现层状态转移，是一种针对网络应用的设计风格，主要用于设计分布式超媒体系统。RESTful API 则是遵循 REST 架构原则的应用程序接口，允许客户端和服务器通过 HTTP 协议进行交互。它采用资源定位的思维模式，将所有的操作都视为对资源的操作，以使系统更加简洁、易于理解和扩展。\n1.2 核心概念 资源（Resource）：RESTful API 中的每一个对象、实体或数据都被抽象为一个资源。例如，用户、文章等都可以作为资源。每个资源都通过一个唯一的 URI（统一资源标识符）标识。比如，/users/123 表示 id 为 123 的用户资源，/posts/456 表示 id 为 456 的文章资源。 URI（统一资源标识符）：用于标识资源的地址，通常使用 URL（统一资源定位符）作为 URI。 HTTP 动作（HTTP Methods）：依赖于 HTTP 协议的常见方法来对资源进行操作，每个 HTTP 方法对应不同的操作： GET：获取服务器上的资源。 POST：在服务器上创建新的资源。 PUT：更新服务器上的资源。 DELETE：删除服务器上的资源。 无状态（Statelessness）：每个请求都应该是独立的，服务器不会在请求之间保存客户端的状态。每次请求都必须包含理解请求所必需的信息。 表现层状态转移（Representational State Transfer）：资源的表现形式可以是 JSON、XML、HTML 等格式，通常 RESTful API 使用 JSON 作为数据交换格式，因为它轻量且易于解析。客户端通过接收资源的表现形式（如 JSON、XML）来感知资源的变化，从而实现状态的“转移”。 二、RESTful API的特点和优势 2.1 特点 资源导向：所有内容都被抽象为资源（如 User、Order、Article 等），每个资源都有一个唯一的标识符（URI）。例如，/users 表示用户资源，/users/1 表示 ID 为 1 的用户。 使用标准协议：基于 HTTP 协议，直接使用其方法（GET、POST 等）来操作资源。 无状态通信：每次请求都包含所有上下文，不依赖服务器保存状态。这使得服务器可以更加容易地进行扩展和负载均衡，因为每个请求都是独立的，不需要考虑之前的请求状态。 统一接口：接口风格统一、易于理解和使用。通过标准的 HTTP 方法（GET、POST、PUT、DELETE 等）和资源标识符（URI）访问资源，简化了系统的整体架构，提升了互操作性。 可缓存：客户端可根据响应头对资源进行缓存，提高性能。利用 HTTP 协议的缓存机制，允许中间件或客户端缓存响应结果，减少不必要的网络请求，从而提升响应速度和减轻服务器负担。 分层架构：客户端无需知道请求最终由谁处理（例如中间层、负载均衡等）。允许通过中间层（如代理服务器、网关）来处理请求，每层只需与相邻层通信，增强了系统的安全性、可扩展性和灵活性。 按需代码（可选）：客户端可以从服务器下载代码或脚本，以扩展其功能，增加了系统的灵活性和可扩展性。 2.2 优势 简洁易懂：使用 HTTP 协议的标准方法和 URI，可以让 API 的设计和使用变得简单。通过 URL 和 HTTP 动词的组合，可以明确地表示对特定资源的操作意图。 灵活性：资源可以有不同的表示形式（JSON、XML 等），同时 HTTP 方法明确区分不同的操作。客户端可以根据自身需求选择合适的数据格式。 扩展性强：通过一致的接口设计，可以很容易地扩展和维护 API。随着应用程序的发展，可以轻松地添加新的资源和操作，而不会影响现有的 API 结构。 无状态性：简化了服务器端的设计，增强了系统的可扩展性。服务器不需要保存客户端的状态信息，使得服务器可以更容易地进行扩展和负载均衡。 跨平台：基于 HTTP 协议，可以被任何支持 HTTP 的客户端调用，适用于各种应用场景，特别是需要跨平台和跨语言交互的系统。 三、RESTful API的设计原则 3.1 基于资源 将网络上的每个实体或概念视为唯一资源是一个关键原则。通过 URL 来表示这些资源，使得资源的定位更加清晰和直观。例如，一个博客文章可以被表示为 /articles/123，其中 “articles” 表示文章资源的集合，“123” 是具体某一篇文章的唯一标识符。这种方式使得开发者和用户都能够轻松地理解和访问特定的资源。以电商平台为例，商品可以表示为 /products，每个具体的商品可以通过 /products/{productId} 来访问，其中 {productId} 是商品的唯一标识。\n3.2 统一接口 资源标识（Resource Identification）：每个资源都应有唯一的 URL。例如，用户资源可以通过 /users/{id} 进行标识，其中 {id} 是用户的具体标识。 资源操作（Resource Manipulation Through Representations）：通过表示（representation）来操作资源，而不是直接操作资源本身。使用标准的 HTTP 方法（GET、POST、PUT、DELETE 等）来操作资源。 自描述消息（Self - descriptive Messages）：响应消息应包含足够的信息以便客户端无需额外文档即可理解。每个请求和响应都包含足够的信息，使得客户端能够理解如何处理它们。 无状态（Stateless）：服务器不保存客户端的上下文信息，每次请求都是独立的。每个请求都必须包含足够的信息，使服务器能够理解和处理该请求，而不依赖于之前的请求。 3.3 使用标准的 HTTP 方法 主要使用 HTTP 方法来定义对资源的操作，常见的 HTTP 方法及其作用如下：\nHTTP 方法 操作 幂等性 安全性 示例 GET 获取资源 ✅ ✅ GET /users 查看所有用户信息；GET /users/id 查看该 id 的用户信息 POST 创建资源 ❌ ❌ POST /users 创建用户，可在 DATA 处带需要的参数 PUT 更新或替换资源 ✅ ❌ PUT /users/id?name='张三'\u0026amp;age=20 修改该 id 的用户信息（name 和 age） PATCH 部分更新资源 ❌ ❌ 对资源进行部分属性的更新 DELETE 删除资源 ✅ ❌ DELETE /users/id 删除该 id 的用户信息 HEAD 获取资源的元数据 ✅ ✅ OPTIONS 获取信息，关于资源的哪些属性是客户端可以改变的 ✅ ✅ 3.4 无状态通信 每个请求必须包含服务器处理该请求所需的所有信息，服务器不依赖之前的请求上下文。这使得服务器可以更加容易地进行扩展和负载均衡，因为每个请求都是独立的，不需要考虑之前的请求状态。例如，当客户端发送多个请求时，服务器不需要记住之前的请求内容，只需要根据当前请求的信息进行处理。\n3.5 返回适当的状态码 API 应返回适当的 HTTP 状态码，准确反映请求结果。常用的状态码如下：\n状态码 含义 说明 200 OK 服务器成功返回用户请求的数据，该操作是幂等的（Idempotent），通用成功。 201 Created 资源已创建，通常用于 POST 请求。 204 No Content 请求成功，但无返回内容，通常用于 DELETE 请求。 400 Bad Request 请求参数有误，服务器无法处理。 401 Unauthorized 认证失败，客户端需要提供身份验证。 403 Forbidden 没有权限访问资源，服务器理解请求，但拒绝执行，通常由于权限问题。 404 Not Found 请求的资源不存在。 500 Internal Server Error 服务器内部错误。 3.6 过滤信息 如果记录数量很多，服务器不可能都将它们返回给用户。API 应该提供参数，过滤返回结果。常见的参数如下：\n?limit=10：指定返回记录的数量。 ?offset=10：指定返回记录的开始位置。 ?page=2\u0026amp;per_page=100：指定第几页，以及每页的记录数。 ?sortby=name\u0026amp;order=asc：指定返回结果按照哪个属性排序，以及排序顺序。 ?animal_type_id=1：指定筛选条件。 3.7 支持 HATEOAS（超媒体作为应用程序状态引擎） HATEOAS 要求客户端能够通过服务器返回的超链接（URL）导航到相关资源。简而言之，HATEOAS 要求 API 的响应不仅包含资源数据，还应该包含与资源相关的操作链接，帮助客户端更好地理解如何进行下一步操作。例如，获取用户信息时，除了返回用户的详细数据外，API 还可以提供相关操作的链接：\n1 2 3 4 5 6 7 8 9 { \u0026#34;user_id\u0026#34;: 123, \u0026#34;name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;links\u0026#34;: { \u0026#34;self\u0026#34;: \u0026#34;/users/123\u0026#34;, \u0026#34;update\u0026#34;: \u0026#34;/users/123/update\u0026#34;, \u0026#34;delete\u0026#34;: \u0026#34;/users/123/delete\u0026#34; } } 3.8 数据格式 返回数据格式通常使用 JSON 或 XML，其中 JSON 因其轻量级和易读性，已成为 RESTful API 事实上的数据交换格式。统一返回 JSON 格式，包含数据、状态码和错误信息，例如：\n1 2 3 4 5 { \u0026#34;status\u0026#34;: 200, \u0026#34;data\u0026#34;: { \u0026#34;id\u0026#34;: 123, \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34; }, \u0026#34;error\u0026#34;: null } 3.9 版本控制 当 API 发生变化时，可以通过版本号来管理不同版本的 API，以保持向后兼容性。常见的版本控制策略有：\nURL 路径版本：如 /v1/resource，优点是简单、显式、易缓存；缺点是不够优雅、URL 膨胀。 请求头版本：如 X - API - Version: 1，优点是 URL 干净、松耦合；缺点是难调试、可能被代理移除。 内容协商版本：如 Accept: application/vnd.example.v1+json，优点是 RESTful、标准 HTTP 头；缺点是复杂、客户端支持不一。 查询参数版本：如 /resource?version=1，优点是简单、显式；缺点是不是 RESTful、URL 污染。 四、RESTful API的设计示例 以一个简单的博客平台为例，设计其 RESTful API：\n4.1 资源列表 用户（/users） 文章（/articles） 评论（/comments） 4.2 操作设计 操作 HTTP 方法 URL 说明 获取所有用户 GET /users 返回所有用户的列表 获取特定用户 GET /users/{id} 返回指定 ID 用户的详细信息 创建用户 POST /users 根据请求体中的数据创建新用户 更新用户信息 PUT /users/{id} 更新指定 ID 用户的信息 删除用户 DELETE /users/{id} 删除指定 ID 的用户 获取所有文章 GET /articles 返回所有文章的列表 获取特定文章 GET /articles/{id} 返回指定 ID 文章的详细信息 创建文章 POST /articles 根据请求体中的数据创建新文章 更新文章信息 PUT /articles/{id} 更新指定 ID 文章的信息 删除文章 DELETE /articles/{id} 删除指定 ID 的文章 获取特定文章的评论 GET /articles/{id}/comments 返回指定 ID 文章的所有评论 创建评论 POST /articles/{id}/comments 在指定 ID 文章下创建新评论 删除评论 DELETE /comments/{id} 删除指定 ID 的评论 五、RESTful API的实现 5.1 服务端实现（以 Node.js 为例） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 const express = require(\u0026#39;express\u0026#39;); const app = express(); app.use(express.json()); // 模拟用户数据 const users = [ { id: 1, name: \u0026#39;John Doe\u0026#39;, email: \u0026#39;john@example.com\u0026#39; }, { id: 2, name: \u0026#39;Jane Smith\u0026#39;, email: \u0026#39;jane@example.com\u0026#39; } ]; // 获取所有用户 app.get(\u0026#39;/api/v1/users\u0026#39;, (req, res) =\u0026gt; { res.status(200).json({ status: 200, data: users }); }); // 创建新用户 app.post(\u0026#39;/api/v1/users\u0026#39;, (req, res) =\u0026gt; { const newUser = req.body; users.push(newUser); res.status(201).json({ status: 201, data: newUser }); }); // 获取特定用户 app.get(\u0026#39;/api/v1/users/:id\u0026#39;, (req, res) =\u0026gt; { const userId = parseInt(req.params.id); const user = users.find(u =\u0026gt; u.id === userId); if (user) { res.status(200).json({ status: 200, data: user }); } else { res.status(404).json({ status: 404, error: \u0026#39;User not found\u0026#39; }); } }); // 更新用户信息 app.put(\u0026#39;/api/v1/users/:id\u0026#39;, (req, res) =\u0026gt; { const userId = parseInt(req.params.id); const updatedUser = req.body; const index = users.findIndex(u =\u0026gt; u.id === userId); if (index!== -1) { users[index] = updatedUser; res.status(200).json({ status: 200, data: updatedUser }); } else { res.status(404).json({ status: 404, error: \u0026#39;User not found\u0026#39; }); } }); // 删除用户 app.delete(\u0026#39;/api/v1/users/:id\u0026#39;, (req, res) =\u0026gt; { const userId = parseInt(req.params.id); const index = users.findIndex(u =\u0026gt; u.id === userId); if (index!== -1) { users.splice(index, 1); res.status(204).send(); } else { res.status(404).json({ status: 404, error: \u0026#39;User not found\u0026#39; }); } }); const port = process.env.PORT || 3000; app.listen(port, () =\u0026gt; { console.log(`Server is running on port ${port}`); }); 5.2 客户端调用（以 JavaScript Fetch 为例） 1 2 3 4 5 6 7 8 9 10 11 12 // 获取用户数据 fetch(\u0026#39;https://api.example.com/users/123\u0026#39;) .then(response =\u0026gt; response.json()) .then(data =\u0026gt; console.log(data)) .catch(error =\u0026gt; console.error(\u0026#39;Error:\u0026#39;, error)); // 提交新文章 fetch(\u0026#39;https://api.example.com/articles\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content - Type\u0026#39;: \u0026#39;application/json\u0026#39; }, body: JSON.stringify({ title: \u0026#39;Hello REST\u0026#39;, content: \u0026#39;...\u0026#39; }) }); 六、RESTful API的应用场景 6.1 Web 服务 提供 Web 服务，如社交媒体、电商网站等。前端（如 JavaScript、Angular、React 等）可以通过 RESTful API 与后端服务器进行通信，获取和更新数据。例如，一个电商网站的前端可以使用 RESTful API 从服务器获取商品列表、用户信息等，并将用户的订单信息发送到服务器进行处理。\n6.2 移动应用 移动应用通过 RESTful API 与服务器交互，获取和更新数据，实现用户登录、数据同步等功能。Android 和 iOS 应用可以使用 RESTful API 与服务器交互，提供丰富的用户体验。\n6.3 物联网（IoT） 设备通过 RESTful API 与服务器通信，实现数据采集和控制。物联网设备可以通过 RESTful API 将采集到的数据发送到服务器，同时接收服务器的控制指令。\n6.4 微服务架构 微服务之间通过 RESTful API 进行通信和协作。每个微服务都可以暴露自己的 RESTful API，其他微服务可以通过调用这些 API 来获取所需的数据或执行特定的操作。例如，一个电商系统可能由用户服务、商品服务、订单服务等多个微服务组成，这些微服务之间可以通过 RESTful API 进行数据交互和业务协作。\n6.5 企业级应用集成 企业内部的不同系统之间可以使用 RESTful API 进行集成。例如，企业的客户关系管理系统（CRM）和企业资源规划系统（ERP）可以通过 RESTful API 进行数据交换，实现信息共享和业务流程的协同。\n七、RESTful API与传统 API的对比 比较项 RESTful API 传统 API 风格 接口风格 资源导向，结构清晰 动作导向，接口混乱 动作表示 使用 HTTP 方法表示动作 接口路径中包含动词（如 /getUser） 可读性 高，可直观理解操作含义 低，需要阅读文档才能理解 维护性 易于扩展和维护 扩展性差，接口膨胀 数据传输格式 通常使用 JSON 或 XML，有明确标准 可能使用多种格式，无明确标准 状态与缓存 强调状态无关性，可利用 HTTP 缓存机制 可能依赖服务器端状态，缓存策略不一致 安全性和认证 支持各种安全性措施，如 HTTPS、认证、授权等 可能缺乏统一的安全标准 接口一致性 统一接口，易于使用 接口设计松散，不一致 资源关联性 在响应中提供相关资源的链接，具有自描述性 通常不提供自描述性 综上所述，RESTful API 以其简洁、灵活、可扩展等优点，成为现代 Web 开发中主流的 API 设计风格之一，广泛应用于各种网络应用场景中。在设计和开发 RESTful API 时，遵循其设计原则和最佳实践，能够构建出高效、易用、可维护的 API 系统。\n","date":"2025-06-26T15:22:59+08:00","permalink":"https://charles-7777.github.io/p/restful-api/","title":"RESTful API"},{"content":"创建public仓库用来存放照片 创建token token将来PicGo操作你的github仓库来上传图片时要用到，在 GitHub 账户下 Setting - Developer setting - Personal access tokens(classic) 下创建一个不过期(no expiration)的 Token，权限需要开启 repo.这个我们在用Github action 来部署博客仓库时已经创建了，用那个就好.token 在创建时只显示一次，要保存好哦！\n配置PicGo github 搜索PicGo 然后安装 图床具体参数配置 仓库名 分支名 对应于之前你创建的GitHub仓库\nToken就是前面创建的Token,被PicGo用来上传照片到长仓库\n路径可以自定义\n自定义域名格式:https://cdn.jsdelivr.net/gh/用户名/仓库名@分支名\nTypora Typora 可搭配PicGo使用\n若PicGo显示unable to verify the first certificate at TLS,无法上传，则可能是你的网络加速工具造成的某些网络加速工具可能会修改网络流量，可能与 SSL证书验证机制发生冲突。这可能导致 SSL 证书验证失败，因为服务器的证书无法正常验证，从而出现类似 \u0026ldquo;unable to verify the first certificate\u0026rdquo; 的错误。例如我之前用Watt Toolkit就出现了：\n如果Typora显示failed to fetch ；检查PicGo server 设置的监听地址是否一致（点击Typora图片设置中的验证图片上传）\njsDelivr jsDelivr是一个免费、开源的加速CDN公共服务,托管了许多大大小小的项目,可加速访问托管的项目目录或图片资源。 他支持提供npm、Github、WordPress上资源cdn服务。\nCDN (全称 Content Delivery Network)，即内容分发网络。\nCDN构建在现有网络基础之上的智能虚拟网络，依靠部署在各地的边缘服务器，通过中心平台的负载均衡、内容分发、调度等功能模块，使用户就近获取所需内容，降低网络拥塞，提高用户访问响应速度和命中率。\nCDN 的关键技术主要有内容存储和分发技术，简单来讲，CDN就是根据用户位置分配最近的资源，于是，用户在上网的时候不用直接访问源站，而是访问离他“最近的”一个 CDN 节点(也叫做“边缘节点”、edge node)，其实就是缓存了源站内容的代理服务器\n","date":"2025-06-12T19:33:46+08:00","permalink":"https://charles-7777.github.io/p/github--picgo-%E6%90%AD%E5%BB%BA%E5%9B%BE%E5%BA%8A/","title":"Github + PicGo 搭建图床"},{"content":" 创建仓库 首先在github创建两个仓库，一个用于存储hugo new site创建的workspace，这个设置为private私密；另一个仓库用来存储hugo -D生成的网页文件 ，设置为公开:\n本地获取ssh密钥 终端输入命令：\n1 2 git config --global user.email \u0026#34;you@example.com\u0026#34; #you@example.com替换为你的邮箱 git config --global user.name \u0026#34;Your Name\u0026#34; #Your Name替换为你的名字并回车 生成ssh key,在git bash中输入以下命令:\nssh-keygen -t rsa -C \u0026quot;your_email@example.com\u0026quot; 生成的密钥将存储在C:\\uers\\username\\.ssh\\路径下 打开公钥文件 id_rsa.pub， 复制所有内容，在GitHub上打开Setting -\u0026gt; SSH and GPG keys -\u0026gt; add SSH key，将复制的内容粘贴在里边，保存。\n若配置完后，还是显示无法连接到github,可能是你的Windows电脑的username 是中文导致的bug 或者尝试在.ssh 下创建一个config 文件\n1 2 3 4 5 6 Host github.com HostName ssh.github.com # **这是最重要的部分** User git Port 443 PreferredAuthentications publickey IdentityFile ~/.ssh/id_rsa 创建github token 在 GitHub 账户下 Setting - Developer setting - Personal access tokens(classic) 下创建一个不过期(no expiration)的 Token，权限需要开启 repo 与 workflow。\n（注意：token只会显示一次，请及时保存） 私密源仓库设置token 在博客源仓库的 Settings-\u0026gt;Secrets-\u0026gt;Actions 中添加 PERSONAL_TOKEN 环境变量为刚才的 Token,\n这样 GitHub Action 就可以获取到Token 了。 本地首次创建博客 1 2 3 hugo new site hugo-stack-blog-dev git init git submodule add https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack git submodule主要是将自己的改动，与引用的stack主题仓库分开。hugo-theme-stack 下的目录结构基本与站点源目录结构一致 hugo 生成网站时优先寻找站点源目录下的，找不到，才会去stack主题对应目录下寻找\n接下来将 exampleSite样例数据中的 Content 和 hugo.yaml 复制到主文件夹中，并删掉hugo.toml\n可以hugo server -D看一下初始网站长啥样\n创建一篇新文章(在主目录下)\n1 hugo new post/blog1/index.zh-cn.md 初始化博客源仓库，提交到github\n1 2 3 4 5 git add . git commit -m \u0026#34;first commit\u0026#34; git branch -M main git remote add origin git@github.com:charles-7777/hugo-stack-blog-dev.git git push -u origin main 创建workflows发布文件 在本地博客主目录下创建 .github/workflows目录，然后创建xxxx.yaml文件。我的 GitHub Action 配置为，自动发布示例配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 name: deploy # 代码提交到main分支时触发github action on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: submodules: recursive fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;0.147.7\u0026#34; extended: true - name: Build Web run: hugo -D - name: Deploy Web uses: peaceiris/actions-gh-pages@v4 with: PERSONAL_TOKEN: ${{ secrets.TOKEN }} EXTERNAL_REPOSITORY: charles-7777/hugo-stack PUBLISH_BRANCH: main PUBLISH_DIR: ./public commit_message: auto deploy 提交github action的改动后，每次push hugo-stack-blog-dev ,就会把基于这个厂库 hugo -D 生成的文件 自动push 到仓库 hugo-stack\nGitHub Pages 前往hugo-stack 仓库的settings--\u0026gt;Pages去添加deploy GitHub Pages 在别的电脑拉取源仓库修改 1 2 3 git clone git@github.com:charles-7777/hugo-stack-blog-dev.git git submodule init git submodule update 参考链接\nhttps://letere-gzj.github.io/hugo-stack/tags/hugo/\n参考博主letere-gzj的视频 markdown中的图片也可以不用本地的图片，可以使用网络url，可以自己创建一个图床，方便管理。 github图床\n","date":"2025-06-09T00:00:00Z","image":"https://cdn.jsdelivr.net/gh/charles-7777/ImageBed@main/blog/hugo.png","permalink":"https://charles-7777.github.io/p/hugo-stack-blog-%E6%90%AD%E5%BB%BA%E8%AE%B0%E5%BD%95/","title":"hugo stack blog 搭建记录"}]