[{"content":"前言 在 Mac 上通过 VS Code Remote SSH 连到远端无图形界面的 Fedora 开发机使用 Pi 时，按 Ctrl+V 默认读不到 Mac 本地的剪贴板图片。\n解决思路很简单：\nMac 端：通过 pngpaste 导出剪贴板图片，用 launchd 监听端口（inetd 模式按需调用）； 传输层：通过 SSH 反向隧道（RemoteForward）把 Mac 本地的服务暴露给 Fedora 的 127.0.0.1:7779； Fedora 端：安装 pi-ssh-image-clipboard 插件（fork 自 pasky/pi-ssh-image-clipboard），拦截 Ctrl+V，从 127.0.0.1:7779 读取图片写入 /tmp/，并将文件路径填入 Pi 输入框。 flowchart LR A[Mac 剪贴板] --\u003e B[launchd + pngpaste 127.0.0.1:7779] B --\u003e|SSH RemoteForward| C[Fedora 127.0.0.1:7779] C --\u003e D[pi-ssh-image-clipboard] D --\u003e E[`/tmp/pi-clipboard-UUID.png`] E --\u003e F[Pi 输入框] 整个链路不复杂，但配置时最容易踩的坑是：以为在 ~/.ssh/config 里写了 RemoteForward 就完事了，实际当前已有的 VS Code SSH 连接或 ControlMaster 并不会动态补上新转发。\nMac 端配置：提供图片服务 安装 pngpaste 1 2 3 4 brew install pngpaste which pngpaste # Apple Silicon: /opt/homebrew/bin/pngpaste # Intel Mac: /usr/local/bin/pngpaste 配置 clipserve (LaunchAgent) 新建 ~/Library/LaunchAgents/clipserve.plist：\n注意：\u0026lt;string\u0026gt; 中的 pngpaste 路径务必替换为 which pngpaste 的实际输出。\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 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;clipserve\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/opt/homebrew/bin/pngpaste\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;Sockets\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Listeners\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;SockNodeName\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;127.0.0.1\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;SockServiceName\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;7779\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;key\u0026gt;inetdCompatibility\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Wait\u0026lt;/key\u0026gt; \u0026lt;false/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/tmp/pngpaste-clipserve.err\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 使用 inetd 模式的好处是：平时不占用后台进程，只有端口收到连接时才调起 pngpaste - 吐出二进制流。\n加载服务并验证：\n1 2 3 4 5 6 # 启动服务 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.pasky.clipserve.plist # 截图到剪贴板后测试本地能否读取 nc 127.0.0.1 7779 | file - # 正常输出：/dev/stdin: PNG image data, ... 配置 SSH Tunnel (LaunchAgent) 为什么不直接靠 VS Code SSH 转发？\nVS Code Remote SSH 复用的是初始建立的 SSH 通道，在 ~/.ssh/config 新增 RemoteForward 后，不断开重连是不会生效的。而且只要 VS Code 窗口关掉或连接重置，转发就会中断。\n更稳妥的方式是用单独的 LaunchAgent 维护一条专职转发的 SSH 隧道。\n新建 ~/Library/LaunchAgents/pi-clip-tunnel.plist：\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 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;pi-clip-tunnel\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/usr/bin/ssh\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-S\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;none\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-NT\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-o\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;BatchMode=yes\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-o\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;ExitOnForwardFailure=yes\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-o\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;ServerAliveInterval=30\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-o\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;ServerAliveCountMax=3\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-o\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;ConnectTimeout=10\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-R\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;127.0.0.1:7779:127.0.0.1:7779\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;fedora\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;ThrottleInterval\u0026lt;/key\u0026gt; \u0026lt;integer\u0026gt;10\u0026lt;/integer\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/tmp/pi-clip-tunnel.err\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 关键参数：\n-S none：不复用已有的 SSH ControlMaster，避免被其他会话状态干扰； -o ExitOnForwardFailure=yes：端口冲突或转发失败时立即退出，交给 launchd 的 KeepAlive 重试； -o BatchMode=yes：非交互模式，依赖配置好的 SSH Key（需已配好公钥免密登录或 1Password / ssh-agent）。 加载隧道：\n1 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/pi-clip-tunnel.plist 在 Fedora 上验证隧道是否通畅：\n1 2 3 4 5 # 检查端口监听 ss -ltn \u0026#39;( sport = :7779 )\u0026#39; # 测试读取 Mac 剪贴板 nc 127.0.0.1 7779 | file - Fedora 端配置：安装与配置 Pi 插件 安装插件 1 pi install git:github.com/kerolt/pi-ssh-image-clipboard 解绑内置冲突快捷键 Pi 原生自带的 app.clipboard.pasteImage 同样绑定了 Ctrl+V，在无 GUI 的 Linux 下无法工作还会截断事件。\n在 Fedora 的 ~/.pi/agent/keybindings.json 中将其置空：\n1 2 3 { \u0026#34;app.clipboard.pasteImage\u0026#34;: [] } 修改后在 Pi 终端里执行 /reload 重新加载配置。\n排障速查 Fedora 报 Connection refused\n原因：远端 7779 端口未建立监听（转发隧道未通）。 排查：Fedora 上运行 ss -ltn '( sport = :7779 )' 检查端口；Mac 上测试 ssh -S none -o BatchMode=yes fedora true 确认 SSH 密钥是否支持非交互免密登录。 报 Remote forwarding failed\n原因：远端 7779 端口被旧残余进程占用，或 sshd 禁止了端口转发。 排查：Fedora 上运行 ss -ltnp '( sport = :7779 )' 查看占用进程；检查远端 /etc/ssh/sshd_config 是否开启 AllowTcpForwarding yes。 提示 No image on client clipboard\n原因：Mac 当前剪贴板中没有可识别的图片数据。 排查：在 Mac 本地执行 pngpaste - | file - 验证剪贴板是否有有效图像流。 按 Ctrl+V 没反应或报快捷键冲突\n原因：Pi 内置图片粘贴动作未解绑，或插件未生效。 排查：检查 ~/.pi/agent/keybindings.json 配置，并在 Pi 终端中执行 /reload。 发送后模型报错无法识别图片\n原因：当前激活的模型不支持 Vision（图片）输入。 排查：运行 pi --list-models，确认当前模型在 images 一列是否标记为 yes。 安全与多用户注意事项 回环端口暴露： 127.0.0.1:7779 在单人开发机上最省事，但在多人共享的服务器上，其他本地用户也能连该端口偷读你的 Mac 剪贴板。多用户环境建议改用插件支持的 Unix Socket 方案：~/.pi-clip/\u0026lt;client\u0026gt;.sock（配合 chmod 700）。\n临时文件权限： 插件保存的 /tmp/pi-clipboard-*.png 默认权限是 0644。如果涉及敏感业务截图，可配置别名让 Pi 在严格 umask 下运行：\n1 alias pi=\u0026#39;(umask 077 \u0026amp;\u0026amp; command pi \u0026#34;$@\u0026#34;)\u0026#39; 并定期清理历史缓存：\n1 find /tmp -maxdepth 1 -user \u0026#34;$USER\u0026#34; -name \u0026#39;pi-clipboard-*\u0026#39; -mtime +1 -delete ","permalink":"https://kerolt.github.io/p/pi-%E8%BF%9C%E7%A8%8B-ssh-%E5%9B%BE%E7%89%87%E5%89%AA%E8%B4%B4%E6%9D%BF%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE%E4%B8%8E%E8%B8%A9%E5%9D%91/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e在 Mac 上通过 VS Code Remote SSH 连到远端无图形界面的 Fedora 开发机使用 Pi 时，按 \u003ccode\u003eCtrl+V\u003c/code\u003e 默认读不到 Mac 本地的剪贴板图片。\u003c/p\u003e\n\u003cp\u003e解决思路很简单：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eMac 端\u003c/strong\u003e：通过 \u003ccode\u003epngpaste\u003c/code\u003e 导出剪贴板图片，用 \u003ccode\u003elaunchd\u003c/code\u003e 监听端口（inetd 模式按需调用）；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e传输层\u003c/strong\u003e：通过 SSH 反向隧道（\u003ccode\u003eRemoteForward\u003c/code\u003e）把 Mac 本地的服务暴露给 Fedora 的 \u003ccode\u003e127.0.0.1:7779\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFedora 端\u003c/strong\u003e：安装 \u003ca href=\"https://github.com/kerolt/pi-ssh-image-clipboard\"\u003e\u003ccode\u003epi-ssh-image-clipboard\u003c/code\u003e\u003c/a\u003e 插件（fork 自 \u003ca href=\"https://github.com/pasky/pi-ssh-image-clipboard\"\u003epasky/pi-ssh-image-clipboard\u003c/a\u003e），拦截 \u003ccode\u003eCtrl+V\u003c/code\u003e，从 \u003ccode\u003e127.0.0.1:7779\u003c/code\u003e 读取图片写入 \u003ccode\u003e/tmp/\u003c/code\u003e，并将文件路径填入 Pi 输入框。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre class=\"mermaid\" style=\"visibility:hidden\"\u003eflowchart LR\n    A[Mac 剪贴板] --\u003e B[launchd + pngpaste\n127.0.0.1:7779]\n    B --\u003e|SSH RemoteForward| C[Fedora\n127.0.0.1:7779]\n    C --\u003e D[pi-ssh-image-clipboard]\n    D --\u003e E[`/tmp/pi-clipboard-UUID.png`]\n    E --\u003e F[Pi 输入框]\u003c/pre\u003e\n\u003cp\u003e整个链路不复杂，但配置时最容易踩的坑是：\u003cstrong\u003e以为在 \u003ccode\u003e~/.ssh/config\u003c/code\u003e 里写了 \u003ccode\u003eRemoteForward\u003c/code\u003e 就完事了，实际当前已有的 VS Code SSH 连接或 ControlMaster 并不会动态补上新转发\u003c/strong\u003e。\u003c/p\u003e","title":"Pi 远程 SSH 图片剪贴板插件配置与踩坑"},{"content":"背景 组网拓扑：本机（Mac）通过 EasyTier 虚拟网连接一台 Linux 网关节点，该节点所在局域网内有一台 GitLab 服务器。 目标：在 Mac 浏览器中直接访问 GitLab 的 Web 页面和 git 服务。 方案选型：使用 EasyTier 的子网代理（subnet proxy）功能，让网关节点把 GitLab 地址代理进虚拟网。相比逐端口转发，一次配置全局受益，浏览器、HTTP、SSH 全部透明可用。 过程 （1）声明代理网段 在已通过 easytier 连接的 Linux 节点上的 easytier 配置文件中加入：\n1 2 [[proxy_network]] cidr = \u0026#34;GITLAB_IP/32\u0026#34; 需要注意的是：子网代理支持多个 cidr（多个 [[proxy_network]] 段），掩码为 /32 时可以只代理单台主机。\n修改后重启服务，并在两个层面确认配置真正生效：\n1 2 3 4 5 # 本节点应看到 Proxy CIDRs 列出代理网段 sudo easytier-cli node # 所有节点都会自动学到这条路由，proxy_cidrs 列应显示代理网段 sudo easytier-cli route （2）开启内核 IP 转发 子网代理的本质是网关节点把虚拟网流量转发到局域网，Linux 默认不转发数据包，必须显式开启：\n1 2 3 4 5 # 运行时生效 sudo sysctl -w net.ipv4.ip_forward=1 # 持久化：写入独立配置文件，重启后依然生效 echo \u0026#34;net.ipv4.ip_forward = 1\u0026#34; | sudo tee /etc/sysctl.d/99-ipforward.conf 验证：\n1 2 sysctl net.ipv4.ip_forward # 输出 net.ipv4.ip_forward = 1 即为开启 （3）配置防火墙地址伪装 来自虚拟网的流量源地址是虚拟网段（如 10.x.x.x），局域网内的被代理主机不认识这个网段，通常会因为路由不可达或防火墙策略丢弃。需要在网关节点把转发流量的源地址伪装成网关自己的内网地址。\n以 firewalld 为例：\n1 2 3 4 5 6 # public zone 开启 masquerade sudo firewall-cmd --zone=public --add-masquerade --permanent sudo firewall-cmd --reload # 确认已开启，输出 yes sudo firewall-cmd --zone=public --query-masquerade 注意：firewalld 使用 nftables 后端，iptables 命令看不到这些规则，验证要用 sudo nft list ruleset 查找 masquerade 条目。\n另外一个容易忽略的点：虚拟网卡（如 tun0）默认没有被 firewalld 划入任何 zone，如果流量被默认丢弃策略拦下，可显式将其加入 public zone：\n1 2 sudo firewall-cmd --zone=public --add-interface=tun0 --permanent sudo firewall-cmd --reload 配置生效后，Mac ping GitLab 地址正常，但 curl 访问 Web 端口直接超时，分层排查为：\nMac 本机：无代理环境变量干扰，路由表已正确学到代理网段（目标地址指向 EasyTier 虚拟网卡）。 网关节点：已开启 IP 转发并持久化，firewalld 已开启 masquerade（ICMP 抓包确认源地址伪装正常）。 关键实验：从网关节点本机 curl GitLab 正常返回，从另一台虚拟网节点 curl 同样超时，且在网关节点物理网卡上完全抓不到 TCP 包，即 SYN 包根本没到达网关。 现象特征： ICMP 能走通整条链路并被正确伪装转发，TCP 的 SYN 却在 EasyTier 内部凭空消失。\n查证后确认这是 EasyTier 的已知缺陷（issue #1355）：子网代理节点使用内核网络栈时，内置 NAT 在处理 TCP 回包时会改错源地址和端口，导致内核 TCP 连接跟踪不匹配、会话被 RST 终止；ICMP 不走 TCP 连接跟踪，所以幸免。\n（4）启用用户态网络栈 解决方案： 在网关节点配置的 [flags] 中启用用户态网络栈：\n1 2 [flags] use_smoltcp = true 修改后重启服务，Mac 访问 GitLab 的 80 端口立即恢复，git SSH 端口也验证连通。\n（5）端到端验证 回到发起访问的客户端节点逐项验证：\n1 2 3 4 5 6 7 8 9 10 # ICMP 层：路由已通 ping GITLAB_IP # TCP 层：Web 服务，出现 302 跳转或 200 即为成功 curl -sk -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; http://GITLAB_IP # git 服务（SSH 协议）：看到 GitLab 欢迎信息即成功 ssh git@GITLAB_IP # 浏览器直接访问 http://GITLAB_IP 常见异常对照：\nping 不通：检查网关节点 proxy_cidrs 是否同步（easytier-cli route）、内核 IP 转发是否开启 ping 通但 TCP 超时：大概率命中内核栈 NAT 缺陷，启用 use_smoltcp；同时确认防火墙 masquerade 已开启 TCP 通但页面跳转到错误地址：GitLab 的 external_url 配置的是内网主机名或 IP，需在客户端 /etc/hosts 增加解析，或调整 external_url ","permalink":"https://kerolt.github.io/p/easytier-%E5%AD%90%E7%BD%91%E4%BB%A3%E7%90%86/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e组网拓扑：本机（Mac）通过 EasyTier 虚拟网连接一台 Linux 网关节点，该节点所在局域网内有一台 GitLab 服务器。\u003c/li\u003e\n\u003cli\u003e目标：在 Mac 浏览器中直接访问 GitLab 的 Web 页面和 git 服务。\u003c/li\u003e\n\u003cli\u003e方案选型：使用 EasyTier 的子网代理（subnet proxy）功能，让网关节点把 GitLab 地址代理进虚拟网。相比逐端口转发，一次配置全局受益，浏览器、HTTP、SSH 全部透明可用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"过程\"\u003e过程\u003c/h2\u003e\n\u003ch3 id=\"1声明代理网段\"\u003e（1）声明代理网段\u003c/h3\u003e\n\u003cp\u003e在已通过 easytier 连接的 Linux 节点上的 easytier 配置文件中加入：\u003c/p\u003e","title":"EasyTier 子网代理"},{"content":"背景与网络拓扑 在一台运行 Fedora 44 Server 的无显示器（Headless）高性能计算服务器上，机器同时接入了多套网络。其中，校园有线网需要通过 Dr.COM Web Portal 进行网页登录认证。由于服务器未安装默认图形界面，且存在多网卡出口路由竞争，无法直接弹窗认证。\n为了实现认证和后续的图形化运维，先后尝试了 CLI 脚本模拟认证、SSH 动态隧道（SOCKS5）+ 本地浏览器、XFCE + xrdp 远程桌面，以及连接同网段另一台 Rocky Linux 10.2（KDE Plasma Wayland + KRDP） 主机。在这一过程中遇到了多默认路由屏蔽、ARP 故障、xrdp 闪退与画面假死、KRDP XDG Desktop Portal 注册失败等一系列典型问题。\n网络拓扑与接口分布 整个排障环境的网络拓扑结构如下所示：\nflowchart TB subgraph Client [\"本地管理端 (Mac / Windows)\"] MAC[\"MacBook\n192.168.202.1 / 10.112.112.1\"] end subgraph FedoraHost [\"Fedora 44 Server (Headless 主机)\"] NIC1[\"enp171s0\n192.168.202.2/24\n(直连管理口)\"] NIC2[\"enp172s0\n10.65.152.121/16\n(校园有线，待 Portal 认证)\"] NIC3[\"wlp173s0\n10.124.245.201/24\n(已认证 WiFi, metric=50)\"] TUN_F[\"tun0\n10.112.112.2/24\n(科研专网隧道)\"] end subgraph LabHost [\"Rocky Linux 10.2 (实验室主机)\"] TUN_R[\"tun0\n10.112.112.4/24\n(KDE Plasma Wayland + KRDP)\"] end subgraph CampusNet [\"校园网络基础设施\"] GW[\"有线网关\n10.65.0.1 (metric=102)\"] PORTAL[\"Dr.COM 认证服务器\n10.1.116.8:80 / :801\"] end MAC \u003c--\u003e|直连双绞线 / 管理流量| NIC1 NIC2 \u003c--\u003e GW GW -. 拦截重定向 .-\u003e PORTAL TUN_F \u003c===\u003e|内网互联| TUN_R MAC -.-\u003e|科研专网路由| TUN_R 涉及的核心网络接口信息表：\n主机节点 接口名称 IP / 掩码 作用说明 Fedora Server enp171s0 192.168.202.2/24 与 Mac 直连网口，提供稳健的带外 SSH 和 RDP 运维通道 Fedora Server enp172s0 10.65.152.121/16 校园有线网（NetworkManager 连接名 有线连接 1），待认证 Fedora Server wlp173s0 10.124.245.201/24 实验室备用 WiFi（连接名 KEROLT），已可连通外网 Fedora Server tun0 10.112.112.2/24 实验室内部互联隧道 Rocky Linux 10.2 tun0 10.112.112.4/24 已运行 KDE Plasma Wayland 桌面及 KRDP 服务的计算节点 一、现象排查：有线网卡已获取 IP，却无法访问互联网 1. 现象观察 通过 nmcli 查看，enp172s0 状态正常，且已通过 DHCP 成功获取到了 IP 地址与默认网关：\n1 2 3 接口: enp172s0 (有线连接 1) IP 地址: 10.65.152.121/16 默认网关: 10.65.0.1 但在强制绑定该接口访问公网时，外部网络完全不可达：\n1 2 ping -I enp172s0 223.5.5.5 # 结果: 100% packet loss 2. 逐层排查证据 二层 / 三层连通性正常：能够稳定 ping 通上联汇聚交换机/默认网关，时延极低且无丢包： 1 2 ping -I enp172s0 10.65.0.1 # 结果: 0% packet loss，rtt min/avg/max = 0.42/0.51/0.63 ms 公网 ICMP 流量被丢弃：外部 IPv4 地址（如公共 DNS 223.5.5.5）100% 丢包。 应用层 HTTP 探测暴露劫持与重定向：使用 curl 针对公网 IP 发起纯 HTTP 请求，捕获到了典型的 302 重定向报文： 1 curl -I --interface enp172s0 http://223.5.5.5 返回如下响应头： 1 2 3 HTTP/1.0 302 Moved Temporarily Server: NetEngine Server 1.0 Location: http://10.1.116.8/a79.htm?zjnudrcom8905=ethtrunk/2:1077.0 进一步获取重定向目标页面的响应，其 HTTP 响应头包含 Server: DrcomServer1.0，页面标题明确显示为 上网登录页。 3. 根因剖析与多默认路由优先级陷阱 物理链路、DHCP 协商及局域网通信均正常。校园网出口部署了 Dr.COM Web Portal 准入控制系统，未认证主机允许访问校园内网及 Portal 认证服务器，但所有外部公网流量均被网关拦截并重定向。\n在排查初期，如果不加 --interface enp172s0 直接在终端执行 curl http://www.baidu.com，会发现能够正常访问互联网。检查系统路由表：\n1 2 3 $ ip route show default via 10.124.245.141 dev wlp173s0 proto dhcp src 10.124.245.201 metric 50 default via 10.65.0.1 dev enp172s0 proto dhcp src 10.65.152.121 metric 102 [!IMPORTANT] 多出口优先级陷阱： 系统同时存在两条默认路由（0.0.0.0/0）。WiFi 接口（wlp173s0）的路由 metric 为 50，优先级高于有线网卡（enp172s0）的 102。因此未经指定出接口的普通应用流量默认通过 WiFi 出口访问外网，完全掩盖了有线网络尚未认证的事实。排查多网卡主机时，**必须显式指定出接口（-I 或 --interface）**才能观测到真实的接口状态。\n二、方案 A：纯命令行自动化 Dr.COM 认证 对于长期运行的 Headless 服务器，最优雅的解决方案是编写无需图形界面的认证脚本，配合 systemd 或 crontab 实现开机与断线自动认证。\n1. 认证接口逆向分析 查看 Portal 登录页源码及加载的 JavaScript 脚本，提取出关键参数：\n1 2 3 4 authloginport = 801 authloginpath = \u0026#39;/eportal/?c=ACSetting\u0026amp;a=Login\u0026#39; authuserfield = \u0026#39;DDDDD\u0026#39; authpassfield = \u0026#39;upass\u0026#39; 进一步确认底层支持 JSONP 方式提交登录请求，其服务端标准登录接口为：\n1 http://10.1.116.8:801/eportal/?c=Portal\u0026amp;a=Login 使用测试参数验证接口响应：\n1 curl -s \u0026#34;http://10.1.116.8:801/eportal/?c=Portal\u0026amp;a=Login\u0026amp;callback=dr1003\u0026amp;login_method=1\u0026amp;user_account=,0,testuser\u0026amp;user_password=wrongpass\u0026#34; 服务端返回 JSONP 格式响应：\n1 dr1003({\u0026#34;result\u0026#34;: 0, \u0026#34;msg\u0026#34;: \u0026#34;账号不存在\u0026#34;, \u0026#34;ret_code\u0026#34;: 1}) 能够明确返回认证系统业务错误码，证明接口路径、方法及参数格式完全正确。参数中 user_account=,0,${USER} 为典型的运营商标识前缀（,0, 代表校园网默认出海线路）。\n2. 自动化登录脚本实现 为提高脚本通用性与安全性，账号密码通过环境变量注入，避免硬编码明文密码。\n创建脚本 ~/.local/bin/drcom-auth.sh：\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 #!/usr/bin/env bash # ============================================================================== # 脚本名称: drcom-auth.sh # 作用说明: 校园网 Dr.COM Portal 命令行自动化认证脚本 # ============================================================================== set -euo pipefail IFACE=\u0026#34;${DRCOM_IFACE:-enp172s0}\u0026#34; REALM=\u0026#34;${DRCOM_REALM:-10.1.116.8:801}\u0026#34; USER=\u0026#34;${DRCOM_USER:?错误: 必须提供 DRCOM_USER 环境变量}\u0026#34; PASS=\u0026#34;${DRCOM_PASS:?错误: 必须提供 DRCOM_PASS 环境变量}\u0026#34; # 1. 动态获取指定接口的当前 IPv4 地址 IP=$(ip -4 -o addr show dev \u0026#34;$IFACE\u0026#34; | awk \u0026#39;{print $4}\u0026#39; | cut -d/ -f1 | head -n1) if [[ -z \u0026#34;$IP\u0026#34; ]]; then echo \u0026#34;[!] 错误: 接口 $IFACE 未获取到有效 IPv4 地址，请检查 DHCP 状态\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi echo \u0026#34;[*] 正在通过接口 $IFACE (IP: $IP) 请求认证...\u0026#34; # 2. 发起 GET JSONP 登录请求 resp=$(curl --interface \u0026#34;$IFACE\u0026#34; -sS -m 15 -G \\ \u0026#34;http://${REALM}/eportal/\u0026#34; \\ --data-urlencode \u0026#34;c=Portal\u0026#34; \\ --data-urlencode \u0026#34;a=Login\u0026#34; \\ --data-urlencode \u0026#34;callback=dr1003\u0026#34; \\ --data-urlencode \u0026#34;login_method=1\u0026#34; \\ --data-urlencode \u0026#34;user_account=,0,${USER}\u0026#34; \\ --data-urlencode \u0026#34;user_password=${PASS}\u0026#34; \\ --data-urlencode \u0026#34;wlan_user_ip=${IP}\u0026#34; \\ --data-urlencode \u0026#34;wlan_user_ipv6=\u0026#34; \\ --data-urlencode \u0026#34;wlan_user_mac=000000000000\u0026#34; \\ --data-urlencode \u0026#34;wlan_ac_ip=\u0026#34; \\ --data-urlencode \u0026#34;wlan_ac_name=\u0026#34;) # 3. 结果判断 if grep -q \u0026#39;\u0026#34;result\u0026#34;:1\u0026#39; \u0026lt;\u0026lt;\u0026lt; \u0026#34;$resp\u0026#34;; then echo \u0026#34;[+] 认证成功！服务器响应: $resp\u0026#34; else echo \u0026#34;[-] 认证失败！服务器响应: $resp\u0026#34; \u0026gt;\u0026amp;2 exit 2 fi 赋予执行权限并测试运行：\n1 2 chmod 700 ~/.local/bin/drcom-auth.sh DRCOM_USER=\u0026#34;my_student_id\u0026#34; DRCOM_PASS=\u0026#34;my_password\u0026#34; ~/.local/bin/drcom-auth.sh [!TIP] 建议将脚本存放在 ~/.local/bin 或 /usr/local/bin 等持久目录。避免放置在 /tmp 下，防止系统定时清理（如 systemd-tmpfiles-clean）或机器重启后丢失。\n三、方案 B：SSH Tunnel + 本地浏览器转发 在某些特殊场景下（例如 Portal 临时启用了滑动验证码、短信二次验证或复杂的动态前端安全检测），纯命令行脚本无法处理交互式验证，此时需要借助客户端浏览器呈现认证页面。\n1. 动态端口转发（SOCKS5） 在本地 Mac 终端执行：\n1 2 # 建立后台动态 SOCKS5 代理隧道 ssh -D 21219 -N -C kerolt@192.168.202.2 配置本地浏览器代理（建议使用独立浏览器 Profile 或 SwitchyOmega 插件）：\n代理协议：SOCKS5 代理服务器：127.0.0.1 端口：21219 [!NOTE] SOCKS5 代理客户端设置的端口必须与 SSH 命令 -D 参数指定的本地监听端口（如 21219）保持严格一致，避免端口不匹配导致连接被拒。\n2. 多网卡环境下的流量出口陷阱与对策 在 SOCKS5 隧道中，连接是由远程 Linux 主机发起的。根据前文分析的 Linux 路由规则，由于 WiFi 的 metric 优先级高于有线网卡，SOCKS5 转发的流量仍然会从 WiFi 接口流出，从而无法触发有线网卡的 Portal 页面。\n快速应对策略：临时断开 WiFi 连接。\n由于 Mac 是通过直连网卡（192.168.202.0/24）与服务器建立 SSH 会话的，关闭 WiFi 并不会中断当前的 SSH 隧道：\n1 2 3 4 5 6 7 # 临时关闭 WiFi，使有线网卡成为唯一默认出口 sudo nmcli connection down KEROLT # 在 Mac 浏览器中访问任意 HTTP 网站（如 http://neverssl.com），完成 Portal 认证 # 认证完成后重新激活 WiFi sudo nmcli connection up KEROLT (进阶方案：亦可配置基于 UID 的 ip rule 策略路由，将特定用户发起的流量固定导向 enp172s0，但单次运维中临时 down 掉 WiFi 效率最高。)\n3. 踩坑复盘：静态端口转发为何报 channel 4: open failed？ 在排查过程中，曾尝试通过静态端口转发将认证服务器直接映射到本地：\n1 2 3 4 ssh -N \\ -L 18080:10.1.116.8:80 \\ -L 18010:10.1.116.8:801 \\ kerolt@192.168.202.2 随后在本地浏览器访问 http://127.0.0.1:18080 时，SSH 终端频繁输出如下报错：\n1 channel 4: open failed: connect failed: open failed 很多工程师遇到这个错误容易误判为 SSH 权限或配置问题。在服务器端查看邻居解析状态暴露了真相：\n1 2 ip neigh show dev enp172s0 # 输出: 10.1.116.8 dev enp172s0 FAILED [!WARNING] 排查经验： channel open failed: connect failed 表明 Mac 到服务器的 SSH 隧道完全通畅，但服务器向目标转发地址发起 TCP 三次握手失败。 当时认证服务器 10.1.116.8 自身的 ARP 处于 FAILED 状态，导致 80 与 801 端口在网络层根本不可达。这属于校园网认证系统服务端的暂时性故障。SSH 隧道无法绕过上游服务端本身的网络宕机。\n四、方案 C：Fedora Server 部署 XFCE + xrdp 远程桌面 如果日常维护需要常驻图形交互环境（例如浏览网页图形控制台、观察 GPU 仪表盘或应对复杂的交互式验证），在 Headless 服务器上安装轻量级桌面环境 + xrdp 是兼顾资源消耗与易用性的方案。\n1. 安装与网络隔离 安装轻量级 XFCE 桌面环境与 xrdp 服务：\n1 2 3 sudo dnf group install -y xfce-desktop sudo dnf install -y xrdp xorgxrdp xrdp-selinux dbus-x11 firefox sudo systemctl enable --now xrdp [!NOTE] 服务器不需要将系统默认目标切换为 graphical.target，继续维持无头服务器的标准 multi-user.target 运行即可。xrdp 的 sesman 组件会在用户通过 RDP 发起连接时，按需动态拉起独立的虚拟 Xorg 会话，无连接时不占用额外 GPU/渲染资源。\n防火墙最小权限放行： 为防止将 RDP 暴露在开放的校园网环境中，只允许 Mac 直连管理网段访问 3389 端口：\n1 2 3 4 sudo firewall-cmd --permanent --zone=FedoraServer \\ --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; source address=\u0026#34;192.168.202.0/24\u0026#34; port port=\u0026#34;3389\u0026#34; protocol=\u0026#34;tcp\u0026#34; accept\u0026#39; sudo firewall-cmd --reload 2. 踩坑与排障（一）：登录认证成功后瞬间闪退 故障现象 在 Mac 端使用 Windows App（原 Microsoft Remote Desktop 客户端）连接服务器，输入用户名和密码验证通过后，远程桌面窗口一闪黑屏，随后立即自动关闭断开。\n日志溯源 查看 xrdp 与 sesman 服务的运行时日志：\n1 journalctl -u xrdp -u xrdp-sesman --since \u0026#34;-5 min\u0026#34; 日志中出现以下典型会话终结记录：\n1 2 3 4 login was successful - creating session connected to Xorg Xorg server closed connection Session on display 10 has finished 根因剖析 在 Fedora 系统中，用户主目录下若不存在 ~/.Xclients，xrdp 会回退调用系统全局的 /etc/X11/xinit/Xclients。由于系统中未安装默认的 GNOME 桌面环境，该全局脚本无法自动探知并拉起 XFCE，窗口管理器启动指令执行完毕后立即退出，导致 Xorg 服务端判定会话结束而直接关闭连接。\n解决方案 在用户主目录下显式创建 ~/.Xclients 启动脚本，通过 dbus-launch 启动 XFCE 会话，并务必刷新 SELinux 安全标签：\n1 2 3 4 5 6 7 8 9 10 11 12 cat \u0026gt; ~/.Xclients \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; #!/bin/sh export XDG_SESSION_TYPE=x11 unset DBUS_SESSION_BUS_ADDRESS exec dbus-launch --exit-with-session startxfce4 EOF chmod 700 ~/.Xclients # 恢复文件 SELinux 上下文，避免被安全策略拦截执行 restorecon -v ~/.Xclients sudo systemctl restart xrdp 3. 踩坑与排障（二）：进入桌面后画面卡死假死 故障现象 成功登录进入 XFCE 桌面，打开终端或 Firefox 运行几秒钟后，鼠标移动无响应，整个桌面画面彻底冻结。\n排查数据 在直连 SSH 终端中实时排查系统资源与连接状态：\n节点负载极低：load average: 0.80, 0.48, 0.32； 可用内存充裕（剩余 ~21 GiB）； xfce4-session、Xorg、xfwm4 及 firefox 进程均处于正常睡眠（S）状态，并无崩溃或死锁； RDP 协议的 TCP 3389 连接处于稳定的 ESTABLISHED 状态。 进一步排查 /var/log/xrdp.log 与图形日志，发现核心症结在于：\n动态分辨率频繁重协商：Mac 客户端在窗口模式下拖动边缘改变大小时，频繁向 xrdp 发送动态分辨率重置请求，导致 xrdp 持续高频重建 OpenH264 编码管道，造成协议处理管道阻塞； 窗口合成器冲突：XFCE 默认的 xfwm4 合成器（Compositor）在虚拟帧缓冲区中处理半透明与阴影特效时发生图形同步阻塞。 综合优化措施 禁用 XFCE 窗口合成器： 在终端下执行命令，关闭当前 X 屏幕下的合成器功能： 1 2 DISPLAY=:10.0 XAUTHORITY=\u0026#34;$HOME/.Xauthority\u0026#34; \\ xfconf-query -c xfwm4 -p /general/use_compositing -s false 客户端锁定固定分辨率： 在 Mac 客户端连接设置中，将显示分辨率固定为如 1920x1080，并取消勾选“自动根据窗口大小调整远程分辨率（Update the session resolution on resize）”。 回退 xrdp 编解码器为稳健的 RFX： OpenH264 硬件/软件加速在某些客户端上容易引发通道协商挂起。编辑 /etc/xrdp/gfx.toml，将编解码器回退为纯 RemoteFX（RFX）： 1 2 [codec] order = [ \u0026#34;RFX\u0026#34; ] 重启服务生效： 1 sudo systemctl restart xrdp [!TIP] 会话生命周期管理： 直接点击客户端窗口的关闭按钮，仅代表断开当前 RDP 传输层连接，后台的 Xorg、XFCE 和 Firefox 等应用会保持原样继续驻留运行，下次重连时会直接恢复现有桌面。如果任务结束希望释放服务器内存，应在 XFCE 界面右上角点击 Log Out（注销） 正常终止会话。\n五、进阶实践：通过 KRDP 接入 Rocky Linux 10.2 (KDE Wayland) 在另一台处于科研专网隧道（10.112.112.4）的实验室计算节点上，系统为 Rocky Linux 10.2，且物理机前台正在运行 KDE Plasma 6 (Wayland) 桌面。\n对于 Wayland 显示服务架构，传统的 X11 xrdp 方案无法直接捕获 Wayland 合成器画面。KDE 官方自 Plasma 6 起深度集成了 KRDP（基于 FreeRDP 实现的 Wayland 原生远程桌面组件），能够直接共享当前活动的 Plasma 桌面会话。\n1. 配置与激活 KRDP 用户服务 KRDP 作为 systemd 用户级服务（User Service）运行，依赖 D-Bus 会话总线。需为其配置自签名 TLS 证书并启用系统用户认证：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 授予 Wayland 远程桌面 Portal 权限 flatpak permission-set kde-authorized remote-desktop org.kde.krdpserver yes # 2. 生成自签名证书与私钥 install -d -m 700 ~/.local/share/krdpserver openssl req -nodes -new -x509 \\ -keyout ~/.local/share/krdpserver/krdp.key \\ -out ~/.local/share/krdpserver/krdp.crt \\ -days 3650 -batch \\ -subj \u0026#34;/CN=10.112.112.4\u0026#34; chmod 600 ~/.local/share/krdpserver/krdp.key # 3. 写入 KRDP 配置文件 kwriteconfig6 --file krdpserverrc --group General \\ --key Certificate \u0026#34;$HOME/.local/share/krdpserver/krdp.crt\u0026#34; kwriteconfig6 --file krdpserverrc --group General \\ --key CertificateKey \u0026#34;$HOME/.local/share/krdpserver/krdp.key\u0026#34; kwriteconfig6 --file krdpserverrc --group General \\ --key SystemUserEnabled true # 4. 启动并启用 KRDP 用户服务 systemctl --user enable --now app-org.kde.krdpserver.service 2. 踩坑与排障（一）：XDG Desktop Portal 注册失败（App ID 命名不一致） 故障现象 KRDP 服务启动后立即进入异常退出状态，用户日志中反复输出如下报错：\n1 2 Failed to register with host portal App info not found for \u0026#39;org.kde.krdp-server\u0026#39; 根因剖析 在系统 RPM 软件包中，安装的 Desktop 入口文件名为：\n1 /usr/share/applications/org.kde.krdpserver.desktop 但 KRDP 内部向 xdg-desktop-portal 请求远程桌面会话句柄时，所传递的 App ID 字符串为 org.kde.krdp-server（中间多了一个连字符 -）。Portal 无法在应用列表中匹配到该 App ID 的 desktop 文件，从而拒绝了注册请求。\n解决方案 在用户级应用目录中创建别名 desktop 文件，并为该 App ID 单独授予权限：\n1 2 3 4 5 6 7 8 9 10 11 mkdir -p ~/.local/share/applications cp /usr/share/applications/org.kde.krdpserver.desktop \\ ~/.local/share/applications/org.kde.krdp-server.desktop # 显式授予新 ID 远程桌面权限 flatpak permission-set kde-authorized remote-desktop org.kde.krdp-server yes # 依次重启 Portal 相关服务与 KRDP systemctl --user restart xdg-desktop-portal.service systemctl --user restart plasma-xdg-desktop-portal-kde.service systemctl --user restart app-org.kde.krdpserver.service 3. 踩坑与排障（二）：客户端 0x204 错误分层诊断 在 Mac 客户端连接 10.112.112.4:3389 时，客户端经常报出笼统的 0x204 错误代码（通常代表连接超时或协议握手失败）。建议严格按照以下流程逐步定位：\nflowchart TD A[\"客户端报 0x204 错误\"] --\u003e B{\"1. 四层端口连通性验证\nnc -vz 10.112.112.4 3389\"} B -- Connection refused / Timeout --\u003e C[\"网络/安全层阻断\n- 检查防火墙真实来源 IP\n- 检查隧道路由状态\n- 确认 KRDP 服务正在 LISTEN\"] B -- Succeeded --\u003e D{\"2. 协议与会话追查\n查看目标机 journalctl\"} D -- 报错: Portal 鉴权失败 --\u003e E[\"修复 XDG Portal 授权\n或使用 FreeRDP 客户端交叉验证\"] D -- 无连接日志 / SDDM 未登录 --\u003e F[\"物理机处于 SDDM 锁屏态\nKRDP 必须在 Wayland 会话登录后才能连接\"] 关键检查点： 四层连通性与监听确认：\n1 2 3 4 5 6 # 在 Mac 端直接探测目标端口 nc -vz 10.112.112.4 3389 # 若通过 SSH 端口转发（如本地映射为 13389）： lsof -nP -iTCP:13389 -sTCP:LISTEN nc -vz 127.0.0.1 13389 防火墙真实来源 IP 校验： 目标机防火墙必须放行真实的报文来源 IP。如果 Mac 拥有隧道直达路由，源 IP 为 10.112.112.1；如果经过 Fedora Server 跳板转发，目标机看到的源 IP 则为 Fedora 的 10.112.112.2：\n1 2 3 4 5 # 以 Mac 直连隧道 IP 为例： sudo firewall-cmd --permanent --zone=public \\ --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; source address=\u0026#34;10.112.112.1/32\u0026#34; port port=\u0026#34;3389\u0026#34; protocol=\u0026#34;tcp\u0026#34; accept\u0026#39; sudo firewall-cmd --reload [!IMPORTANT] KRDP 的核心设计限制： KRDP 本质上是现有活跃桌面的屏幕共享器（Screen Sharer），而不是独立的会话创建器。它无法像 xrdp 那样在后台按需创建独立的 X/Wayland 虚拟桌面，也无法在锁屏界面 SDDM 阶段进行远程认证。 主机重启后如果停留在 SDDM 显示管理器登录界面，KRDP 用户服务将无法启动。必须在物理机本地登录一次进入桌面，或者在 SDDM 中配置自动登录（需结合物理环境安全风险综合评估）。\n六、体系化排障方法论与命令速查表 在面对 Linux 多网卡路由竞争、Web Portal 准入重定向以及跨架构远程桌面（X11 vs Wayland）的复合故障时，建议遵循自底向上（Bottom-Up）的分层诊断法：\n诊断层级 验证重点 典型故障特征 核心排查命令 L1/L2 物理与链路层 网线、PHY 状态、协商速率、ARP 解析 物理断开、ARP 状态为 FAILED ip link show\nethtool \u0026lt;接口\u0026gt;\nip neigh show L3 网络与路由层 IP 掩码正确性、多默认路由 metric 权重、源地址路由 流量从次级网卡流出、无法 ping 通网关 ip -4 addr show\nip route show\nip route get \u0026lt;目标IP\u0026gt; L4 传输与安全层 TCP 监听端口、iptables/firewalld 规则、NAT 源地址 端口未监听、SYN 报文被防火墙 DROP ss -lntp\nnc -vz \u0026lt;目标IP\u0026gt; \u0026lt;端口\u0026gt;\nfirewall-cmd --list-all L7 准入与认证层 HTTP 302 重定向、Portal 状态码、JSONP 响应 外网流量被拦截、Portal 脚本报错 curl -I -v --interface \u0026lt;接口\u0026gt; http://neverssl.com 桌面与应用会话层 Xorg/Wayland 生命周期、D-Bus、SELinux、合成器 RDP 登录后闪退、画面卡顿冻结、XDG 授权失败 journalctl -u xrdp -u xrdp-sesman -e\nsystemctl --user status app-org.kde.krdpserver 常用诊断命令速查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 快速检查多网卡连接状态与路由 nmcli device status ip route show ip route get 223.5.5.5 # 2. 绑定特定接口测试网关与外网连通性 ping -I enp172s0 10.65.0.1 curl -I --interface enp172s0 http://223.5.5.5 # 3. 端口连通性探测与监听确认 nc -vz 10.112.112.4 3389 ss -lntp | grep -E \u0026#39;3389|801\u0026#39; # 4. 远程桌面核心日志追查 journalctl -u xrdp -u xrdp-sesman --since \u0026#34;-10 min\u0026#34; journalctl --user -u app-org.kde.krdpserver.service --since \u0026#34;-10 min\u0026#34; 总结与备忘清单 通过本次排障与调优，我们打通了多网卡 Headless 主机在校园网复杂准入环境下的完整访问链路：\n有线网络连通性：确认物理与 DHCP 链路正常，根因为 Dr.COM Portal 拦截；理清了 WiFi 与有线网卡的多默认路由优先级陷阱。 自动化认证：完成了命令行逆向与 JSONP 脚本化封装（drcom-auth.sh），支持环境变量传参，可随时接入定时保活。 SSH 代理旁路：明确了 SOCKS5 代理受系统路由表支配的特点，掌握了多出口主机的临时断网认证策略，溯源了静态端口转发时上游 ARP FAILED 故障。 xrdp 稳定运行：通过配置 ~/.Xclients 修复了缺少窗口管理器的闪退问题；通过禁用 xfwm4 合成器、锁定客户端分辨率与切换 RFX 编解码器，彻底根治了画面假死。 KRDP 机制摸底：定位并修复了 Wayland 环境下 org.kde.krdp-server XDG Desktop Portal App ID 不匹配的缺陷；明确了其屏幕共享属性及依赖已登录桌面会话的运行边界。 ","permalink":"https://kerolt.github.io/p/headless-server-portal-%E8%AE%A4%E8%AF%81%E4%B8%8E%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E6%8E%92%E9%9A%9C%E5%85%A8%E8%AE%B0%E5%BD%95/","summary":"\u003ch2 id=\"背景与网络拓扑\"\u003e背景与网络拓扑\u003c/h2\u003e\n\u003cp\u003e在一台运行 \u003cstrong\u003eFedora 44 Server\u003c/strong\u003e 的无显示器（Headless）高性能计算服务器上，机器同时接入了多套网络。其中，校园有线网需要通过 \u003cstrong\u003eDr.COM Web Portal\u003c/strong\u003e 进行网页登录认证。由于服务器未安装默认图形界面，且存在多网卡出口路由竞争，无法直接弹窗认证。\u003c/p\u003e","title":"Headless Server Portal 认证与远程桌面排障全记录"},{"content":"背景 Hermes 把 CLI、TUI、桌面 app、几十种 messaging 平台（飞书/微信/Telegram/Discord/Slack…）接到同一份\u0026quot;agent core\u0026quot;上。理解它的 session 模型，是理解为什么\u0026quot;我换个入口跟它说话\u0026quot;和\u0026quot;我在同个地方继续说\u0026quot;行为完全不同的关键。\n本文整理自一次源码阅读（~/.hermes/hermes-agent/ 仓库，commit 详见文末版本），结论都标注了文件路径 + 行号，便于回查。\n核心结论 Hermes 的 session 体系是三层叠加，每一层解决一类问题：\nflowchart TB A[\"路由层\nbuild_session_key()\"] --\u003e B[\"存储层\nSessionStore (SQLite, FTS5)\"] B --\u003e C[\"并发层\nSessionTurnLeaseRegistry\"] A --\u003e C 路由层：决定\u0026quot;这条消息属于哪个 session\u0026quot; 存储层：决定\u0026quot;这个 session 持久化到哪、怎么 append\u0026quot; 并发层：决定\u0026quot;同一 session 不能并发写\u0026quot; 三层各管一摊，只允许 prompt cache 失效的唯一场景是显式 reset。这条原则写在 AGENTS.md 顶部，Hermes 一切设计都向它对齐。\n1. 路由层：session key 怎么算 gateway/session.py::build_session_key()（1090–1230 行），注释自称为 \u0026ldquo;single source of truth for session key construction\u0026rdquo;。\n关键规则 场景 key 组成 DM（私聊） agent:main:\u0026lt;platform\u0026gt;:dm[:\u0026lt;scope_id\u0026gt;]:\u0026lt;chat_id\u0026gt;[:\u0026lt;thread_id\u0026gt;] DM 没 chat_id 退化到 \u0026lt;platform\u0026gt;:dm:\u0026lt;user_id\u0026gt;[:\u0026lt;thread_id\u0026gt;]（防止跨用户串线，session.py:1151–1154） 群 / 频道 agent:main:\u0026lt;platform\u0026gt;:\u0026lt;chat_type\u0026gt;:\u0026lt;slack_scope_id\u0026gt;:\u0026lt;chat_id\u0026gt;[:\u0026lt;thread_id\u0026gt;] 群默认 thread 共享 同一个 thread 内所有用户共享 session（论坛/话题的预期 UX） 群按用户隔离 group_sessions_per_user=True 时 key 末尾追加 user_id Slack 例外 Slack 额外带 scope_id（workspace 标识）防止不同 workspace 同名 channel 撞 key。其他平台不引入这种 namespace。\n飞书 / 微信具体怎么填字段 微信（gateway/platforms/weixin.py）：\n走 iLink long-polling（EP_GET_UPDATES），不是 webhook（1371 行 _poll_loop） _guess_chat_type()（391 行）用 room_id 判群/单聊 群默认 group_sessions_per_user=True，每个发件人独立 session 1577 行 _text_batch_key：用 build_session_key 把同一 session 的连发文本合并成一条，避免 cache 反复 invalidate 飞书（适配器被打包/合并到 helpers.py，没有独立 platforms/feishu.py）：\n共用基类 platforms/base.py:6146 的 build_session_key 入口 群/单聊判定靠官方 message_type + chat_type（p2p / group） 平台 enum：gateway/config.py:349 Platform.FEISHU = \u0026quot;feishu\u0026quot; /resume 跨 routing key 复用 session switch_session() 把一个现有 session_id 绑到新的 routing key，turn_lease 同步 rebind（turn_lease.py:266 _rebind()）。但只支持 in-process，跨进程要走 hermes --session=\u0026lt;id\u0026gt; --continue 串行模式（见下文\u0026quot;已知边界\u0026quot;）。\n2. 存储层：SessionStore 怎么持久化 gateway/session.py 是一个 4233 行的 SessionStore，底层 SQLite + FTS5。\n入口合同 1 2 3 4 5 def get_or_create_session( source: SessionSource, force_new: bool = False, touch_activity: bool = True, ) -\u0026gt; SessionEntry 默认 force_new=False → 能复用就复用，绝不主动新建 force_new=True 的唯一调用点：slash_commands.py:292（/new / /reset） 一次访问的\u0026quot;single-flight\u0026quot;路径 session.py:2598 起：\n拿 inflight lock，按 session_key 检查是否有同 key 的进行中查询 有就 event.wait() 共享 owner 的结果（同一 routing key 的并发消息只产生一次 session 落库） 没有就调用 _get_or_create_session_impl() 真正读 / 写 store 不管成败都 event.set() 唤醒所有 follower Slack 跨 workspace 兼容迁移 session.py:2682–2712 有一段 Slack 老数据迁移：\n老 key 不带 scope_id，新 key 带 迁移策略（组合自 #20583/#66398/#68925）： 老 entry 记录的 workspace 明确 → 迁到匹配的新 key 老 entry scope-less 且 DM → 第一个 workspace 一次性认领（1:1 DM 一个人） 老 entry scope-less 且 channel/group → 拒绝迁移（跨 workspace 撞 key 会导致 transcript 串到错租户，正是这次修的 bug） 持久化跨重启 SQLite 落盘 + FTS5，gateway 重启历史不丢。\n3. 并发层：turn_lease 怎么避免交错写 存在理由写在 gateway/turn_lease.py 顶部 1–49 行：\nswitch_session()、/resume、CLI continuity rebind、async delegation 完成后 pin、telegram topic tip-walk——这些操作都让一个 session_id 被多个 routing key 指向。旧的 busy guard 按 routing key 加锁（adapter 的 _active_sessions、runner 的 _running_agents），但持久化 transcript 按 session_id 持有。当一个 session_id 被两条 routing key 同时指到，两个 turn 各自拿自己 routing key 的锁，谁都看不到对方——结果在 transcript 上交错写。\n怎么关 run.py:19396 附近：\n1 2 3 4 5 6 _lease_token = await _lease_registry.acquire( session_entry.session_id, # ← 锁的是解析后的 session_id owner_key=... generation=run_generation, timeout=_float_env(\u0026#34;HERMES_TURN_LEASE_TIMEOUT\u0026#34;, DEFAULT_LEASE_WAIT), ) 三个安全性质 性质 实现 Generation-scoped, identity-checked release token 带 owner_key + run_generation，释放时只认当前持有者（#28686 教训：老 turn 的 unwind 不能释放新 turn 的 lease） Fail-closed on timeout 超时抛 TurnLeaseTimeoutError → 必须拒掉消息并提示\u0026quot;请重发\u0026quot;，绝不并发跑 Bounded registry 字典上限 DEFAULT_MAX_LEASES=512；只能驱逐 idle entry，正在持有的永远不被驱逐 已知边界（注释自己承认的） CLI 进程是另一个 process，不在 in-process lock 里——CLI continuity 跨进程那一对要靠 DB-level lease 解决（#64934 还在跟） Mid-turn compression rotation 留了一个小 alias 窗口：tip-walk 切到新 child id 时 parent 的 lease 还在跑——后续要在 binding-sync 站点把 lease 别名过去 拿到 lease 后到写 transcript 之间的安全区 run.py:19419–19422：\n1 2 await self._mark_durable_active_turn(event, session_entry.session_key) history = await self.async_session_store.load_transcript(session_entry.session_id) 注释：\u0026quot;A turn only becomes durable recovery work after it owns the lease\u0026quot;——排队等 lease 的消息不算 \u0026ldquo;durable active\u0026rdquo;。如果 gateway 在等 lease 的瞬间被 kill，恢复时不会把这部分\u0026quot;还没真开始的 turn\u0026quot;当作要恢复的工作。\n4. 自动压缩：session hygiene run.py:19439–19460 起的 long session 自动压缩：\n阈值 值 为什么 触发条数 history and len(history) \u0026gt;= 4 至少 4 条才考虑 触发比例 0.85（不是 0.5） 比 agent 自带 compressor 阈值（0.5）高得多；hygiene 是兜底，agent 自带 compressor 在 tool loop 里用真实 token 计数更准 硬上限 _hyg_hard_msg_limit=5000 触发即压 总耗时上限 _hyg_total_ceiling_seconds=600.0 整个压缩总耗时不能超 10 分钟 失败冷却 _hyg_failure_cooldown_seconds=300.0 5 分钟内不再尝试，防止反复重试拖死 turn 压缩模型 anthropic/claude-sonnet-4.6 写死 hygiene 必须用稳定可复现的便宜模型压，不允许跟 user 当前模型走，否则 cache 又破 5. 什么时候会\u0026quot;开新 session\u0026quot; 主动：/new / /reset 只支持这两条命令，config.py:941 的 reset_triggers = [\u0026quot;/new\u0026quot;, \u0026quot;/reset\u0026quot;] 可配。\nslash_commands.py:144 _handle_reset_command() 实际做了 7 件事：\n_invalidate_session_run_generation(session_key, reason=\u0026quot;session_reset\u0026quot;)——让正在跑的 turn 后续解锁时不再有效 _release_running_agent_state(session_key)——清掉 _running_agents 槽位 关掉老 agent 的资源（terminal sandbox、browser daemon、后台子进程）——走 worker 线程 + asyncio.wait_for(timeout=_RESET_CLEANUP_TIMEOUT_S)，不能阻塞 event loop（#35994） 清所有 conversation-scoped 状态：model/reasoning 临时覆盖、one-turn restore、model note、cache、queue overflow 中断这个 session 在飞的 async 子任务（#55578）——reset 后老 session 的子 agent 回填无主 下次消息来时 get_or_create_session(force_new=True) 拿到新 session 发 reset 通知给用户（notify=True 且平台不是 api_server / webhook） 关键：/new 不是\u0026quot;立即生成一个新 session\u0026quot;，而是\u0026quot;拆掉当前的；下条消息来时再拿个新的\u0026quot;。老 transcript 仍然在 DB 里，只是 routing 不再指过去；这是 route 漂移，不是数据销毁。导出用 hermes_state_portability.py::export_session()。\n自动：session_reset 策略 config.py:554 起的 SessionResetPolicy，默认 mode=\u0026quot;none\u0026quot;（2026-07 后改的；之前是 \u0026ldquo;both\u0026rdquo;——24h idle + 每天 4am 强清，抱怨\u0026quot;对话怎么没了\u0026quot;后改的）：\nmode 触发条件 默认推荐 \u0026quot;daily\u0026quot; 每天本地时区 at_hour（默认 4 点） 不推荐给个人 \u0026quot;idle\u0026quot; 距上次活动 idle_minutes（默认 1440 = 24h） 企业 / SRE 场景 \u0026quot;both\u0026quot; 上面两个哪个先到 极端保守 \u0026quot;none\u0026quot; 永不自动 reset 当前默认 完整配置：\n1 2 3 4 5 6 7 session_reset: mode: \u0026#34;idle\u0026#34; # 或 daily / both / none idle_minutes: 1440 at_hour: 4 notify: true # 触发时给你发一条通知 notify_exclude_platforms: [\u0026#34;api_server\u0026#34;, \u0026#34;webhook\u0026#34;] bg_process_max_age_hours: 24 # 后台子进程超过这个年龄不再 pin 住 session bg_process_max_age_hours 防 #29177：开了个长期 devbox，session 一直显示\u0026quot;有进程在跑\u0026quot;而不让 reset。\n6. iLink long-polling 入口的特殊性（微信专属） gateway/platforms/weixin.py：\n端点 EP_GET_UPDATES = \u0026quot;ilink/bot/getupdates\u0026quot;（97 行） _poll_loop()（1371 行）：hold 连接最多 LONG_POLL_TIMEOUT_MS 毫秒，server 端有消息立刻 push 回来；response 的 suggested_timeout_ms 还能动态调整 拿到 msgs[] 后每条独立 asyncio.create_task(self._process_message_safe(message))（1421 行）——同批次 N 条消息并行进 dispatcher，这正是 lease 存在的第一个原因 _process_message() 里三层去重： message_id 去重（iLink 偶发重发，1476 行） 内容 MD5 二次去重（整条重投兜底，1482 行） 群/单聊 + 黑白名单（1488–1497 行） 文本消息先入 _enqueue_text_event() 走 batch 合并（1577–1610 行）——避免 cache 反复 invalidate 7. 一图：iLink webhook → transcript 完整链路 flowchart TD LP[\"iLink long-poll msgs[]\"] --\u003e PM[(\"_process_message()\n_guess_chat_type / 内容去重\")] FE[(\"飞书/Telegram webhook event\")] --\u003e PM PM --\u003e ME[\"MessageEvent(source=SessionSource)\"] ME --\u003e HM[\"handle_message() in base.py\"] HM --\u003e BSK[\"build_session_key()\nsingle source of truth\"] BSK --\u003e GUARD[\"_heal_stale_lock\n_active_sessions 检查\nshould_bypass_active_session\"] GUARD --\u003e HND[\"_handle_message() in run.py\"] HND --\u003e GOC[\"SessionStore.get_or_create_session(source)\nSQLite, FTS5, 跨重启持久\"] GOC --\u003e LEASE[\"_lease_registry.acquire(session_id, owner_key, gen)\n第二层锁，锁的是 session_id\"] LEASE --\u003e MARK[\"_mark_durable_active_turn\n此时才'算'真开始\"] MARK --\u003e LOAD[\"load_transcript(session_id)\"] LOAD --\u003e HYG[\"session hygiene\nauto-compress 阈值 0.85 / 5000 msgs\"] HYG --\u003e RUN[\"agent.run + flush_transcript\"] RUN --\u003e REL[\"_release_turn_lease / _rebind_turn_lease\"] REL --\u003e SEND[\"adapter.send() → 微信/飞书\"] 8. 用户行为 → 内部机制对照表 现象 原因 同一 DM 反复聊，AI 记得之前的话 同一 key → 同一 session_id → 同一 transcript row 群里两个人 @ AI，AI 不会把 A 的话当成 B 说的 群模式默认 group_sessions_per_user=True，key 里带 user_id 微信连发 3 条短消息，AI 看到 1 条合并消息 微信 adapter 的 _enqueue_text_event + _text_batch_key 飞书/微信同机器上同时用 = 各是各的 session Platform.WEIXIN=\u0026quot;weixin\u0026quot; vs Platform.FEISHU=\u0026quot;feishu\u0026quot; /approve /deny /stop 不会被合并到对话里 should_bypass_active_session() 在 base.py:6189 直派 偶尔 agent 让你\u0026quot;请重发\u0026quot; _lease_registry.acquire(timeout=...) 抛 TurnLeaseTimeoutError，dispatch 返回 \u0026ldquo;bounded rejection/resend notice\u0026rdquo; 长聊了几个月后某天突然自己\u0026quot;瘦身\u0026quot; session hygiene 在 turn 入口按 0.85 阈值自动压 9. 跨入口混用：飞书 ↔ 终端 CLI 的实际行为 Hermes 自己承认（turn_lease.py 顶部注释）CLI 进程不在 in-process lock 里，跨进程 session 复用是 known limit。\n三种情况 场景 行为 安全吗 同一个 hermes-agent 进程（gateway 模式 + 终端 attach） platform 维度不同，key 不同 → 不会合流 ✅ 安全 独立 CLI 进程（hermes / pi / codex），跨进程共享 session_id 没有跨进程锁；两进程各自写，顺序按 flush 时间而非打字时间 ⚠️ 串扰 用 /resume 跨 routing key 复用 session in-process 安全，lease 同步 rebind ✅ 但只限 in-process 跨进程会出现的具体问题 Prompt cache 反复失效——下次问\u0026quot;刚才我们说到哪了\u0026quot;答得很糟 identity-marker 错位——repair_message_sequence 触发，悄悄删/合并行，飞书里可能看到自己\u0026quot;说过\u0026quot;但其实没在飞书说的话 async delegation 跨进程悬挂（#55578 姊妹问题）——subagent 跑完找不到 owner，token 烧了结果不归你 持久 ledger 的 session_key 漂移——shutdown_flush.py:359 那个 session_key 字段记录的是 routing key 不是 session_id 推荐用法 把终端和飞书当成\u0026quot;两种入口\u0026quot;而不是\u0026quot;同时入口\u0026quot; 想要\u0026quot;在 A 上面接到 B 继续\u0026quot;：主动 /new 或 /resume 显式断；/resume 同进程安全；跨进程要走 hermes --session=\u0026lt;id\u0026gt; --continue 这种单进程串行模式 真要两边都用：当前 Hermes 没给你一个干净的并发方案 10. 关键源码定位 主题 文件:行 session key 构造 gateway/session.py:1090 DM 跨用户隔离规则 gateway/session.py:1151–1154 Slack scope 迁移 gateway/session.py:2682–2712 SessionStore get_or_create gateway/session.py:2598 单飞行复用 gateway/session.py:2619–2651 Platform enum gateway/config.py:325 微信 iLink long-poll gateway/platforms/weixin.py:1371 _poll_loop 微信文本 batch gateway/platforms/weixin.py:1577 _text_batch_key 微信群/单聊判定 gateway/platforms/weixin.py:391 _guess_chat_type 通用 handle_message gateway/platforms/base.py:6146 turn_lease 实现 gateway/turn_lease.py lease acquire gateway/run.py:19396 durable_active_turn 标记 gateway/run.py:19419 session hygiene gateway/run.py:19439–19460 /new / /reset 处理 gateway/slash_commands.py:144 _handle_reset_command reset_triggers 配置 gateway/config.py:941 自动 reset 策略 gateway/config.py:554 SessionResetPolicy 平台 prompt 块（feishu/weixin） agent/prompt_builder.py:1093, 1101 11. 还没挖但值得看的几个点 _quick_key 跟 session_key 区别——为什么 routing 用 _quick_key、锁 session 用 session_id repair_message_sequence 什么时候触发、修了什么 hermes --session=\u0026lt;id\u0026gt; --continue CLI 串行模式具体怎么用 prompt cache 怎么跨 turn 保持稳定（prompt_builder.py 跟 system prompt 的字节稳定性） 微信多账号 iLink 在 key 算法下是否会跨账号串 session（当前算法不会，因为 account_id 不参与 key——这是隐患） 12. 我自己用这套的实践建议 默认 mode: \u0026quot;none\u0026quot;——别开自动 reset，长 session 是价值 飞书/微信之外，CLI 同一时刻只在一个进程跑——避免跨进程 session 漂移 想\u0026quot;换入口继续\u0026quot;，先 /new 再切——把责任显式化，不要靠\u0026quot;它会自动理解\u0026quot; 长 session（\u0026gt;1000 条）观察 hygiene 是否会触发——超过 0.85 阈值时它会自动压，但阈值是 len(history) \u0026gt;= 4 才考虑，小 session 别担心 修改 routing key 维度配置（group_sessions_per_user、thread_sessions_per_user）前先 export_session 老数据——改完之后 key 变了，老 session 就够不着了 ","permalink":"https://kerolt.github.io/p/hermes-session-%E7%AE%A1%E7%90%86%E9%A3%9E%E4%B9%A6-%E5%BE%AE%E4%BF%A1-cli-%E7%BB%9F%E4%B8%80%E4%BC%9A%E8%AF%9D%E6%A8%A1%E5%9E%8B/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003eHermes 把 CLI、TUI、桌面 app、几十种 messaging 平台（飞书/微信/Telegram/Discord/Slack…）接到同一份\u0026quot;agent core\u0026quot;上。理解它的 session 模型，是理解为什么\u0026quot;我换个入口跟它说话\u0026quot;和\u0026quot;我在同个地方继续说\u0026quot;行为完全不同的关键。\u003c/p\u003e","title":"Hermes Session 管理：飞书/微信/CLI 统一会话模型"},{"content":"核心结论 Hugo 的 content 目录不是简单的 Markdown 文件仓库，而是一棵 Page Tree。目录层级和特殊文件名会决定页面的 Kind、Section、Type、Bundle 类型、模板选择、资源归属以及默认 URL。\n设计 content 结构时，不应只考虑最终 URL，而要先判断当前节点是一个具体内容页，还是一个可以包含其他页面的集合。\n1 2 3 4 5 具体页面、没有子页面 → 普通 Markdown 文件或 index.md 集合页面、允许包含子页面 → _index.md Hugo 的五种 Page Kind Kind 表示页面在站点内容模型中的结构角色。它主要由文件位置和 Hugo 内置机制决定，不是一个通常应在 Front Matter 中随意指定的展示参数。\nKind 典型内容路径 含义 常用模板类型 home content/_index.md 站点根页面 home.html page content/about.md 或 content/about/index.md 普通内容页 single.html、page.html section content/post/ 或 content/post/_index.md 内容集合 section.html、list.html taxonomy content/tags/_index.md 某个 Taxonomy 的 Term 总览 taxonomy.html、list.html term content/tags/hugo/_index.md 某个具体 Term 的内容列表 term.html、list.html 在模板中可以通过以下字段检查当前页面：\n1 2 3 4 5 6 Kind={{ .Kind }} Type={{ .Type }} Section={{ .Section }} Layout={{ .Layout }} BundleType={{ .BundleType }} RelPermalink={{ .RelPermalink }} 普通文件、index.md 与 _index.md 这三种形式最容易混淆。\n普通 Markdown 文件 1 content/about.md 它表示一个普通页面，适合没有同目录资源的简单内容。\n1 2 Kind = page BundleType = 空 index.md：Leaf Bundle 1 2 3 4 content/about/ ├── index.md ├── avatar.jpg └── resume.pdf index.md 表示当前目录是一篇具体页面，同时让同目录图片、视频和附件成为该页面的 Page Resources。\n1 2 Kind = page BundleType = leaf 模板可以通过 .Resources 获取这些资源：\n1 2 3 {{ with .Resources.Get \u0026#34;avatar.jpg\u0026#34; }} \u0026lt;img src=\u0026#34;{{ .RelPermalink }}\u0026#34; alt=\u0026#34;Avatar\u0026#34;\u0026gt; {{ end }} Leaf Bundle 是内容树的叶子，不应该再包含需要独立渲染的子页面。目录中的其他 Markdown 文件通常会成为 Page Resource，而不是独立 Page。\n_index.md：Branch Bundle 1 2 3 4 content/docs/ ├── _index.md ├── install.md └── configuration.md _index.md 表示当前目录是一个集合节点，可以拥有子页面和子 Section。\n1 2 Kind = section BundleType = branch 可以把两者记成：\n1 2 3 4 5 index.md = 当前目录就是一篇内容，到这里结束 _index.md = 当前目录是一个分支，下面还可以挂其他内容 Section 如何形成 content 下的顶层内容目录天然是 Root Section。例如：\n1 2 3 4 5 6 7 content/post/ ├── Go/ │ └── defer.md ├── Linux/ │ └── 零拷贝技术.md └── C++/ └── lambda.md 这些文章的 Root Section 都是 post：\n1 2 Section = post Type = post Languages/Go、Systems/Linux 和 Languages/C++ 等中间目录如果没有 _index.md，通常只是文件组织和 URL 路径的一部分，不是具有独立列表页的 Section。如果需要让某个目录成为嵌套 Section，应为它创建 _index.md。\n1 content/post/Languages/Go/_index.md 此时 /post/go/ 才具有明确的 Section Page，并能拥有自己的标题、描述、菜单和子页面集合。\nKind、Type、Section 与 Layout 的区别 这几个概念属于不同维度。\n字段 回答的问题 主要来源 Kind 这是普通页、Section 还是 Taxonomy？ 内容树结构和 Hugo 内置机制 Section 这个页面属于哪个 Root Section？ content 下的顶层 Section Type 应优先使用哪一组类型模板？ Front Matter 的 type，否则通常来自 Root Section Layout 在当前 Type 中选择哪个具体模板？ Front Matter 的 layout Slug URL 最后一个路径段是什么？ Front Matter 或文件名 例如 Stack 的搜索页：\n1 content/page/search/index.md Front Matter：\n1 2 3 4 5 6 7 --- title: 搜索 layout: search outputs: - html - json --- 对应的核心属性为：\n1 2 3 Kind = page Type = page Layout = search 于是可以匹配 Stack 的：\n1 2 layouts/page/search.html layouts/page/search.json 如果移动页面后 Type 不再符合主题约定，可以在 Front Matter 中显式设置：\n1 2 type: page layout: search layout 不是全局模板开关，它会与 Kind、Type、语言和 Output Format 一起参与 Template Lookup。\n每类 Markdown 文件的 Kind、Type 和 Section Kind、Type 和 Section 不是三个需要在每篇 Front Matter 中同时手工填写的字段。Hugo 会先根据内容路径和内置页面类型确定 Kind 与 Section，再根据 Front Matter 或 Root Section 推导 Type。\n可以按照下面的顺序理解：\nHugo 先判断当前文件是否对应首页、普通页面、Section、Taxonomy 根页或 Term 页，由此得到 Kind。 Hugo 根据页面所在的顶层内容目录确定 Section；首页没有 Section，Taxonomy 页面使用 Taxonomy 的 plural 名称。 如果 Front Matter 显式设置了 type，Hugo将其作为 Type；否则普通内容通常继承 Root Section 名称，根目录普通页和首页默认使用 page，Taxonomy 与 Term 默认使用 Taxonomy 的 plural 名称。 layout、slug、url、outputs 和 menu 不负责设置这三个字段，它们分别影响模板选择、发布地址、输出格式和菜单。 当前站点主要文件的实际值如下：\nMarkdown 文件 页面角色 Kind Type Section content/_index.md 站点首页 home page 空字符串 content/page/about/index.md 关于我普通页面 page page page content/page/archives/index.md 归档普通页面 page page page content/page/search/index.md 搜索普通页面 page page page content/tags/_index.md Tag Taxonomy 根页 taxonomy tags tags content/categories/_index.md Category Taxonomy 根页 taxonomy categories categories content/post/Engineering/Hugo/Hugo-content目录的页面类型与组织方式.md 普通博客文章 page post post content/post/Engineering/Hugo/example/index.md Leaf Bundle 形式的博客文章 page post post content/post/_index.md post Root Section 元信息 section post post content/post/Languages/Go/_index.md post 下的嵌套 Section 元信息 section post post content/tags/hugo/_index.md hugo Tag Term 元信息 term tags tags content/categories/misc/_index.md misc Category Term 元信息 term categories categories 表中 content/post/Engineering/Hugo/example/index.md、content/post/_index.md、content/post/Languages/Go/_index.md、content/tags/hugo/_index.md 和 content/categories/misc/_index.md 用于说明对应结构，当前站点不一定已经创建这些具体示例文件。\n首页 content 根目录的 _index.md 是 Hugo 站点首页的元信息文件，因此其属性为：\n1 2 3 Kind = home Type = page Section = \u0026#34;\u0026#34; 这里的 _index.md 不会得到 Kind=section，因为 content 根节点在 Hugo 页面模型中具有特殊的首页身份。它当前设置的 menu 只负责把首页加入主菜单，不影响 Kind、Type 或 Section。\n关于我 “关于我”使用 content/page/about/index.md，因此 content/page/about/ 是一个 Leaf Bundle，当前节点是一篇具体页面：\n1 2 3 4 Kind = page Type = page Section = page Layout = about Kind=page 来自 index.md 的 Leaf Page 语义，Section=page 来自顶层目录 content/page。由于 Front Matter 没有显式设置 type，Type 默认继承 Root Section，因此也是 page。\n当前 Front Matter 中的字段分别负责：\n1 2 3 4 layout: about # 在 Type=page 的候选模板中优先查找 about 布局 slug: about # 配合 page permalink 生成 /about/ toc: false # 关闭当前页面的 TOC menu: # 将页面加入主菜单 这些字段都不会改变 Kind=page、Type=page、Section=page。\n归档 归档页同样是位于 page Root Section 下的 Leaf Page，content/page/archives/index.md：\n1 2 3 4 Kind = page Type = page Section = page Layout = archives layout: archives 使它选择 Stack 的归档布局，归档数据由该布局读取并组织。页面仍然是普通 page，并不存在 archives 这种 Hugo 内置 Kind。\nslug: archives 只参与 /archives/ URL 的生成，不会把 Type 或 Section 设置为 archives。\n搜索 搜索页 content/page/search/index.md 也是 Leaf Page：\n1 2 3 4 Kind = page Type = page Section = page Layout = search 它的特殊之处不在 Kind，而在自定义布局和 Output Format：\n1 2 3 4 layout: search outputs: - html - json layout: search 使 HTML 输出匹配 Stack 的 layouts/page/search.html，JSON 输出匹配对应的搜索数据模板。outputs 只决定同一个 Page 需要生成哪些输出格式，不会创建新的 Kind，也不会改变 Type 或 Section。\nTag Taxonomy 根页 Hugo 默认配置中存在 tag = \u0026quot;tags\u0026quot;，因此逻辑路径 /tags 被识别为 Tag Taxonomy 根节点 content/tags/_index.md：\n1 2 3 4 Kind = taxonomy Type = tags Section = tags Layout = tags 这里的 Kind=taxonomy 不是单独由 _index.md 决定的，而是由“启用了 plural 名称为 tags 的 Taxonomy”与“文件位于 content/tags/_index.md”共同决定。_index.md 为 Hugo 自动生成的 Taxonomy Page 补充标题、菜单等元信息。\n如果没有这个文件，只要 Tag Taxonomy 已启用并且文章使用了 tags，Hugo 仍然可以生成 /tags/。缺少的只是自定义标题、菜单和其他页面元信息。\nCategory Taxonomy 根页 Hugo 默认配置中存在 category = \u0026quot;categories\u0026quot;，因此该文件对应 Category Taxonomy 根页面 content/categories/_index.md：\n1 2 3 4 Kind = taxonomy Type = categories Section = categories Layout = categories Type 和 Section 使用 plural 名称 categories，而不是 singular 名称 category。文章 Front Matter 也使用 plural 字段建立关系：\n1 2 categories: - Misc 当前文件中的 layout: categories、slug: categories 和 menu 都不会创建 Taxonomy；真正让页面获得 Kind=taxonomy 的是 Hugo Taxonomy 配置和逻辑路径。\n普通博客文章 普通博客文章自身位于：\n1 content/post/.../*.md 它是普通 Markdown 内容文件，并且位于 post Root Section 下，因此：\n1 2 3 Kind = page Type = post Section = post 中间目录 Misc 不会把 Type 或 Section 改为 Misc。它当前只是文件组织和 URL 逻辑路径的一部分；文章的 categories: [Misc] 是独立的 Taxonomy 关系，也不会改变 Section。\n普通文件与 Leaf Bundle 在这三个值上可以完全相同：\n1 2 content/post/Engineering/Hugo/example.md content/post/Engineering/Hugo/example/index.md 二者通常都是：\n1 2 3 Kind = page Type = post Section = post 区别在于后者是 Leaf Bundle，可以把同目录图片和附件作为 Page Resources 管理，而不是 Kind、Type 或 Section 不同。\npost 目录下的 _index.md 如果创建：\n1 content/post/_index.md 它会成为 post Root Section 的元信息页面：\n1 2 3 Kind = section Type = post Section = post 如果进一步创建：\n1 content/post/Languages/Go/_index.md Go 会成为嵌套 Section，但 .Section 仍然返回 Root Section post：\n1 2 3 Kind = section Type = post Section = post 需要获得当前嵌套 Section Page 时应使用 .CurrentSection，不能只依赖 .Section 区分 post/Languages/Go 与 post/System/Linux。\nTag Term 页 如果为具体 Tag 创建元信息文件：\n1 content/tags/hugo/_index.md 它对应 /tags/hugo/，属性为：\n1 2 3 Kind = term Type = tags Section = tags Kind=term 表示它是一个具体 Term 的文章集合，.Pages 通常是所有声明了 tags: [Hugo] 的文章。Type 与 Section 仍然使用 Taxonomy plural 名称 tags，而不是 Term 名称 hugo。\nCategory Term 采用相同规则：\n1 2 3 4 5 content/categories/misc/_index.md Kind = term Type = categories Section = categories Front Matter 能修改哪些值 三个字段的可控方式并不相同：\n字段 能否通过 Front Matter 修改 正确控制方式 Kind 不能直接修改 调整文件形态、内容路径，或者启用相应 Hugo 内置机制 Type 可以 设置 type；未设置时由 Root Section 等信息推导 Section 不能直接修改 调整文件所在的 Root Section 或使用 Hugo Module mount 例如在“关于我”页面设置：\n1 type: landing 结果会变成：\n1 2 3 Kind = page Type = landing Section = page 只有 Type 被覆盖，页面仍然是普通 Page，也仍然位于 page Section。\n类似地，下面这些字段都不能改变 Page Kind：\n1 2 3 layout: taxonomy slug: tags url: /tags/ 它们可以改变模板候选或发布 URL，但不能把 content/page/tags/index.md 转换为 Taxonomy Page。\n把 Taxonomy 文件移动到 page 后会发生什么 正确位置：\n1 2 3 4 5 content/tags/_index.md Kind = taxonomy Type = tags Section = tags 如果移动并改成：\n1 2 3 4 5 content/page/tags/index.md Kind = page Type = page Section = page 如果移动后仍使用 _index.md：\n1 2 3 4 5 content/page/tags/_index.md Kind = section Type = page Section = page 即使通过 slug、url 或 permalink 让它们最终发布到 /tags/，它们也不会恢复 Kind=taxonomy。与此同时，Hugo 仍会为真正的 Tag Taxonomy 生成 /tags/，从而产生重复输出路径。因此 Taxonomy 元信息文件应保持在 content/tags/_index.md 和 content/categories/_index.md。\n在模板中验证实际值 判断页面类型时应直接查看 Hugo Page 对象，而不是根据浏览器地址猜测。可以临时在模板中输出：\n1 2 3 4 5 6 7 8 \u0026lt;pre\u0026gt; Kind={{ .Kind }} Type={{ .Type }} Section={{ printf \u0026#34;%q\u0026#34; .Section }} Layout={{ .Layout }} BundleType={{ .BundleType }} RelPermalink={{ .RelPermalink }} \u0026lt;/pre\u0026gt; 其中 printf \u0026quot;%q\u0026quot; 可以明确显示首页的空 Section。当前站点还可以使用 hugo list all 查看内容文件的 kind、section 和最终 permalink，再结合 .Type 的推导规则定位模板匹配问题。\n搜索页为什么必须使用 index.md 正确结构是：\n1 content/page/search/index.md 如果误写成：\n1 content/page/search/_index.md 搜索节点会从普通页面变成 Section：\n1 2 Kind=page → Kind=section single 模板语义 → list 模板语义 Stack 的搜索功能依赖 layouts/page/search.html 和 layouts/page/search.json，还需要加载 search.tsx。一旦页面变成 Section，模板查找方向就会改变，最终可能只得到空白列表页或无法生成正确的 JSON 搜索索引。\nTaxonomy 页面为什么使用 _index.md Hugo 默认提供：\n1 2 3 [taxonomies] category = \u0026#34;categories\u0026#34; tag = \u0026#34;tags\u0026#34; 文章通过 plural 字段关联 Term：\n1 2 3 4 5 categories: - 博客 tags: - Hugo - Stack Hugo 会自动生成：\n1 2 3 4 5 /categories/ /categories/博客/ /tags/ /tags/hugo/ /tags/stack/ 对应的可选元信息文件是：\n1 2 3 4 5 6 7 8 content/categories/_index.md → 分类 Taxonomy 根页 content/tags/_index.md → 标签 Taxonomy 根页 content/tags/hugo/_index.md → Hugo Term 页 这些节点都是集合，所以使用 _index.md。文件负责补充标题、描述、封面和菜单，真正的文章归属关系来自每篇文章的 Front Matter。\n如果把 content/tags/_index.md 写成 content/tags/index.md，就会把 /tags/ 声明成普通叶子 Page，与 Hugo 自动生成的 Taxonomy 根页发生内容模型冲突。\n内容路径与最终 URL 可以分离 内容文件所在目录不会绝对决定最终 URL。Hugo 还会考虑 slug、url 和站点的 permalinks 配置。\n当前站点使用：\n1 2 3 [permalinks] post = \u0026#34;/p/:slug/\u0026#34; page = \u0026#34;/:slug/\u0026#34; 因此：\n1 2 3 4 5 content/post/System/Linux/零拷贝技术.md → /p/零拷贝技术/ content/page/search/index.md → /search/ 这说明目录主要用于表达内容模型和模板类型，而 permalink 可以独立控制发布路径。不要为了缩短 URL 随意移动内容目录，应优先使用 permalink 或 url 配置。\nStack 主题下的推荐结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 content/ ├── _index.md ├── post/ │ ├── _index.md # 可选，post Section 元信息 │ ├── Go/ │ │ └── defer.md │ └── Hugo/ │ └── page-bundle/ │ ├── index.md │ └── cover.png ├── page/ │ ├── about/ │ │ └── index.md │ ├── archives/ │ │ └── index.md │ └── search/ │ └── index.md ├── categories/ │ └── _index.md └── tags/ ├── _index.md └── hugo/ └── _index.md 这里的职责划分是：\n路径 职责 content/post 首页、归档和 RSS 中的主要文章 content/page 搜索、关于、归档等独立功能页 content/categories Category Taxonomy 元信息 content/tags Tag Taxonomy 和 Term 元信息 Stack 通过 mainSections = [\u0026quot;post\u0026quot;] 识别主要文章，因此普通功能页不会混入首页文章列表和搜索索引。\n组织页面时的判断顺序 新增页面前可以依次判断：\n这是具体内容还是集合？具体内容使用 foo.md 或 index.md，集合使用 _index.md。 是否需要与页面绑定图片和附件？需要时使用 Leaf Bundle，即 目录/index.md。 是否需要子页面和独立列表页？需要时使用 Branch Bundle，即 目录/_index.md。 它属于哪个业务类型？博客文章放在 post，功能页放在 page，不要只根据 URL 选择目录。 是否属于 Taxonomy？标签和分类关系写入文章 Front Matter，content/tags 与 content/categories 只保存集合元信息。 主题模板位于哪个 Type 目录？例如 layouts/page/search.html 要求搜索页以 Type=page 参与查找。 最终 URL 是否符合需求？不符合时优先调整 permalink、slug 或 url，不要先破坏内容模型。 常见错误 错误 后果 正确方式 用 _index.md 创建普通功能页 页面变成 Section，使用 list 语义 改为 index.md 用 index.md 创建标签根页 普通 Page 与 Taxonomy 根页冲突 改为 tags/_index.md 为了修改 URL 随意移动顶层目录 Type 和模板匹配可能改变 配置 permalink 或显式 type 认为 layout 可以脱离 Type 独立工作 找不到主题中的类型模板 同时检查 Kind、Type 和 Layout 在 Leaf Bundle 中放独立 Markdown 子页面 子 Markdown 可能变成 Page Resource 改用 Branch Bundle 手工维护标签文章列表 与 Hugo 自动 Taxonomy 重复 在文章 Front Matter 中声明 tags 参考资料 Hugo Page Bundles Hugo Sections Hugo Taxonomies Hugo Template Lookup Order ","permalink":"https://kerolt.github.io/p/hugo-content-%E7%9B%AE%E5%BD%95%E7%9A%84%E9%A1%B5%E9%9D%A2%E7%B1%BB%E5%9E%8B%E4%B8%8E%E7%BB%84%E7%BB%87%E6%96%B9%E5%BC%8F/","summary":"\u003ch2 id=\"核心结论\"\u003e核心结论\u003c/h2\u003e\n\u003cp\u003eHugo 的 \u003ccode\u003econtent\u003c/code\u003e 目录不是简单的 Markdown 文件仓库，而是一棵 Page Tree。目录层级和特殊文件名会决定页面的 \u003ccode\u003eKind\u003c/code\u003e、\u003ccode\u003eSection\u003c/code\u003e、\u003ccode\u003eType\u003c/code\u003e、Bundle 类型、模板选择、资源归属以及默认 URL。\u003c/p\u003e","title":"Hugo content 目录的页面类型与组织方式"},{"content":"核心结论 Stack 没有重新实现 Taxonomy 系统。Hugo 负责读取文章 Front Matter、建立 Taxonomy 与 Term 的关联、生成页面对象和 URL；Stack 负责调用 Hugo 提供的 .Site.Taxonomies、.GetTerms 和 .Pages，将这些数据展示为分类徽章、文章标签、侧栏 Widget、归档入口以及列表页面。\n可以把整体流程理解为：\n1 2 3 4 5 6 7 8 9 10 11 12 文章 Front Matter │ ▼ Hugo 建立 Taxonomy 数据 │ ├── 生成 Taxonomy 根页面 ├── 生成 Term 页面 ├── 建立 Term 与文章的关联 └── 提供 .Site.Taxonomies、.GetTerms、.Pages │ ▼ Stack 负责选择模板和渲染界面 Hugo 默认的 Taxonomy 在没有显式配置 [taxonomies] 时，Hugo 默认启用 Category 和 Tag：\n1 2 3 [taxonomies] category = \u0026#34;categories\u0026#34; tag = \u0026#34;tags\u0026#34; 左侧的 category 和 tag 是 Taxonomy 的 singular 名称，右侧的 categories 和 tags 是 plural 名称。文章 Front Matter 使用 plural 字段声明关联：\n1 2 3 4 5 categories: - Hugo tags: - Stack - Theme Hugo 构建后会生成相应的页面对象和默认路径：\n页面 Page Kind 含义 /categories/ taxonomy 所有 Category Term 的集合 /categories/hugo/ term 属于 Hugo Category 的文章集合 /tags/ taxonomy 所有 Tag Term 的集合 /tags/stack/ term 带有 Stack Tag 的文章集合 这些页面的数据来自文章 Front Matter，而不是来自 content/categories 或 content/tags 目录。\nHugo 与 Stack 的职责边界 能力 Hugo Stack 解析文章中的 categories 和 tags 负责 不负责 建立 Term 与文章的关联 负责 不负责 生成 Taxonomy 和 Term Page 负责 不负责 生成默认 Taxonomy URL 负责 不负责 选择最终模板 按 Template Lookup 执行 提供候选模板 展示分类徽章和标签 提供数据 负责 展示分类和标签 Widget 提供 .Site.Taxonomies 负责 设计分类颜色、卡片和布局 不负责 负责 因此，Stack 不会扫描文章并自行统计标签。它只是消费 Hugo 构建完成的内容模型。\nStack 如何渲染分类 Stack 在文章详情组件中通过下面的方式读取当前文章的分类：\n1 {{ range $Page.GetTerms \u0026#34;categories\u0026#34; }} 对应源码为：\n1 theme/hugo-theme-stack/layouts/_partials/article/components/details.html .GetTerms \u0026quot;categories\u0026quot; 返回的不是普通字符串，而是当前文章关联的 Category Term Page。Stack 可以直接读取每个 Term 的 .LinkTitle、.RelPermalink 和 .Params，将分类渲染成指向 /categories/\u0026lt;term\u0026gt;/ 的徽章。\n分类徽章默认根据 Term 名称计算颜色。分类 Term 的 Front Matter 也可以通过 style.background 和 style.color 覆盖默认颜色：\n1 2 3 4 5 6 --- title: Hugo style: background: \u0026#34;#2a9d8f\u0026#34; color: \u0026#34;#fff\u0026#34; --- Stack 提供的 Category archetype 也包含这些字段：\n1 theme/hugo-theme-stack/archetypes/categories.md Stack 如何渲染标签 文章详情页通过下面的调用读取标签：\n1 {{ range $Page.GetTerms \u0026#34;tags\u0026#34; }} 对应源码为：\n1 theme/hugo-theme-stack/layouts/_partials/article/components/tags.html Stack 通常把标签显示在文章正文后面。文章列表中的标签是可选内容，由 article.list.showTags 控制：\n1 2 [article.list] showTags = true Stack 对 Category 和 Tag 的定位并不完全相同。Category 更接近文章的主要归类，会显示为醒目的彩色徽章；Tag 更接近辅助检索信息，通常显示在文章底部或标签云中。这是主题的界面设计差异，不是 Hugo 数据模型上的能力差异。\nTaxonomy 和 Term 页面如何选择模板 Stack 没有分别为 categories 和 tags 实现一套完整的专用列表模板。Hugo 按照 Template Lookup Order 查找模板后，通常会回退到 Stack 的通用列表模板：\n1 theme/hugo-theme-stack/layouts/list.html Stack 的页面筛选辅助模板明确处理 section、taxonomy 和 term：\n1 {{ if in (slice \u0026#34;section\u0026#34; \u0026#34;taxonomy\u0026#34; \u0026#34;term\u0026#34;) .Kind }} 对应源码为：\n1 theme/hugo-theme-stack/layouts/_partials/helper/pages.html 不同 Page Kind 进入同一套列表界面后，数据含义不同：\n1 2 3 4 5 6 7 /tags/ Kind = taxonomy .Pages = 所有 Tag Term Page /tags/hugo/ Kind = term .Pages = 所有带有 hugo 标签的文章 Stack 的 list.html 再统一负责标题卡片、描述、封面、子项、文章列表和分页等界面。\ncontent/tags 和 content/categories 的作用 推荐的可选元信息结构是：\n1 2 3 4 5 6 7 8 9 content/ ├── categories/ │ ├── _index.md │ └── hugo/ │ └── _index.md └── tags/ ├── _index.md └── stack/ └── _index.md 各文件的作用如下：\n文件 补充元信息的页面 content/categories/_index.md /categories/ Taxonomy 根页面 content/categories/hugo/_index.md /categories/hugo/ Term 页面 content/tags/_index.md /tags/ Taxonomy 根页面 content/tags/stack/_index.md /tags/stack/ Term 页面 这些 _index.md 可以设置 title、description、image、菜单信息以及分类样式，但不会创建文章关联。即使不存在这些文件，只要 Taxonomy 已启用且文章声明了相应 Term，Hugo 仍能创建 Taxonomy 和 Term 页面。\n反过来，仅创建下面的文件并不会自动让文章拥有 Stack 标签：\n1 content/tags/stack/_index.md 文章仍然需要显式声明：\n1 2 tags: - Stack 为什么必须使用 _index.md Taxonomy 根页面和 Term 页面都是集合节点，因此对应的元信息文件应使用 _index.md。例如：\n1 content/tags/_index.md 如果改成：\n1 content/tags/index.md 这个目录会被声明为 Leaf Bundle，也就是普通内容 Page，可能与 Hugo 自动生成的 /tags/ Taxonomy Page 产生内容模型和模板查找冲突。\n同理，把标签页放到下面的位置：\n1 content/page/tags/index.md 创建的是 Kind=page、Type=page 的普通功能页面，不是 Hugo 的 Kind=taxonomy 页面。即使最终 URL 被 slug 或 permalink 改成 /tags/，它在内容模型中仍然不是 Tag Taxonomy 根节点。\nlayout: tags 也不能把普通 Page 转换成 Taxonomy。layout 只参与模板选择，页面的 Kind 仍由内容结构和 Hugo 的 Taxonomy 机制决定。\n侧栏 Widget 如何读取 Taxonomy 当前站点启用了 Category 和 Tag Cloud Widget：\n1 2 3 4 5 6 [widgets] homepage = [ {type = \u0026#34;search\u0026#34;}, {type = \u0026#34;categories\u0026#34;, params = {limit = 15}}, {type = \u0026#34;tag-cloud\u0026#34;, params = {limit = 20}}, ] Category Widget 和 Tag Cloud Widget 最终都会调用 Stack 的通用 Taxonomy Widget：\n1 theme/hugo-theme-stack/layouts/_partials/widget/taxonomy.html 核心读取逻辑是：\n1 {{ $taxonomy := index $context.Site.Taxonomies .Params.taxonomy }} 然后通过 $taxonomy.ByCount 按文章数量排列 Term。因此，Widget 的统计数据来自 .Site.Taxonomies，并不依赖 content/tags/_index.md 或 content/categories/_index.md。\n归档页对 Category 的特殊处理 Stack 的归档模板会额外读取 Category Taxonomy Page，并在归档页上展示分类入口：\n1 theme/hugo-theme-stack/layouts/archives.html 其核心逻辑会获取 categories 的 Taxonomy 根页面。Tag 没有进入同一段归档页逻辑，因此 Category 在 Stack 中比 Tag 多了一个归档入口。这仍然只是主题展示策略，不代表 Hugo 对 Category 和 Tag 的处理能力不同。\n相关文章与 Taxonomy 的关系 Stack 的相关文章组件只是调用 Hugo 的 Related Content API：\n1 {{ $related := (.Site.RegularPages.Related .) | first 5 }} 对应源码为：\n1 theme/hugo-theme-stack/layouts/_partials/article/components/related-content.html tags 或 categories 是否参与相似度计算、各自权重是多少，由 Hugo 的 Related Content 配置决定，不是 Stack 在模板中硬编码的行为。不能因为 Stack 展示了分类和标签，就直接推断相关文章一定按照它们计算。\n自定义 Taxonomy 的限制 如果自定义：\n1 2 [taxonomies] series = \u0026#34;series\u0026#34; Hugo 可以正常生成 /series/ 和 /series/\u0026lt;term\u0026gt;/，Stack 的通用 list.html 通常也可以渲染这些列表页面。但 Stack 的多个界面组件明确使用了 categories 或 tags：\nStack 功能 使用的 Taxonomy 文章分类徽章 categories 文章标签组件 tags Category Widget categories Tag Cloud Widget tags 归档页分类入口 categories 因此，自定义 Taxonomy 可以被 Hugo 建模和生成页面，但不会自动获得 Stack 为默认 Category 和 Tag 提供的所有界面能力。需要显示徽章、Widget 或专用入口时，还要补充相应模板或组件。\n一次完整的渲染过程 假设文章包含：\n1 2 3 4 5 6 7 8 --- title: Hugo 页面模型 categories: - Hugo tags: - Stack - Taxonomy --- 构建过程如下：\nHugo 解析 categories 和 tags。 Hugo 把文章加入 categories/Hugo、tags/Stack 和 tags/Taxonomy 三个 Term 集合。 Hugo 创建或补全相应的 Taxonomy Page 和 Term Page。 Stack 在文章页中通过 .GetTerms 渲染分类徽章和标签链接。 Stack 的 Taxonomy Widget 从 .Site.Taxonomies 读取 Term 和文章数量。 用户进入 /tags/stack/ 后，Hugo 将该页面识别为 Kind=term。 Stack 的通用 list.html 遍历该 Term Page 的 .Pages 并渲染文章列表。 常见误区 误区 实际情况 Stack 自己扫描文章并生成标签 Hugo 建立 Taxonomy，Stack 只负责消费和展示 content/tags/_index.md 创建标签系统 它只为已有 Taxonomy 根页面补充元信息 创建 Term 的 _index.md 就会关联文章 文章仍需在 Front Matter 中声明对应 Term content/page/tags/index.md 等同于标签根页 前者是普通 Page，后者应是 Hugo Taxonomy Page layout: tags 能把普通页变成 Taxonomy layout 只影响模板查找，不能改变 Page Kind Category 和 Tag 在 Hugo 中能力不同 二者机制相同，差异主要来自 Stack 的界面设计 自定义 Taxonomy 会自动显示在 Stack 各组件中 通用列表可能可用，但主题组件通常只识别 categories 和 tags 组织原则 理解 Stack 中的 Taxonomy 时，应始终先分清数据模型和展示层：\n1 2 3 4 5 6 7 8 9 10 11 文章 Front Matter = 声明真实的分类与标签关系 content/categories、content/tags = 为 Hugo 自动生成的集合页面补充元信息 Hugo Taxonomy 系统 = 建立关联、页面对象和 URL Stack 模板 = 决定这些数据如何显示 只要沿着这四层检查，就能判断问题究竟来自文章 Front Matter、Taxonomy 配置、内容路径，还是 Stack 模板匹配。\n参考资料 Hugo Taxonomies Hugo Taxonomy Templates Hugo Template Lookup Order Hugo Related Content ","permalink":"https://kerolt.github.io/p/hugo-theme-stack-%E5%AF%B9%E9%BB%98%E8%AE%A4-taxonomy-%E7%9A%84%E5%A4%84%E7%90%86/","summary":"\u003ch2 id=\"核心结论\"\u003e核心结论\u003c/h2\u003e\n\u003cp\u003eStack 没有重新实现 Taxonomy 系统。Hugo 负责读取文章 Front Matter、建立 Taxonomy 与 Term 的关联、生成页面对象和 URL；Stack 负责调用 Hugo 提供的 \u003ccode\u003e.Site.Taxonomies\u003c/code\u003e、\u003ccode\u003e.GetTerms\u003c/code\u003e 和 \u003ccode\u003e.Pages\u003c/code\u003e，将这些数据展示为分类徽章、文章标签、侧栏 Widget、归档入口以及列表页面。\u003c/p\u003e","title":"Hugo Theme Stack 对默认 Taxonomy 的处理"},{"content":"核心本质：Submodule 是什么？ Git Submodule 并不是简单的“文件夹克隆”，它是父仓库中指向子仓库某个特定提交（Commit ID） 的指针。\n.gitmodules 文件：账本。记录子模块的远程仓库地址（URL）和本地存放路径（Path）。 Gitlink (160000 模式)：锁。父仓库索引中记录的一个 40 位哈希值。它确保了无论子仓库如何更新，父仓库始终锁定在特定的版本。 版本锁定：如何固定到 Tag (如 v0.2.2)？ 在 Git 中，锁定版本不是修改文本文件，而是移动指针。\n规范化三步走：\n进目录：cd third_party/blazesym 换版本：git checkout v0.2.2 (此时子模块处于 detached HEAD 状态) 定指针：回到父目录，执行 git add third_party/blazesym 并提交。 .gitmodules 决定“从哪来”，父仓库的 Commit 决定“在哪停”。\n常用指令清单 命令 用途 备注 git submodule add \u0026lt;url\u0026gt; \u0026lt;path\u0026gt; 首次引入子模块 会自动生成 .gitmodules git submodule update --init --recursive 最常用：初始化并下载代码 团队成员拉取代码后必跑 git submodule status 查看当前状态 + 号表示子模块版本与父仓库记录不一致 git submodule sync 同步 .gitmodules 的 URL 变更 修改了 SSH/HTTPS 地址后使用 CMake 工程集成规范 在混合开发（如 C++ 链接 Rust 库）时，Submodule 应当配合 CMake 做到“无感构建”：\n自动化校验：在 CMakeLists.txt 中检查 Cargo.toml 或头文件是否存在，防止协作者忘记初始化 Submodule。 路径解耦：使用 ${CMAKE_CURRENT_SOURCE_DIR} 配合子模块路径，避免绝对路径污染。 构建隔离：将生成的静态库（.a）通过 add_library(... STATIC IMPORTED) 导入，并将系统依赖（pthread, dl 等）封装在 INTERFACE_LINK_LIBRARIES 中。 避坑指南 不要在子模块目录直接 git pull：除非你真的想升级库版本，否则这会导致版本漂移。 谨慎对待 + 状态：如果 git status 显示子模块有变动，请确认是你主动升级了版本，还是误操作。 SSH vs HTTPS：在 .gitmodules 中使用 HTTPS 通常对 CI/CD 环境更友好，避免了 SSH Key 的权限问题。 ","permalink":"https://kerolt.github.io/p/git-submodule-%E7%89%88%E6%9C%AC%E9%94%81%E5%AE%9A/","summary":"\u003ch2 id=\"核心本质submodule-是什么\"\u003e核心本质：Submodule 是什么？\u003c/h2\u003e\n\u003cp\u003eGit Submodule 并不是简单的“文件夹克隆”，它是父仓库中指向子仓库某个\u003cstrong\u003e特定提交（Commit ID）\u003c/strong\u003e 的指针。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e.gitmodules\u003c/code\u003e 文件\u003c/strong\u003e：账本。记录子模块的远程仓库地址（URL）和本地存放路径（Path）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGitlink (160000 模式)\u003c/strong\u003e：锁。父仓库索引中记录的一个 40 位哈希值。它确保了无论子仓库如何更新，父仓库始终锁定在特定的版本。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"版本锁定如何固定到-tag-如-v022\"\u003e版本锁定：如何固定到 Tag (如 v0.2.2)？\u003c/h2\u003e\n\u003cp\u003e在 Git 中，锁定版本\u003cstrong\u003e不是修改文本文件\u003c/strong\u003e，而是\u003cstrong\u003e移动指针\u003c/strong\u003e。\u003c/p\u003e","title":"Git Submodule 版本锁定"},{"content":"基本结构 nmcli [对象] [动作] [参数] 主要对象：con（连接）、dev（设备）、gen（通用）\n常用命令 查看 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 列出所有连接 nmcli con show # 列出活跃连接 nmcli con show --active # 查看连接详情 nmcli con show \u0026#34;连接名\u0026#34; # 查看设备状态 nmcli dev status # 查看设备详情（IP、MAC 等） nmcli dev show enp171s0 创建连接 1 2 3 4 5 6 7 8 9 10 nmcli con add \\ con-name \u0026#34;连接名\u0026#34; \\ type ethernet \\ ifname enp171s0 \\ ipv4.addresses 192.168.100.2/24 \\ ipv4.method manual \\ ipv4.gateway \u0026#34;\u0026#34; \\ ipv4.dns \u0026#34;\u0026#34; \\ ipv4.never-default yes \\ connection.autoconnect yes 参数说明：\n参数 作用 ipv4.method manual 静态 IP（auto = DHCP） ipv4.never-default yes 不添加默认路由，不影响 WiFi 上网 ipv4.gateway \u0026quot;\u0026quot; 不设网关 ipv4.dns \u0026quot;\u0026quot; 不设 DNS connection.autoconnect yes 开机自动激活 修改连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 改 IP nmcli con mod \u0026#34;连接名\u0026#34; ipv4.addresses 192.168.200.2/24 # 绑多个 IP nmcli con mod \u0026#34;连接名\u0026#34; ipv4.addresses \u0026#34;192.168.100.2/24,192.168.200.2/24\u0026#34; # 改连接名 nmcli con mod \u0026#34;旧名\u0026#34; connection.id \u0026#34;新名\u0026#34; # 改 autoconnect nmcli con mod \u0026#34;连接名\u0026#34; connection.autoconnect no # 绑定到指定网口 nmcli con mod \u0026#34;连接名\u0026#34; connection.interface-name enp171s0 启停连接 1 2 3 4 nmcli con up \u0026#34;连接名\u0026#34; # 激活 nmcli con down \u0026#34;连接名\u0026#34; # 停用（仅停此连接，不影响设备） nmcli dev disconnect enp171s0 # 断开设备（所有连接） nmcli dev connect enp171s0 # 重新连接 删除连接 1 nmcli con del \u0026#34;连接名\u0026#34; 设备与连接的关系 一个设备（如 enp171s0）可以绑定多个连接 一个设备同一时间只能激活一个连接 切换连接：nmcli con up \u0026quot;另一个连接\u0026quot; 切换后旧连接自动断开 多连接切换场景 1 2 3 4 5 # 场景 A：直连 Mac（100 网段） nmcli con up \u0026#34;rj45-direct\u0026#34; # 场景 B：直连其他设备（200 网段） nmcli con up \u0026#34;rj45-direct-v2\u0026#34; autoconnect 建议只保留一个连接设为 yes，其余手动 up。\n理解 nmcli con show 只显示持久化配置 nmcli con show 列出的是保存在 /etc/NetworkManager/system-connections/ 的 连接配置文件（profile），相当于\u0026quot;预设方案\u0026quot;。只有你手动 nmcli con add 或 nmcli con save 过的才会出现。\n插网线后自动协商出的 169.254.x.x（Zeroconf/IPv4LL）是内核自动生成的临时地址，没有 profile，所以在 nmcli con show 里看不见，但 ip addr 里能看见。\n来源 出现 ip addr 出现 nmcli con show 重启后持久 nmcli con add 静态 IP 是 是 是 DHCP 自动获取 是 是（自动生成临时 profile） 仅本次会话 IPv4LL 自动协商 (169.254.x.x) 是 否 否 macOS 与 Fedora 的默认退避差异 两端插网线且无 DHCP 时：\n系统 无 DHCP 时的默认行为 结果 macOS 自动启用 Bonjour/Zeroconf → 协商 169.254.x.x 自动拿到 IPv4 Fedora ipv4.link-local disabled IPv4 空着，只有 fe80:: 如果希望 Fedora 和 Mac 一样，没 DHCP 时自动协商链路本地地址：\n1 2 nmcli con mod \u0026#34;连接名\u0026#34; ipv4.link-local enabled nmcli con up \u0026#34;连接名\u0026#34; 但对于直连场景，固定静态 IP（192.168.100.x）更可靠，避免每次随机协商地址变化带来的麻烦。\nautoconnect 与多网口场景 场景还原：enp171s0 绑定了静态 rj45-direct（autoconnect yes），换插到入户网线后，NetworkManager 自动激活了该连接，导致 IP/网关/ DNS 不对，无法上网。\n原因：autoconnect yes 的连接，当网口链路 UP 时 NetworkManager 会自动激活它，无视另一端连接的是什么设备。\n管理原则 每个网口只保留一个连接设为 autoconnect yes（或全设为 no） 不确定场景的连接优先设为 autoconnect no，需要时手动 nmcli con up 实际方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 方案一：固定分工，不冲突 # enp170s0 → 入户网（默认 DHCP，可不配任何 profile） # enp171s0 → 直连 Mac（关 autoconnect） nmcli con mod \u0026#34;rj45-direct\u0026#34; connection.autoconnect no # 方案二：同一网口按场景切换 nmcli con add \\ con-name \u0026#34;rj45-home\u0026#34; \\ type ethernet \\ ifname enp171s0 \\ ipv4.method auto \\ connection.autoconnect no # 切换 nmcli con up \u0026#34;rj45-direct\u0026#34; # 直连 Mac nmcli con up \u0026#34;rj45-home\u0026#34; # 入户上网 判断当前哪个连接激活 1 nmcli con show --active | grep enp171s0 快速排查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 看网口状态 ip link show enp171s0 # 看 IP ip addr show enp171s0 # 看路由表 ip route # 看哪个连接激活了 nmcli con show --active # 看防火墙是否拦截 sudo firewall-cmd --list-all 防火墙相关 1 2 3 4 5 6 7 # 放行内网 SSH sudo firewall-cmd --permanent --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; source address=\u0026#34;192.168.100.0/24\u0026#34; service name=\u0026#34;ssh\u0026#34; accept\u0026#39; sudo firewall-cmd --reload # 放行 mDNS（用于 hostname.local 解析） sudo firewall-cmd --permanent --add-service=mdns sudo firewall-cmd --reload ","permalink":"https://kerolt.github.io/p/nmcli-%E5%BF%AB%E9%80%9F%E5%8F%82%E8%80%83/","summary":"\u003ch2 id=\"基本结构\"\u003e基本结构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003enmcli [对象] [动作] [参数]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e主要对象：\u003ccode\u003econ\u003c/code\u003e（连接）、\u003ccode\u003edev\u003c/code\u003e（设备）、\u003ccode\u003egen\u003c/code\u003e（通用）\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"常用命令\"\u003e常用命令\u003c/h2\u003e\n\u003ch3 id=\"查看\"\u003e查看\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 列出所有连接\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli con show\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 列出活跃连接\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli con show --active\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看连接详情\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli con show \u003cspan class=\"s2\"\u003e\u0026#34;连接名\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看设备状态\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli dev status\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看设备详情（IP、MAC 等）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli dev show enp171s0\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"创建连接\"\u003e创建连接\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli con add \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  con-name \u003cspan class=\"s2\"\u003e\u0026#34;连接名\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003etype\u003c/span\u003e ethernet \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ifname enp171s0 \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ipv4.addresses 192.168.100.2/24 \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ipv4.method manual \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ipv4.gateway \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ipv4.dns \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ipv4.never-default yes \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  connection.autoconnect yes\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e参数说明：\u003c/p\u003e","title":"nmcli 快速参考"},{"content":"搭建中继服务器 虽说是搭建中继服务器，但其实 easytier 是点对点去中心化的组网方式，所谓中继服务器就是一个有公网 ip 的节点，用于让其他没有公网 ip 的主机通过它进行中继转发，从而实现异地组网。\n在有公网 ip 的服务器上通过官网文档安装好 EasyTier，可使用官方安装脚本，或者直接安装二进制文件。我这里是下载的二进制文件（easytier-core 和 easytier-cli）到 /usr/local/bin 目录下，并且将配置文件放在 /opt/easytier/config/default.conf 中（这里文件位置可以自定义，例如 /usr/local/share/easytier/default.conf）。\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 # /opt/easytier/config/default.conf instance_name = \u0026#34;kerolt\u0026#34; # 主机名，用于标识此设备的主机名 hostname = \u0026#34;server\u0026#34; # 如果是作为中继服务器，这里的 ipv4 可以随便设置一个虚拟局域网的 ip 地址，注意不要和其他主机的虚拟局域网 ip 冲突 ipv4 = \u0026#34;10.112.112.1/24\u0026#34; # 由 Easytier 自动确定并设置IP地址，默认从10.0.0.1开始。警告：在使用 DHCP 时，如果网络中出现 IP 冲突，IP 将自动更改（只做服务器写false） dhcp = false listeners = [ \u0026#34;tcp://0.0.0.0:11010\u0026#34;, \u0026#34;udp://0.0.0.0:11010\u0026#34;, \u0026#34;wg://0.0.0.0:11011\u0026#34;, \u0026#34;ws://0.0.0.0:11011/\u0026#34;, \u0026#34;wss://0.0.0.0:11012/\u0026#34;, ] exit_nodes = [ ] #[network_identity] # 网络名称，用于标识 VPN 网络（只做服务器不用填） #network_name = \u0026#34;\u0026#34; # 网络密钥，用于验证此节点属于 VPN 网络（只做服务器不用填） #network_secret = \u0026#34;\u0026#34; [flags] relay_network_whitelist = \u0026#34;kerolt\u0026#34; # 之后我其他主机上的 easytier 网络名称都是使用 kerolt，如果需要所有网络都可以通过该服务器进行中继转发，可以将值设置为 \u0026#34;*\u0026#34; 同时，你需要在服务器防火墙中放行 11010、11011、11012 端口。\n然后通过 systemd 让 easytier 开机自启，编辑 /etc/systemd/system/easytier.service 文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=EasyTier Service After=network.target [Service] Type=simple # 主要是下面这行，取决于你的 easytier-core 和对应配置文件的位置 ExecStart=/usr/local/bin/easytier-core -c /opt/easytier/config/default.conf Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target 执行如下命令，专属于你自己的 easytier 服务器就启动好了：\n1 2 3 sudo systemctl daemon-reload sudo systemctl enable easytier sudo systemctl restart easytier 组网 和刚刚搭建中继服务器的步骤类似，在需要进行组网的主机上也需要安装 easytier（easytier-core 和 easytier-cli），并且编辑对应的配置文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 instance_name = \u0026#34;kerolt\u0026#34; ipv4 = \u0026#34;10.112.112.2/24\u0026#34; dhcp = false listeners = [ \u0026#34;tcp://0.0.0.0:11010\u0026#34;, \u0026#34;udp://0.0.0.0:11010\u0026#34;, \u0026#34;wg://0.0.0.0:11011\u0026#34;, ] [network_identity] network_name = \u0026#34;kerolt\u0026#34; # 之前服务器上只配置了 kerolt 这个网络的白名单 network_secret = \u0026#34;xxxx\u0026#34; [[peer]] uri = \u0026#34;tcp://your-server-ip:11010\u0026#34; easytier 就会默认给你这个主机 A分配一个虚拟局域网 ip 为 10.112.112.2/24；同样的，对于另一台主机 B，使用同样的配置文件，只需要修改 ipv4 = \u0026quot;10.112.112.3/24\u0026quot; 和 dhcp = false，或者直接使用 dhcp = true 让 easytier 自动进行 DHCP 分配 ip。\n到这里，主机 A（10.112.112.2/24）和主机 B（10.112.112.3/23）已经处于同一虚拟局域网内，即主机 A 和主机 B 已经可以通过虚拟网通信了。\n","permalink":"https://kerolt.github.io/p/%E6%90%AD%E5%BB%BAeasytier%E4%B8%AD%E7%BB%A7%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%B9%B6%E8%BF%9B%E8%A1%8C%E7%BB%84%E7%BD%91/","summary":"\u003ch2 id=\"搭建中继服务器\"\u003e搭建中继服务器\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e虽说是搭建中继服务器，但其实 easytier 是点对点去中心化的组网方式，所谓中继服务器就是一个\u003cstrong\u003e有公网 ip 的节点\u003c/strong\u003e，用于让其他没有公网 ip 的主机通过它进行中继转发，从而实现异地组网。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"搭建EasyTier中继服务器并进行组网"},{"content":"问题现象 执行 git fetch --all 报错：\n1 2 Connection closed by 198.18.0.35 port 22 fatal: Could not read from remote repository. 排查过程 1. 确认 remote 地址 1 2 git remote -v # origin git@github.com:xxx/yyy.git 使用的是 SSH 协议。\n2. 排查 DNS 解析 1 2 3 nslookup github.com # Name: github.com # Address: 198.18.0.35 198.18.0.35 不是 GitHub 真实 IP，属于 198.18.0.0/15 保留段。这是 Clash Fake-IP 模式 的特征，即 DNS 返回虚拟 IP，由 Clash 透明代理接管流量。\n3. 确认 Clash 运行状态 1 2 3 ss -tlnp | grep 789 # LISTEN *:7890 # HTTP 代理 # LISTEN *:7891 # SOCKS5 代理 Clash (mihomo) 正在运行，HTTP 代理端口 7890，SOCKS5 端口 7891，均开启了 Basic 认证。\n4. 尝试 SSH ProxyCommand 在 ~/.ssh/config 中为 github.com 添加 ProxyCommand：\n1 2 3 Host github.com HostName github.com ProxyCommand ncat --proxy-type http --proxy 127.0.0.1:7890 --proxy-auth kerolt:xxxxxx %h %p ncat 能成功建立 CONNECT 隧道（exit 0），但 SSH 握手阶段被远端关闭：\n1 2 debug1: Local version string SSH-2.0-OpenSSH_10.2 kex_exchange_identification: Connection closed by remote host 在 Fake-IP 模式下，即使通过 HTTP CONNECT 代理转发 SSH 流量，也无法完成完整的 SSH 握手。Clash 对 SSH 流量的透明代理处理存在问题。\n解决方案 方案一（推荐）：改用 HTTPS + Git 代理 1 2 3 4 5 6 7 8 9 # 1. 配置 git 全局 HTTPS 代理（含 Clash 认证） git config --global http.proxy http://kerolt:\u0026lt;password\u0026gt;@127.0.0.1:7890 git config --global https.proxy http://kerolt:\u0026lt;password\u0026gt;@127.0.0.1:7890 # 2. 将 remote 从 SSH 改为 HTTPS git remote set-url origin https://github.com/\u0026lt;owner\u0026gt;/\u0026lt;repo\u0026gt;.git # 3. 验证 git fetch --all 注意： GitHub 已不支持密码认证，push 时需要 Personal Access Token (PAT)。可以配置：\n1 git config --global url.\u0026#34;https://\u0026lt;TOKEN\u0026gt;@github.com/\u0026#34;.insteadOf \u0026#34;https://github.com/\u0026#34; 方案二：Clash 切换为 Redir-Host 模式 在 Clash 配置中将 fake-ip 改为 redir-host，让 DNS 返回真实 IP，SSH 就能正常走透明代理。但这可能影响其他依赖 Fake-IP 的功能。\n方案三：使用 SOCKS5 代理工具 安装 connect-proxy 或 socat 等工具替代 ncat，部分工具对 SOCKS5 代理的 SSH 转发支持更好。\n核心原因总结 环节 说明 Clash Fake-IP DNS 返回虚拟 IP 198.18.0.x，依赖透明代理接管流量 SSH 协议 不是 HTTP，无法被 HTTP 代理直接转发 CONNECT 隧道 可以建立 TCP 隧道，但 SSH 握手阶段被阻断 HTTPS 方案 Git over HTTPS 是标准 HTTP 流量，Clash 代理完美支持 ","permalink":"https://kerolt.github.io/p/clash-fake-ip%E6%A8%A1%E5%BC%8F%E4%B8%8Bgit-ssh%E8%BF%9E%E6%8E%A5%E5%A4%B1%E8%B4%A5/","summary":"\u003ch2 id=\"问题现象\"\u003e问题现象\u003c/h2\u003e\n\u003cp\u003e执行 \u003ccode\u003egit fetch --all\u003c/code\u003e 报错：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConnection closed by 198.18.0.35 port \u003cspan class=\"m\"\u003e22\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efatal: Could not \u003cspan class=\"nb\"\u003eread\u003c/span\u003e from remote repository.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"排查过程\"\u003e排查过程\u003c/h2\u003e\n\u003ch3 id=\"1-确认-remote-地址\"\u003e1. 确认 remote 地址\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit remote -v\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# origin  git@github.com:xxx/yyy.git\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e使用的是 SSH 协议。\u003c/p\u003e\n\u003ch3 id=\"2-排查-dns-解析\"\u003e2. 排查 DNS 解析\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enslookup github.com\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Name:   github.com\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Address: 198.18.0.35\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003ccode\u003e198.18.0.35\u003c/code\u003e 不是 GitHub 真实 IP，属于 \u003ccode\u003e198.18.0.0/15\u003c/code\u003e 保留段。这是 \u003cstrong\u003eClash Fake-IP 模式\u003c/strong\u003e 的特征，即 DNS 返回虚拟 IP，由 Clash 透明代理接管流量。\u003c/p\u003e","title":"Clash-Fake-IP模式下Git-SSH连接失败"},{"content":"一句话概括：零拷贝技术让数据在 磁盘 → 内核缓冲区 → 网卡 之间直接传输，避免用户态与内核态之间的冗余数据拷贝，核心是消除 CPU 参与的数据搬移。\n传统 IO 路径（4 次拷贝，2 次 CPU 参与） 1 2 磁盘 → [DMA] → Page Cache (内核) → [CPU] → 用户 Buffer → [CPU] → Socket Buffer (内核) → [DMA] → NIC ① ② ③ ④ ① DMA 读磁盘到 page cache ② CPU 把 page cache 拷到用户态 buffer（read） ③ CPU 把用户态 buffer 拷到 socket buffer（send） ④ DMA 从 socket buffer 发送到网卡 零拷贝的目标：去掉 ② 和 ③ 这两步 CPU 拷贝。\n1. sendfile（最常用，静态文件服务） 1 2 sendfile(out_fd, in_fd, \u0026amp;offset, count); // out_fd= socket, in_fd= 文件 fd 路径：\n1 2 磁盘 → [DMA] → Page Cache → [SG-DMA 直接描述] → NIC ① ② (无 CPU 数据拷贝) 底层： Page Cache 中的页面直接通过 scatter-gather DMA 发送到网卡。内核只需要把 page 指针链入 skb 的 frag 数组，DMA 引擎直接读取。CPU 不碰数据，只传元数据（指针、长度）。\n限制：\n数据必须从文件到 socket（不支持 socket→socket 或文件→文件） out_fd 必须支持 DMA 发送（通常只有 TCP socket） in_fd 必须是支持 splice_read 的文件（普通文件、块设备） 2. splice（通用 pipe 搬运，任意 fd 之间） 1 2 3 4 int pipefd[2]; pipe(pipefd); splice(fd_in, off_in, pipefd[1], NULL, len, SPLICE_F_MOVE); splice(pipefd[0], NULL, fd_out, off_out, len, SPLICE_F_MOVE); 路径：\n1 文件 → pipe buffer (只传指针，不拷数据) → socket 底层： splice 将 fd_in 的 page 引用直接挂到 pipe 的 struct pipe_buffer 上，再从 pipe 挂到 fd_out 的 skb 上。数据始终在内核 page cache 里不动，只移动了引用计数。\nsendfile vs splice：\nsendfile 是 splice 的特化（splice 内部实现就是 pipe + 两个 splice 调用） splice 可以连接任意两个 fd（文件 → socket、socket → socket、pipe → 任何），而 sendfile 只支持文件 → socket 3. mmap + write（用户态直接操作 page cache） 1 2 void *addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); write(sockfd, addr, len); 路径：\n1 2 3 4 磁盘 → [DMA] → Page Cache ↕ (mmap 共用同一物理页，不拷贝) 用户地址空间 ↓ write 时内核直接从 page cache 构建 skb 底层： mmap 将 page cache 的物理页直接映射到用户地址空间，用户 write 时内核直接从映射页构造 skb-\u0026gt;frags，无需 copy_from_user。\n问题：\n多线程并发写同一个 mmap 区域需加锁 文件长度变化、截断时的 SIGBUS 处理复杂 脏页回写时机不可控 4. SO_ZEROCOPY（Linux 4.14+，用户态直接发零拷贝） 1 2 3 int one = 1; setsockopt(sockfd, SOL_SOCKET, SO_ZEROCOPY, \u0026amp;one, sizeof(one)); send(sockfd, buf, len, MSG_ZEROCOPY); 路径：\n1 2 3 4 用户 Buffer → [DMA 直接读取] → NIC ① 内核锁定用户页，记录其物理地址 ② DMA 直接从用户页读取数据发送 ③ 发送完成后通知用户页可释放 底层：\n内核调用 pin_user_pages 固定用户缓冲区物理页（防止被 swap 出去） 将用户页地址填入 skb frags，DMA 引擎直接读取用户态内存 发送完成通过 recvmsg 返回 SO_EE_ORIGIN_ZEROCOPY 通知用户释放 去掉了一次内核态到用户态的拷贝（copy_from_user），但需要 pin/unpin 开销。\n适用场景： 大块数据发送（\u0026gt;10KB），小包反而更慢（pin/unpin 开销 \u0026gt; 直接拷贝）\n5. io_uring + 注册缓冲区（Linux 5.1+） 1 2 3 4 5 6 7 8 // 注册固定缓冲区（一次 pin，多次复用） struct iovec iov = { .iov_base = buf, .iov_len = len }; io_uring_register_buf_ring(ring, \u0026amp;iov, 1); // 提交 IO，直接引用已 pin 的 buffer struct io_uring_sqe *sqe = io_uring_get_sqe(ring); io_uring_prep_read_fixed(sqe, fd, buf, len, 0, buf_index); io_uring_prep_write_fixed(sqe, fd, buf, len, 0, buf_index); 路径：\n1 磁盘 → [DMA] → 用户注册 Buffer（直接） 底层： io_uring_register_buf_ring 一次 pin_user_pages 后返回 buffer index，后续所有 IO 请求直接通过 index 引用 buffer，无需再 pin/unpin。避免了每次 IO 的页锁定开销，同时去掉 CPU 拷贝。\n6. RDMA（InfiniBand / RoCE / iWARP，硬件级零拷贝） 1 2 3 4 5 6 7 8 // 注册 MR (Memory Region) struct ibv_mr *mr = ibv_reg_mr(pd, buf, len, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE); // 远端直接写本端内存（不需要本端 CPU 参与） struct ibv_sge sge = { .addr = (uintptr_t)buf, .length = len, .lkey = mr-\u0026gt;lkey }; struct ibv_send_wr wr = { .wr_id = 1, .sg_list = \u0026amp;sge, .num_sge = 1, .opcode = IBV_WR_RDMA_WRITE }; ibv_post_send(qp, \u0026amp;wr, \u0026amp;bad_wr); 路径：\n1 2 远端内存 → [网卡硬件 DMA] → 本端用户内存 全程不需要本端 CPU 参与 底层： 网卡通过 ibv_reg_mr 获取用户虚拟地址 → 物理页的映射表，远端网卡直接写入本端物理页，旁路内核，零 CPU。本端应用直接看到数据，不经过内核 socket buffer。\n代价：\n需要 RDMA 硬件（InfiniBand 交换机/网卡或支持 RoCE 的以太网卡） 内存注册（ibv_reg_mr）开销大（页表遍历 + pin pages），需池化复用 7. DPDK（用户态驱动，完全绕过内核） 1 2 3 应用 → DPDK PMD → NIC ↑ 全程用户态操作 网卡直接把数据 DMA 到用户态预先分配的 hugepage 里。没有系统调用，没有上下文切换，没有内核数据结构。\n但代价是应用层必须实现 TCP 协议栈（如 mTCP、F-Stack）。\n各场景推荐 场景 推荐技术 Web 服务器静态文件 sendfile 反向代理/网关转发 splice 大块数据发送（视频服务） SO_ZEROCOPY 高性能存储 + 网络 io_uring + registered buffers 超低延迟（\u0026lt; 10us） RDMA 纯包转发（\u0026gt; 100Gbps） DPDK 注意： 零拷贝不是银弹。小包场景下，CPU 拷贝的延迟远小于 pin/unpin 或上下文切换的开销，传统路径反而更快。Nginx 对 \u0026lt; 16KB 的文件直接用 read + write，超过才用 sendfile——这也是一种工程权衡。\n","permalink":"https://kerolt.github.io/p/%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%8A%80%E6%9C%AF/","summary":"\u003cp\u003e一句话概括：零拷贝技术让数据在 \u003cstrong\u003e磁盘 → 内核缓冲区 → 网卡\u003c/strong\u003e 之间直接传输，避免用户态与内核态之间的冗余数据拷贝，核心是消除 CPU 参与的数据搬移。\u003c/p\u003e\n\u003ch2 id=\"传统-io-路径4-次拷贝2-次-cpu-参与\"\u003e传统 IO 路径（4 次拷贝，2 次 CPU 参与）\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e磁盘 → \u003cspan class=\"o\"\u003e[\u003c/span\u003eDMA\u003cspan class=\"o\"\u003e]\u003c/span\u003e → Page Cache \u003cspan class=\"o\"\u003e(\u003c/span\u003e内核\u003cspan class=\"o\"\u003e)\u003c/span\u003e → \u003cspan class=\"o\"\u003e[\u003c/span\u003eCPU\u003cspan class=\"o\"\u003e]\u003c/span\u003e → 用户 Buffer → \u003cspan class=\"o\"\u003e[\u003c/span\u003eCPU\u003cspan class=\"o\"\u003e]\u003c/span\u003e → Socket Buffer \u003cspan class=\"o\"\u003e(\u003c/span\u003e内核\u003cspan class=\"o\"\u003e)\u003c/span\u003e → \u003cspan class=\"o\"\u003e[\u003c/span\u003eDMA\u003cspan class=\"o\"\u003e]\u003c/span\u003e → NIC\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t  ①                        ②                     ③                         ④\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e① DMA 读磁盘到 page cache\u003c/li\u003e\n\u003cli\u003e② CPU 把 page cache 拷到用户态 buffer（read）\u003c/li\u003e\n\u003cli\u003e③ CPU 把用户态 buffer 拷到 socket buffer（send）\u003c/li\u003e\n\u003cli\u003e④ DMA 从 socket buffer 发送到网卡\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e零拷贝的目标：去掉 ② 和 ③ 这两步 CPU 拷贝。\u003c/p\u003e","title":"零拷贝技术"},{"content":" 本文是对 Build a Container from Scratch in Go (Modern Namespaces + cgroup v2) 这篇文章的翻译，以下是正文\n作者：Faizan Firdousi 发布时间：2026 年 2 月 18 日 我用 Go 从零构建了一个容器，代码行数尽可能少，并在过程中了解了容器内部通常发生的各种事情——也就是 Docker 抽象掉的那些细节。\n虽然网上也有类似的文章，但我写这篇是因为 Linux 内核的一些变化，导致很多博客在理解和代码实现上已经有些过时了。\n什么是容器？ 首先理解一下容器是什么。你可能已经知道，从根本上说，容器就是“将依赖打包起来，以便以可重复、安全的方式交付代码”。\n但让我们深入理解背后的概念：命名空间（Namespaces）、控制组（Cgroups）和文件系统隔离，这些才是容器的基石。\n命名空间 Linux 命名空间是内核的一个基础特性，它提供资源隔离，让不同的进程集合看到不同的资源视图。\n命名空间非常重要，稍后我们会看到它的使用频率，因为它们是容器构建的核心技术。当你用 Docker 或 Podman 创建容器时，它会自动为你创建命名空间。\n命名空间主要有 6 种类型（注意，我们会在编写代码时更详细地介绍它们，因为我喜欢边做边学，不会一开始就塞给你太多信息）：\nPID —— 分配独立的进程 ID。新命名空间中的第一个进程获得 PID 1。 Network —— 通过虚拟以太网对提供独立的网络栈。 MNT —— 维护独立的挂载点列表，允许在不影响宿主机的情况下挂载/卸载文件系统。 USER —— 拥有自己的用户 ID 和组 ID，允许一个进程在自己的命名空间内拥有 root 权限，但在别处没有。 IPC —— 隔离进程间通信资源，如 POSIX 消息队列。 UTS —— 允许同一系统上的不同进程拥有不同的主机名和域名。 Cgroups 理解 cgroups 的表面含义很容易——它代表控制组（control groups），是 Linux 内核的一个特性，用于限制、统计和隔离进程集合的资源使用，包括 CPU、内存、磁盘 I/O 和网络。\n关于 cgroups 可以讲很多，但这里我只介绍实现所需的基础知识，后面还会再提到。\n为了方便一步步深入理解，我们把代码分成几个阶段。\n这里是完整代码的 GitHub 链接（请给个 star）：\nhttps://github.com/faizanfirdousi/container-from-scratch\n阶段 0 首先，明确一下我们要做什么：主要目标是创建一个与宿主机隔离的容器。换句话说，我们要构建一个沙盒环境，运行一个拥有自己文件系统、进程命名空间和资源限制的 shell，从而与宿主机实现强隔离。\n容器可以运行任何东西。为了简单起见，我们将使用 Alpine Linux 容器，因为它体积小，同时也拥有完整的组件，能提供完整的系统感觉。最终，我们将创建一个容器——或者说一个进程——它认为 Alpine 目录就是整个计算机。\n为此，我们需要 Alpine 的根文件系统。在开始编码前先准备好：\n1 2 3 4 # 下载并解压 Alpine Linux rootfs docker export $(docker create alpine) -o alpine.tar mkdir -p /home/faizan/alpine-rootfs tar -xf alpine.tar -C /home/faizan/alpine-rootfs 如果你没有 Docker，可以通过 curl 安装。这里用它只是为了简单。\n阶段 1 首先，我们创建一个简单的程序来运行命令，但它还没有任何隔离。这似乎有点反直觉，但有一个很好的理由：通过观察没有隔离时会发生什么，当我们后续添加每个隔离原语时，你才能真正理解它们的作用。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/exec\u0026#34; ) func main() { if len(os.Args) \u0026lt; 2 { fmt.Fprintf(os.Stderr, \u0026#34;Usage: %s run \u0026lt;cmd\u0026gt; [args...]\\n\u0026#34;, os.Args[0]) os.Exit(1) } switch os.Args[1] { case \u0026#34;run\u0026#34;: run() default: panic(\u0026#34;Unknown command\u0026#34;) } } func run() { fmt.Printf(\u0026#34;Running %v\\n\u0026#34;, os.Args[2:]) cmd := exec.Command(os.Args[2], os.Args[3:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(cmd.Run()) } func must(err error) { if err != nil { panic(err) } } 这段代码做了什么 程序首先检查传递给它的参数。第一个参数必须是 run，告诉我们的程序要执行一个命令。run 之后的所有内容都成为要执行的命令及其参数——例如 /bin/bash 或 /bin/sh。\n当 run 函数被调用时，它使用 Go 的 exec.Command 创建一个新进程。工作原理很简单：os.Args[2] 包含命令本身（如 /bin/bash），os.Args[3:] 包含要传递给该命令的任何附加参数。... 语法将这些参数展开，逐个传递给命令。\n然后我们将标准输入、输出和错误流连接起来。这很重要，因为它允许命令与你的终端自然交互。你可以像任何普通命令一样输入内容并看到输出。最后，cmd.Run() 实际执行命令并等待它完成。\n测试 1 go run main.go run /bin/bash 你应该会看到类似这样的输出：\n1 2 Running [/bin/bash] [root@archlinux cfs]# 测试一下是否隔离：运行 ps 和 hostname 等命令。\n现在让我展示一下测试结果。在 shell 中运行 ps，仔细观察输出。\n注意到什么有趣的事情了吗？PID 并不是从 1 开始（就像在真正的容器中那样）。相反，你看到的 PID 是 46628、46629、46669 等——这些正是宿主机看到的进程 ID。如果你在宿主机上打开另一个终端窗口并运行 ps aux，你会看到完全相同的 PID 值。这证明我们与宿主机共享着同一个进程命名空间。\n现在检查主机名。\n运行 hostname，你会看到你机器的真实主机名。试着用 sudo hostname test-container 修改它，然后再次运行 hostname 确认修改成功。退出这个 shell，再检查你宿主机的 hostname——它也被修改了！我们直接修改了宿主系统的 hostname。我们的“容器”内部没有任何隔离屏障来保护宿主机免受修改的影响。\n文件系统也是一样。当你运行 ls / 时，你看到的是实际宿主机的根目录。你在 /tmp 中创建的任何文件都会出现在宿主机上。我们完全在宿主机系统上操作，没有任何隔离。\n为什么这很重要 我们现在构建的东西基本上只是一个执行命令的包装器——根本没有发生任何容器化。但这个基线很重要。在接下来的步骤中，当我们添加 Linux 命名空间时，你会看到情况会发生多么戏剧性的变化。PID 将从 1 开始，修改 hostname 不会影响宿主机，我们还将拥有自己隔离的文件系统。\n阶段 2 现在把 run() 函数更新为如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func run() { fmt.Printf(\u0026#34;Running %v\\n\u0026#34;, os.Args[2:]) // 重新执行我们自己作为 \u0026#34;child\u0026#34;，并进入新的命名空间 cmd := exec.Command(\u0026#34;/proc/self/exe\u0026#34;, append([]string{\u0026#34;child\u0026#34;}, os.Args[2:]...)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = \u0026amp;syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, Unshareflags: syscall.CLONE_NEWNS, } must(cmd.Run()) } 并添加这个新的 child() 函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func child() { fmt.Printf(\u0026#34;Running %v as child\\n\u0026#34;, os.Args[2:]) cmd := exec.Command(os.Args[2], os.Args[3:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(syscall.Sethostname([]byte(\u0026#34;container\u0026#34;))) if err := cmd.Run(); err != nil { fmt.Println(\u0026#34;Process exited:\u0026#34;, err) } } 让我们理解一下这里在做什么。首先，我们需要创建命名空间。在 run() 中我们写下：\n1 cmd := exec.Command(\u0026#34;/proc/self/exe\u0026#34;, append([]string{\u0026#34;child\u0026#34;}, os.Args[2:]...)...) 这是关键洞察：我们不是直接运行 /bin/bash。相反，我们重新执行我们自己的 Go 程序（/proc/self/exe 是当前运行二进制文件的路径），但使用了一个新的参数列表：[\u0026quot;child\u0026quot;, \u0026quot;/bin/bash\u0026quot;]。\n为什么需要这种奇怪的自我重新执行？因为 Linux 命名空间只能在创建新进程时建立，你不能为一个已经运行的进程创建命名空间。所以我们需要让我们的代码再次运行，但这一次处于全新的隔离命名空间内部。\n然后我们使用以下代码实际创建命名空间：\n1 2 3 4 cmd.SysProcAttr = \u0026amp;syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, Unshareflags: syscall.CLONE_NEWNS, } 正如我在博客开头所说，不同的命名空间隔离不同的东西。这里我们使用 Linux 系统调用来创建命名空间。具体来说，我们使用带有特殊标志的 clone() 系统调用。\n我们使用的系统调用属于 CLONE_* 家族。每个 CLONE_NEW* 标志告诉 Linux 内核：“当你创建这个子进程时，把它放到一个独立的、针对该资源的命名空间中。”让我分解一下每个标志的作用：\nCLONE_NEWUTS 创建一个新的 UTS（Unix Time-Sharing）命名空间。这隔离了主机名和域名。有了这个标志，当子进程将其主机名改为 \u0026ldquo;container\u0026rdquo; 时，宿主机的主机名完全保持不变。如果没有这个标志，内部修改 hostname 会影响到你的实际宿主机——这正是我们在阶段 1 中看到的情况。\nCLONE_NEWPID 创建一个新的 PID（进程 ID）命名空间。这非常重要。这意味着子进程获得了一个完全独立的进程 ID 编号系统。在容器内部，shell 会认为自己是 PID 1——第一个进程，就像真正的 Linux 启动中的 init 系统。但从宿主机的角度看，同一个进程可能只是 PID 15234。这就是为什么在真正的容器中运行 ps aux 时，你只能看到容器内的进程，而不是宿主机的所有进程。\nCLONE_NEWNS 创建一个新的 Mount 命名空间。这隔离了文件系统挂载点。我们在容器内部做的任何挂载（如挂载 /proc 或 /tmp）都不会出现在宿主机上，反之亦然。这对文件系统隔离至关重要。\n这些标志之间的 |（竖线）是位或操作——它表示同时启用多个命名空间。本质上我们在说：“为子进程同时创建这三类命名空间。”\nUnshareflags 配合 CLONE_NEWNS 是一个额外的安全措施。它使挂载命名空间“不可共享”，以防止挂载传播，基本上确保容器内的任何文件系统变更绝对不会泄露到宿主机。\n测试 现在测试一下我们的命名空间隔离是否真的有效。运行你更新后的代码：\n你应该会看到：\n1 2 3 Running [/bin/bash] Running [/bin/bash] as child [root@container cfs]# 你会注意到提示符中的主机名已经显示为 container——这正是 Sethostname 调用生效的结果。\n现在检查修改 hostname 是否保持隔离：修改容器内的 hostname，然后检查宿主机的 hostname 是否也被改变了。\n接下来检查 PID 命名空间隔离——这个很有趣也很重要。\n在容器内部运行：\n1 echo $$ 在你的宿主机上（另一个终端）：\n1 ps aux | grep bash 同一个进程，两个不同的 PID！ 在命名空间内部它是 PID 7（对你来说可能是其他较小的数字），从宿主机看它是 PID 44999。这证明了 PID 隔离正在工作。\n测试 /proc 问题 在容器内部运行 ps：\n1 ps aux 你会看到宿主机的所有进程——systemd、kthreadd、Chrome，等等。为什么？因为即使我们有 PID 命名空间隔离，我们仍然在读取宿主机的 /proc 文件系统。ps 命令从 /proc 获取信息，而我们还没有隔离 /proc。\n阶段 3 更新你的 child() 函数，添加文件系统隔离。首先，在 cmd 设置之后立即添加环境变量：\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 func child() { fmt.Printf(\u0026#34;Running %v \\n\u0026#34;, os.Args[2:]) cmd := exec.Command(os.Args[2], os.Args[3:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // 添加这些环境变量 cmd.Env = []string{ \u0026#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34;, \u0026#34;HOME=/root\u0026#34;, \u0026#34;TERM=xterm\u0026#34;, } must(syscall.Sethostname([]byte(\u0026#34;container\u0026#34;))) // 添加文件系统隔离 must(syscall.Chroot(\u0026#34;/home/faizan/alpine-rootfs\u0026#34;)) must(os.Chdir(\u0026#34;/\u0026#34;)) // 挂载 /proc 文件系统 must(syscall.Mount(\u0026#34;proc\u0026#34;, \u0026#34;proc\u0026#34;, \u0026#34;proc\u0026#34;, 0, \u0026#34;\u0026#34;)) // 在 /tmp 挂载 tmpfs must(os.MkdirAll(\u0026#34;tmp\u0026#34;, 0755)) must(syscall.Mount(\u0026#34;tmpfs\u0026#34;, \u0026#34;tmp\u0026#34;, \u0026#34;tmpfs\u0026#34;, 0, \u0026#34;\u0026#34;)) if err := cmd.Run(); err != nil { fmt.Println(\u0026#34;Process exited:\u0026#34;, err) } // 退出时清理挂载 syscall.Unmount(\u0026#34;proc\u0026#34;, 0) syscall.Unmount(\u0026#34;tmp\u0026#34;, 0) } 环境变量 1 2 3 4 5 cmd.Env = []string{ \u0026#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34;, \u0026#34;HOME=/root\u0026#34;, \u0026#34;TERM=xterm\u0026#34;, } 这是一个虽小但重要的细节。当子进程被生成时，默认会继承父进程的环境变量，这意味着它会继承你宿主机的 PATH、宿主机的 HOME 等等。这是一个问题，因为在我们下一步进行文件系统隔离之后，那些宿主机路径在容器内部将根本不存在。\n所以我们显式设置了一个干净、最小的环境。PATH 告诉 shell 去哪里找可执行文件。HOME 设置家目录。TERM=xterm 确保终端行为正常工作，比如清屏、箭头键、彩色输出。没有 TERM，很多终端程序会表现异常或拒绝运行。\nChroot（关键步骤） 1 2 must(syscall.Chroot(\u0026#34;/home/faizan/alpine-rootfs\u0026#34;)) must(os.Chdir(\u0026#34;/\u0026#34;)) chroot 为这个进程及其所有子进程重新定义了 / 的含义。在这个调用之后，当 shell 打开 /etc/passwd 时，内核将其解析为 /home/faizan/alpine-rootfs/etc/passwd。宿主机的文件系统变得完全不可见。运行 ls /，你会看到 Alpine 的 bin、etc、usr，就像一台真正的 Alpine 机器。\nchroot 改变了 / 指向的位置，但你的当前工作目录并不会移动。所以如果你在运行代码的目录下，chroot 之后你仍然停留在那个目录——也就是在“监狱”外面。从那里，一个进程可以简单地执行 cd ../../.. 然后直接走出去。os.Chdir(\u0026quot;/\u0026quot;) 立即将你移入监狱内部，这样就没有地方可以逃逸了——非常完美。\n挂载 /proc —— 修复 ps 读取的问题 1 must(syscall.Mount(\u0026#34;proc\u0026#34;, \u0026#34;proc\u0026#34;, \u0026#34;proc\u0026#34;, 0, \u0026#34;\u0026#34;)) 在 chroot 之后，Alpine 的 /proc 是一个空目录。ps 命令从 /proc/\u0026lt;pid\u0026gt;/status 读取所有信息，如果不挂载东西，ps 就会失败。\n因此我们挂载一个新的 procfs。关键细节：这不是宿主机 /proc 的副本。内核根据当前 PID 命名空间能看到什么来填充它。由于我们处于阶段 2 的 CLONE_NEWPID 内部，所以只有容器的进程会出现在这里。现在运行 ps aux，你只会看到容器的 shell。宿主机进程的洪流消失了。这是 PID 隔离和文件系统隔离终于协同工作。\n在 /tmp 挂载 tmpfs 1 2 must(os.MkdirAll(\u0026#34;tmp\u0026#34;, 0755)) must(syscall.Mount(\u0026#34;tmpfs\u0026#34;, \u0026#34;tmp\u0026#34;, \u0026#34;tmpfs\u0026#34;, 0, \u0026#34;\u0026#34;)) tmpfs 是基于内存的文件系统。容器内部写入 /tmp 的任何内容都存在于 RAM 中，永远不会触及宿主机的 /tmp，并且在容器退出的那一刻就会消失。这也是为什么阶段 2 中的 CLONE_NEWNS 很重要。如果没有挂载命名空间，这些挂载会传播到宿主机的挂载表。挂载命名空间是前提条件，而这些挂载是实际负载。\n测试 现在检查一切：是否显示容器自身的文件系统和进程，而不是宿主机的。\n阶段 4 —— Cgroups 现在我们有了一个真正隔离的容器：自己的文件系统、自己的进程树、自己的主机名。但仍然存在一个问题：容器内的进程没有任何限制来阻止它消耗宿主机的所有 CPU、生成数千个进程或吃掉所有 RAM。容器内部一个简单的 fork bomb 就会让整台机器崩溃。\n这就是 cgroups（控制组）要解决的问题。命名空间控制进程能看到什么，而 cgroups 控制进程能使用多少资源。\n因此，用这个函数更新你的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func cg() { cgroupRoot := \u0026#34;/sys/fs/cgroup/\u0026#34; cgroupName := \u0026#34;container-cgroup\u0026#34; containerCgroup := filepath.Join(cgroupRoot, cgroupName) os.WriteFile( filepath.Join(cgroupRoot, \u0026#34;cgroup.subtree_control\u0026#34;), []byte(\u0026#34;+pids +memory +cpu\u0026#34;), 0644, ) must(os.MkdirAll(containerCgroup, 0755)) must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;pids.max\u0026#34;), []byte(\u0026#34;20\u0026#34;), 0700)) must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;memory.max\u0026#34;), []byte(\u0026#34;52428800\u0026#34;), 0700)) must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;cpu.max\u0026#34;), []byte(\u0026#34;50000 100000\u0026#34;), 0700)) must(os.WriteFile( filepath.Join(containerCgroup, \u0026#34;cgroup.procs\u0026#34;), []byte(strconv.Itoa(os.Getpid())), 0700, )) } Cgroup v2 这正是我写这篇博客的主要原因。你看，大多数博客都是基于 cgroups v1 的，但很早以前内核就已经切换到 cgroup v2 了，所以代码也得相应地改变。\nCgroup v2 使用统一层级结构。每个控制器——pids、memory、cpu——都位于 /sys/fs/cgroup/ 下的单棵树中。这与 v1 不同，v1 为每个控制器使用单独的树，例如 /sys/fs/cgroup/pids/、/sys/fs/cgroup/memory/ 等等。如果你看过使用 v1 路径的老容器博客，那就是为什么它们在现代内核上无法工作的原因。\n操作 cgroups 完全通过文件系统进行——你创建目录，向文件写入内容。不需要特殊的系统调用。\n1 2 3 4 5 os.WriteFile( filepath.Join(cgroupRoot, \u0026#34;cgroup.subtree_control\u0026#34;), []byte(\u0026#34;+pids +memory +cpu\u0026#34;), 0644, ) 首先，我们在根 cgroup 上启用控制器。向 subtree_control 写入 +pids +memory +cpu 告诉内核让这些控制器可用于我们后续创建的子 cgroup。这就像在使用功能之前先解锁它。\n创建 Cgroup 1 must(os.MkdirAll(containerCgroup, 0755)) 这就是创建 cgroup 的全部操作。你在 /sys/fs/cgroup/ 下创建一个目录，内核会识别它，将其视为一个新的 cgroup，并自动在其中填充控制文件：pids.max、memory.max、cpu.max、cgroup.procs 等等。不需要其他任何操作。\n设置限制 1 2 3 must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;pids.max\u0026#34;), []byte(\u0026#34;20\u0026#34;), 0700)) must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;memory.max\u0026#34;), []byte(\u0026#34;52428800\u0026#34;), 0700)) must(os.WriteFile(filepath.Join(containerCgroup, \u0026#34;cpu.max\u0026#34;), []byte(\u0026#34;50000 100000\u0026#34;), 0700)) pids.max 是最直接重要的一个。将其设置为 20 意味着一旦进程数达到 20，内核就会开始拒绝 fork() 和 clone() 调用。容器内部的 fork bomb 只会自己崩溃，宿主机不受影响。\nmemory.max 是 52428800 字节 —— 50 MiB。如果容器超过这个限制，内核的 OOM killer 会介入并杀死超限的进程。宿主机的内存永远不会受到威胁。\ncpu.max 的格式是 $MAX $PERIOD，单位是微秒。50000 100000 的意思是：在每 100ms 的时间窗口内，这个 cgroup 最多获得 50ms 的 CPU 时间，即单核的 50%。容器内部失控的 CPU 循环会在内核调度器层面被限流。你的机器保持响应。\n将进程移入 1 2 3 4 5 must(os.WriteFile( filepath.Join(containerCgroup, \u0026#34;cgroup.procs\u0026#34;), []byte(strconv.Itoa(os.Getpid())), 0700, )) 之前的所有操作都只是配置。这行代码才是真正执行限制的。将我们的 PID 写入 cgroup.procs 会将当前进程移入 cgroup。从这一刻起，这个进程及其产生的所有子进程——包括 shell 和用户运行的所有东西——都将受到上述三个限制的约束。\n测试 在容器内部尝试一个 fork bomb：\n1 :(){ :|:\u0026amp; };: 由于 pids.max = 20，内核会在超过限制后拒绝每一个 fork() 调用。容器 shell 会死掉。在宿主机上打开另一个终端，一切正常运行。如果没有 cgroups，那个 fork bomb 会需要硬重启才能恢复。\n好了！你已经从零构建了一个真正的容器，具备了：使用命名空间的隔离、使用 chroot 的文件系统虚拟化、以及使用 cgroup v2 的资源限制。这本质上就是 Docker 在底层做的事情，只不过 Docker 有更多的功能、更好的用户体验和生产级工具。\n","permalink":"https://kerolt.github.io/p/%E8%AF%91%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%94%A8-go-%E6%9E%84%E5%BB%BA%E5%AE%B9%E5%99%A8%E7%8E%B0%E4%BB%A3%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4--cgroup-v2/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文是对 \u003ca href=\"https://dev.to/faizanfirdousi/build-a-container-from-scratch-in-go-modern-namespaces-cgroup-v2-5556?utm_source=dormosheio\u0026amp;utm_campaign=dormosheio\"\u003eBuild a Container from Scratch in Go (Modern Namespaces + cgroup v2)\u003c/a\u003e 这篇文章的翻译，以下是正文\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003cul\u003e\n\u003cli\u003e作者：\u003ca href=\"https://dev.to/faizanfirdousi\"\u003eFaizan Firdousi\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e发布时间：2026 年 2 月 18 日\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我用 Go 从零构建了一个容器，代码行数尽可能少，并在过程中了解了容器内部通常发生的各种事情——也就是 Docker 抽象掉的那些细节。\u003c/p\u003e","title":"【译】从零开始用 Go 构建容器：现代命名空间 + cgroup v2"},{"content":"1. 为什么 Go 需要汇编 Go 在以下场景允许直接嵌入汇编：\n性能热点：手写 SIMD 指令、避免编译器优化不足 系统调用：部分 runtime 代码（如 runtime·sys_linux_amd64.s）直接发起 syscall 引导/启动：rt0_linux_amd64.s 等启动代码必须在汇编层面完成栈和寄存器初始化 密码学：crypto/aes, crypto/sha256 等标准库大量使用汇编加速 Go 使用 Plan 9 汇编语法，与 AT\u0026amp;T / Intel 语法都不同。\n2. Plan 9 汇编基础 2.1 操作数顺序 Plan 9 汇编是源操作数在前，目标操作数在后（类似 AT\u0026amp;T，但无 % / $ 前缀）：\n1 2 ADDQ BX, AX // AX = AX + BX (Intel: add rax, rbx) MOVQ x+0(FP), AX // AX = *(FP + x_offset) 2.2 寄存器命名 通用寄存器 Plan 9 名 Intel 名 AX / EAX / AX:BX AX RAX BX BX RBX CX CX RCX DX DX RDX Stack Pointer SP RSP Base Pointer BP RBP R8–R15 R8–R15 R8–R15 X0–X15 (SSE) X0–X15 XMM0–XMM15 2.3 伪寄存器（核心概念） Go 汇编定义了四个伪寄存器，没有物理硬件对应：\n伪寄存器 含义 示例 SB Static Base — 全局符号基址 TEXT ·Sum(SB) 声明全局函数 FP Frame Pointer — 指向参数和返回值区域的起始 x+0(FP) 访问第一个参数 SP 栈指针 — 指向当前 goroutine 栈顶 MOVQ AX, local+0(SP) 局部变量 PC Program Counter — 指令地址 用于跳转表和 CALL 重点：FP 是函数参数/返回值的入口地址，硬件寄存器 SP 是 RSP，而汇编中的 SP 指的是局部变量区的基址。两者是不同的。\n2.4 指令后缀 后缀 操作数大小 对应 C 类型 B 1 字节 byte / uint8 W 2 字节 uint16 L 4 字节 uint32 Q 8 字节 uint64 / int (64-bit) 3. 函数声明与栈帧 3.1 TEXT 指令 1 TEXT ·函数名(SB), $帧大小-参数大小 ·函数名：·（U+00B7）是包分隔符，等价于 Go 的 . SB：使该符号全局可见，Go 链接器可找到它 $N-M： N：局部变量所需的栈空间（0 表示不需要） M：参数 + 返回值占用的总字节数 3.2 栈布局（amd64） 1 2 3 4 5 6 7 8 9 10 11 12 调用者视角: +------------------+ 高地址 | caller frame | +------------------+ | return addr | (8 bytes, 由 CALL 压栈) +------------------+ ← 被调函数的硬件 SP (RSP) | 参数1 x | x+0(FP) | 参数2 y | y+8(FP) | 返回值 | ret+16(FP) +------------------+ ← FP (伪寄存器) | 局部变量区 | local+0(SP) ... local+N(SP) +------------------+ ← 硬件 SP 最终位置 关键：FP 指向参数区的起始，SP（伪寄存器）指向局部变量区的起始。返回值存放在参数区的最末尾，紧接在参数之后。\n3.3 参数与返回值的偏移计算 对于 func Sum(x, y int) int（amd64，int = 8 字节）：\n1 2 3 4 FP + 0: x (8 bytes) FP + 8: y (8 bytes) FP + 16: ret (8 bytes) 总大小: 24 bytes 因此 TEXT 指令的帧大小应为 $0-24（0 字节局部变量 + 24 字节参数/返回值空间）。\n4. 实例解析 4.1 Go 侧 (main.go) 1 2 3 4 5 6 7 8 9 10 11 12 package main import \u0026#34;fmt\u0026#34; func main() { x := 10 y := 20 sum := Sum(x, y) fmt.Println(\u0026#34;Sum:\u0026#34;, sum) } func Sum(x, y int) int // 只声明，无函数体 Sum 只有签名没有实现体 Go 编译器看到这样的声明会自动查找同名汇编符号 注意：汇编函数名大小写敏感，Sum 大写表示导出（exported） 4.2 汇编侧 (add.s) 1 2 3 4 5 6 TEXT ·Sum(SB), $0-24 MOVQ x+0(FP), AX // AX = x MOVQ y+8(FP), BX // BX = y ADDQ BX, AX // AX = AX + BX MOVQ AX, ret+16(FP) // 将结果写入返回值位置 RET 逐行解释：\nMOVQ x+0(FP), AX — 从栈上偏移 0 处取出参数 x，放入 AX MOVQ y+8(FP), BX — 从栈上偏移 8 处取出参数 y，放入 BX ADDQ BX, AX — AX += BX MOVQ AX, ret+16(FP) — 将 AX 的值写入返回值槽位 RET — 函数返回，调用者从 ret+16(FP) 处读取结果 4.3 编译与运行 1 2 3 4 5 6 7 8 9 10 11 12 13 # 直接 go build，汇编文件 (.s) 会被自动识别 go build -o go-asm ./go-asm # 输出: Sum: 30 # 查看 Go 生成的汇编（对比编译器生成版本） go tool compile -S main.go # 只编译汇编文件 go tool asm add.s # 反汇编已编译的二进制 go tool objdump -s Sum go-asm 5. 调试与工具 5.1 验证栈帧大小 编译时设置 -asan 或者直接检查：\n1 go build -gcflags=\u0026#34;-live\u0026#34; 2\u0026gt;\u0026amp;1 | head 5.2 查看调用约定兼容性 1 2 # 确认寄存器传参是否被使用（Go 1.17+ ABI 内部变化） go build -gcflags=\u0026#34;-S\u0026#34; 2\u0026gt;\u0026amp;1 | grep Sum Go 1.17 起引入了基于寄存器的调用约定（ABIInternal），参数和返回值优先通过寄存器传递而非栈。但手写的汇编 .s 文件默认仍使用栈传参（ABI0）。如果需要兼容 ABIInternal，需要在 TEXT 指令中指定。\n6. 常见陷阱 6.1 帧大小标注不匹配 如果 TEXT 中声明的 $N-M 与实际使用的偏移量不一致，链接时可能不会报错但运行时会产生数据竞争或栈损坏。\n当前仓库中 add.s 的 $0-8 应修正为 $0-24（amd64 下 int 为 8 字节，参数 + 返回值共 24 字节）。\n6.2 符号命名 Go 中使用 ·（middle dot）分隔包名和函数名，不可用普通点 . 导出函数名（首字母大写）在汇编中同样需要大写 使用 \u0026quot;\u0026quot;.Sum 或 ·Sum 均可（后者是缩写形式） 6.3 栈对齐 x86-64 ABI 要求栈在 CALL 前 16 字节对齐。如果手动调整栈指针，需要确保对齐。\n6.4 寄存器保存 Go 汇编中，调用者需要保存 callee-saved 寄存器（BP、BX、R12–R15）。AX、CX、DX 等是调用者保存的 scratch 寄存器，可以随意使用。\n6.5 Go 版本差异 版本 变化 Go 1.17 引入 ABIInternal（寄存器传参），汇编默认仍使用 ABI0 Go 1.21+ 部分标准库汇编适配寄存器 ABI Go 1.22+ GOAMD64=v3 支持 AVX2 等新指令 7. 参考资源 A Quick Guide to Go\u0026rsquo;s Assembler — Go 官方汇编快速指南 go tool compile -S xxx.go — 查看编译器生成的汇编 go doc cmd/asm — 汇编器文档 标准库 crypto/ 目录下大量 .s 文件可供学习 ","permalink":"https://kerolt.github.io/p/go-%E4%B8%8E%E6%B1%87%E7%BC%96/","summary":"\u003ch2 id=\"1-为什么-go-需要汇编\"\u003e1. 为什么 Go 需要汇编\u003c/h2\u003e\n\u003cp\u003eGo 在以下场景允许直接嵌入汇编：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e性能热点\u003c/strong\u003e：手写 SIMD 指令、避免编译器优化不足\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e系统调用\u003c/strong\u003e：部分 runtime 代码（如 \u003ccode\u003eruntime·sys_linux_amd64.s\u003c/code\u003e）直接发起 syscall\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e引导/启动\u003c/strong\u003e：\u003ccode\u003ert0_linux_amd64.s\u003c/code\u003e 等启动代码必须在汇编层面完成栈和寄存器初始化\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e密码学\u003c/strong\u003e：\u003ccode\u003ecrypto/aes\u003c/code\u003e, \u003ccode\u003ecrypto/sha256\u003c/code\u003e 等标准库大量使用汇编加速\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eGo 使用 \u003cstrong\u003ePlan 9 汇编语法\u003c/strong\u003e，与 AT\u0026amp;T / Intel 语法都不同。\u003c/p\u003e","title":"Go 与汇编"},{"content":" 既然都 panic 了，不就是想让程序崩溃吗？为什么还要“恢复”它？\npanic 不一定是“程序该死”，更多时候是“当前请求/任务该死”。\n我们可以把程序分为两种崩溃模式：\n1. 致命错误 —— 不应该 recover 例如：配置文件缺失、数据库连接失败、关键初始化逻辑出错。\n这种错误发生后，程序已无法继续正常运行，panic 后不应该 recover，让程序崩溃退出是正确的。\n2. 局部错误 —— 应该 recover 很多 HTTP/RPC 服务、goroutine 子任务中，panic 只代表当前请求或子任务出错，而不代表整个进程该死。\n例如：\n一个 HTTP 请求的处理函数里数组越界 panic 一个 goroutine 里发生了未预期的空指针 如果不用 recover：\n整个进程崩溃 → 服务停止 → 影响所有用户 如果用 recover：\n该请求返回 500 错误 主进程继续运行，其他请求不受影响 示例对比 没有 recover（错误） 1 2 3 4 5 func handler(w http.ResponseWriter, r *http.Request) { var arr []int _ = arr[1] // panic } // 一个请求出错 → 整个程序崩溃 有 recover（正确） 1 2 3 4 5 6 7 8 9 10 11 12 func handler(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf(\u0026#34;panic: %v\u0026#34;, err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(\u0026#34;internal error\u0026#34;)) } }() var arr []int _ = arr[1] // panic 被捕获，该请求返回 500，进程继续 } 总结：什么时候 recover？ 场景 是否 recover 原因 初始化失败 ❌ 程序无法运行 HTTP 请求内 panic ✅ 只影响单个请求 goroutine 任务 ✅ 防止进程崩溃 明确不该出现的 bug 看情况 开发时可崩溃，生产环境建议 recover + 记录日志 panic 代表“当前逻辑无法继续执行”，但不代表“整个进程必须死”。 recover 是在进程层面保护边界（请求边界、任务边界），让错误可控、服务保持可用。 ","permalink":"https://kerolt.github.io/p/deferpanic%E4%B8%8Erecover/","summary":"\u003cblockquote\u003e\n\u003cp\u003e既然都 \u003ccode\u003epanic\u003c/code\u003e 了，不就是想让程序崩溃吗？为什么还要“恢复”它？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003epanic\u003c/code\u003e 不一定是“程序该死”，更多时候是“当前请求/任务该死”。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我们可以把程序分为两种崩溃模式：\u003c/p\u003e","title":"defer、panic与recover"},{"content":"一、常用输出函数（按输出目标分类） 函数 说明 示例 Print / Println / Printf 输出到标准输出 fmt.Println(\u0026quot;Hello\u0026quot;) Fprint / Fprintln / Fprintf 输出到任意 io.Writer fmt.Fprintf(os.Stderr, \u0026quot;err: %v\u0026quot;, err) Sprint / Sprintln / Sprintf 格式化结果返回字符串（不打印） s := fmt.Sprintf(\u0026quot;age=%d\u0026quot;, 20) Errorf 格式化后返回 error 类型 return fmt.Errorf(\u0026quot;invalid: %v\u0026quot;, val) Println/Sprintln/Fprintln 会自动在末尾加换行，Print 不加；Printf 需要显式写 \\n。\n二、常用格式占位符（格式化动词） 动词 用于 示例输出 %v 任意类型的默认格式 123 \u0026quot;abc\u0026quot; [1 2] %#v Go 语法表示（带类型/引号） \u0026quot;abc\u0026quot; []int{1,2} %T 值的类型 int string []int %% 百分号字面量 % 整数 %d 十进制 123 %b 二进制 1111011 %o 八进制 173 %x / %X 十六进制（小写/大写） 7b / 7B %c Unicode 字符 { (ASCII 123) 浮点数 %f 十进制小数 3.141593 %.2f 指定小数位数 3.14 %e / %E 科学计数法 3.141593e+00 %g 自动选择 %f 或 %e 更紧凑 字符串/字节切片 %s 原始字符串 hello %q 带双引号的字符串 \u0026quot;hello\u0026quot; %x 十六进制字节 68656c6c6f 指针 %p 十六进制地址 0xc0000140f0 布尔 %t true 或 false true 三、宽度与精度 通用格式：%[flags][width][.precision]verb\n示例 含义 %5d 宽度 5，右对齐，不足补空格 %-5d 宽度 5，左对齐 %05d 宽度 5，右对齐，前补零 %5.2f 总宽度 5（含小数点），小数 2 位 %5s 最小宽度 5 的字符串 %q 或 %x 也可跟宽度/精度 四、常用辅助函数 函数 作用 fmt.Scan / Scanf / Scanln 从标准输入读取格式化数据 fmt.Fscan / Fscanf 从 io.Reader 读取 fmt.Sscan / Sscanf 从字符串解析 五、自定义格式化（Stringer 接口） 任何类型实现 String() string 方法后，使用 %v、%s、Print 等都会自动调用该方法。\n1 2 3 4 type Person struct { Name string; Age int } func (p Person) String() string { return fmt.Sprintf(\u0026#34;%s (%d)\u0026#34;, p.Name, p.Age) } 六、性能提示 Sprintf 有内存分配开销；频繁调用可考虑 strings.Builder + fmt.Fprintf。 类型断言比 %v 反射更快，但对通用代码可接受。 ","permalink":"https://kerolt.github.io/p/go%E4%B8%ADfmt%E7%9A%84%E5%B8%B8%E8%A7%81%E7%94%A8%E6%B3%95%E6%80%BB%E7%BB%93/","summary":"\u003ch2 id=\"一常用输出函数按输出目标分类\"\u003e一、常用输出函数（按输出目标分类）\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e函数\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e说明\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e示例\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePrint\u003c/code\u003e / \u003ccode\u003ePrintln\u003c/code\u003e / \u003ccode\u003ePrintf\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e输出到标准输出\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003efmt.Println(\u0026quot;Hello\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eFprint\u003c/code\u003e / \u003ccode\u003eFprintln\u003c/code\u003e / \u003ccode\u003eFprintf\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e输出到任意 \u003ccode\u003eio.Writer\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003efmt.Fprintf(os.Stderr, \u0026quot;err: %v\u0026quot;, err)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSprint\u003c/code\u003e / \u003ccode\u003eSprintln\u003c/code\u003e / \u003ccode\u003eSprintf\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e格式化结果返回字符串（不打印）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003es := fmt.Sprintf(\u0026quot;age=%d\u0026quot;, 20)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eErrorf\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e格式化后返回 \u003ccode\u003eerror\u003c/code\u003e 类型\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ereturn fmt.Errorf(\u0026quot;invalid: %v\u0026quot;, val)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003ePrintln\u003c/code\u003e/\u003ccode\u003eSprintln\u003c/code\u003e/\u003ccode\u003eFprintln\u003c/code\u003e 会自动在末尾加换行，\u003ccode\u003ePrint\u003c/code\u003e 不加；\u003ccode\u003ePrintf\u003c/code\u003e 需要显式写 \u003ccode\u003e\\n\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Go中fmt的常见用法总结"},{"content":"/sys/fs/bpf 是 Linux 内核中 BPF 文件系统 的挂载点。简单来说，它是 BPF 对象（如 Maps 和 Programs）在用户空间文件系统中的“家”。\n持久化与命名空间化 在 BPF 文件系统出现之前，BPF 程序和 Map 的生命周期完全依赖于持有其文件描述符的进程。如果创建 BPF Map 的进程退出了，Map 也就随之销毁了。\n/sys/fs/bpf 解决了这个问题，它提供了两个核心能力：\n持久化： 当将一个 BPF Map 或 BPF 程序 pin 到这个文件系统中时，即使创建它的进程结束，只要内核还在，这个对象就会一直存在。其他进程可以随后通过读取该文件来获取对象的句柄，从而实现数据的共享和程序的复用。 命名空间隔离： 它遵循文件系统的权限模型和命名空间规则。不同的容器或用户可以在各自的目录下管理 BPF 对象，避免了冲突，也增加了安全性。 Pin 当用户空间程序加载一个 BPF 程序或创建一个 Map 时，内核会在内存中创建相应的数据结构，并返回一个文件描述符给用户空间。\n默认情况下，当 fd 关闭（进程退出）时，内核引用计数归零，对象被销毁。\n用户空间程序可以调用 bpf_obj_pin() 系统调用，将这个 fd 与 /sys/fs/bpf/ 下的一个路径绑定：\n这会在该路径下创建一个文件（这就是你看到的那些文件）。 内核会增加该对象的引用计数。 只要该文件存在，对象就不会被内核销毁。 目录下的文件是什么？ 在 /sys/fs/bpf 下执行 ls -l 时，看到的文件实际上是 内核 BPF 对象的句柄。\nMap 文件： 代表一个 Key-Value 存储结构。多个 BPF 程序可以共享同一个 Map 文件来交换数据（例如：一个程序统计流量，另一个程序读取统计数据）。 Program 文件： 代表编译好的 BPF 字节码。可以被附加到 Hook 点（如 XDP, Tracepoint, TC 等）上执行。 它们不存储文本或二进制数据流，无法用 cat 读取 Map 里的内容（虽然可以用 bpftool 命令操作）。\n自动挂载机制 在现代 Linux 发行版中，Systemd 通常会自动挂载 bpffs。你可以通过 mount 命令验证：\n1 2 3 mount | grep bpf # 输出通常类似： # bpffs on /sys/fs/bpf type bpf (rw,relatime) 如果没有挂载，你需要手动挂载才能使用 BPF 的文件系统特性：\n1 mount -t bpf none /sys/fs/bpf 实际工作流程示例 假设你编写了一个 BPF 程序来统计网络包：\n加载： 你的用户空间加载器（Loader）编译并加载 BPF 程序到内核。 创建 Map： 程序创建了一个 hash map 用来存储 IP 地址和计数。 Pin（关键步骤）： 加载器调用系统调用，将 Map 钉在 /sys/fs/bpf/my_stats_map。 将程序钉在 /sys/fs/bpf/my_xdp_prog。 进程退出： 加载器完成任务后退出了。 结果： BPF 程序依然在网卡上运行（如果已附加）。 Map 里的数据依然在内存中。 另一个全新的监控程序可以打开 /sys/fs/bpf/my_stats_map，拿到 fd，读取里面的统计数据。 运维和开发人员通常使用 bpftool 来与这个目录交互，而不是直接操作文件。\nbpftool map list：列出系统中所有的 Map。 bpftool map pin id \u0026lt;ID\u0026gt; /sys/fs/bpf/my_map：将一个 Map 钉入文件系统。 bpftool prog load my.bpf.o /sys/fs/bpf/my_prog：直接将编译好的对象文件加载并钉住。 ","permalink":"https://kerolt.github.io/p/%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3sys-fs-bpf/","summary":"\u003cp\u003e\u003ccode\u003e/sys/fs/bpf\u003c/code\u003e 是 Linux 内核中 \u003cstrong\u003eBPF 文件系统\u003c/strong\u003e 的挂载点。简单来说，它是 \u003cstrong\u003eBPF 对象（如 Maps 和 Programs）在用户空间文件系统中的“家”\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"持久化与命名空间化\"\u003e持久化与命名空间化\u003c/h2\u003e\n\u003cp\u003e在 BPF 文件系统出现之前，BPF 程序和 Map 的生命周期完全依赖于持有其文件描述符的进程。如果创建 BPF Map 的进程退出了，Map 也就随之销毁了。\u003c/p\u003e","title":"如何理解sys-fs-bpf"},{"content":"What 本地的 kind / minikube 集群拉取镜像时，总是报同一个错误：\n1 spec.containers{nginx}: Failed to pull image \u0026#34;nginx:alpine\u0026#34;: failed to pull and unpack image \u0026#34;docker.io/library/nginx:alpine\u0026#34;: failed to resolve reference \u0026#34;docker.io/library/nginx:alpine\u0026#34;: failed to do request: Head \u0026#34;https://registry-1.docker.io/v2/library/nginx/manifests/alpine \u0026#34;: proxyconnect tcp: dial tcp 127.0.0.1:7890: connect: connection refused 明明宿主机上已经开着代理（Clash，7890 端口），为什么容器里还是连不上？\n核心原因：网络隔离层级搞错了 kind 的 K8s 节点、minikube（docker driver）的节点，本质都是运行在宿主机上的 Docker 容器。\n宿主机视角：127.0.0.1:7890 指向宿主机上运行的代理软件。 节点（容器）视角：容器内访问 127.0.0.1:7890，访问的是容器自己的 Loopback 接口，而不是宿主机的。 结果：容器内部根本没有监听 7890 的服务 → connection refused。 所以配置里不能写 127.0.0.1，必须让节点访问\u0026quot;宿主机在容器网络里对应的那个 IP\u0026quot;。而不同运行时，这个 IP 不一样：\n运行时 节点所在网段 宿主机在节点眼里的 IP kind（docker0） 172.17.0.0/16 172.17.0.1（docker0 网关） minikube（docker driver） 192.168.49.0/24 192.168.49.1（minikube 默认集群网段） 代理配置 首先，需要注意两个前提：\n代理必须监听容器可达的地址。Clash 等要开启 Allow LAN（监听 0.0.0.0:\u0026lt;port\u0026gt;），否则即使写了正确的 IP，172.17.x.x / 192.168.x.x 这类非本机流量也会被代理拒绝。 NO_PROXY 必须放行集群内部网段。否则集群组件间通信的 Service ClusterIP、Pod CIDR、.svc 域名等等也会被错误地送进代理，导致集群通信异常。这是最容易被忽略、也最容易踩坑的一条。 Kind 的配置方法 Kind 走的是容器运行时（docker 或 containerd）的环境变量，用 systemd drop-in 注入：\ndocker：/etc/systemd/system/docker.service.d/http-proxy.conf containerd：/etc/systemd/system/containerd.service.d/http-proxy.conf 1 2 3 4 [Service] Environment=\u0026#34;HTTP_PROXY=http://172.17.0.1:7890\u0026#34; Environment=\u0026#34;HTTPS_PROXY=http://172.17.0.1:7890\u0026#34; Environment=\u0026#34;NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.0.0.0/8,192.168.0.0/16,.svc,.cluster.local\u0026#34; 重启生效：\n1 2 sudo systemctl daemon-reload sudo systemctl restart docker Minikube 的配置方法 minikube 节点在 192.168.49.0/24 网段，宿主机对节点是 192.168.49.1。\n方法一：重建节点时通过 --docker-env 传（若已有节点需先 minikube delete）：\n1 2 3 4 minikube start \\ --docker-env HTTP_PROXY=http://192.168.49.1:7890 \\ --docker-env HTTPS_PROXY=http://192.168.49.1:7890 \\ --docker-env NO_PROXY=localhost,127.0.0.1,::1,192.168.49.0/24,10.96.0.0/12,10.244.0.0/16,.svc,.cluster.local 方法二：在原节点内部给 docker 运行时加环境变量（docker driver 可用 minikube profile list 确认）：\n1 2 3 4 5 6 7 8 9 10 11 12 sudo mkdir -p /etc/systemd/system/docker.service.d sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; [Service] Environment=\u0026#34;HTTP_PROXY=http://192.168.49.1:7890\u0026#34; Environment=\u0026#34;HTTPS_PROXY=http://192.168.49.1:7890\u0026#34; Environment=\u0026#34;NO_PROXY=localhost,127.0.0.1,192.168.49.0/24,10.96.0.0/12,10.244.0.0/16,.svc,.cluster.local\u0026#34; EOF sudo systemctl daemon-reexec sudo systemctl daemon-reload sudo systemctl restart docker 配置前先确认节点能连通代理端口（在 minikube 节点内执行）：\n1 2 nc -vz 192.168.49.1 7890 # Connection to 192.168.49.1 7890 port [tcp/*] succeeded! ","permalink":"https://kerolt.github.io/p/kind-%E4%B8%8E-minikube-%E9%85%8D%E7%BD%AE%E4%BB%A3%E7%90%86/","summary":"\u003ch2 id=\"what\"\u003eWhat\u003c/h2\u003e\n\u003cp\u003e本地的 kind / minikube 集群拉取镜像时，总是报同一个错误：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003espec.containers\u003cspan class=\"o\"\u003e{\u003c/span\u003enginx\u003cspan class=\"o\"\u003e}\u003c/span\u003e: Failed to pull image \u003cspan class=\"s2\"\u003e\u0026#34;nginx:alpine\u0026#34;\u003c/span\u003e: failed to pull and unpack image \u003cspan class=\"s2\"\u003e\u0026#34;docker.io/library/nginx:alpine\u0026#34;\u003c/span\u003e: failed to resolve reference \u003cspan class=\"s2\"\u003e\u0026#34;docker.io/library/nginx:alpine\u0026#34;\u003c/span\u003e: failed to \u003cspan class=\"k\"\u003edo\u003c/span\u003e request: Head \u003cspan class=\"s2\"\u003e\u0026#34;https://registry-1.docker.io/v2/library/nginx/manifests/alpine \u0026#34;\u003c/span\u003e: proxyconnect tcp: dial tcp 127.0.0.1:7890: connect: connection refused\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e明明宿主机上已经开着代理（Clash，7890 端口），为什么容器里还是连不上？\u003c/p\u003e","title":"Kind 与 Minikube 配置代理"},{"content":" 本篇文章由 AI 辅助完成\n在 eBPF（extended Berkeley Packet Filter）生态中，Ring Buffer 和 Perf Buffer 都是用于将内核态采集的数据高效传递到用户态的机制。但从 Linux 5.8 开始引入的 Ring Buffer 被认为在多数场景下优于传统的 Perf Buffer，主要原因包括以下几点：\n1. 内存模型与数据一致性（Memory Model \u0026amp; Data Consistency） Perf Buffer： 基于 per-CPU 的 ring buffer（每个 CPU 一个独立 buffer）。 用户态需轮询所有 CPU 的 buffer，增加了复杂度。 写入时若跨页（跨越单个 perf event buffer 的页面边界），会丢弃整个样本（sample loss）。 不保证写入原子性：如果事件大于 buffer 剩余空间，会被截断或丢弃。 Ring Buffer： 单一共享 buffer（可选 per-CPU 模式，但默认是全局的）。 支持 原子 reserve-commit 语义：先 bpf_ringbuf_reserve() 预留空间，填充后再 bpf_ringbuf_submit() 提交。这确保了即使事件较大，也不会因 buffer 空间不足而被静默丢弃。 若空间不足，reserve 直接失败，用户程序可感知并处理（如降级、计数等），避免 silent data loss。 💡 底层原理：Ring Buffer 使用 memory-mapped shared memory + consumer/producer index（类似 DPDK 或 Linux kernel 的 kfifo），通过 memory barrier 保证 SMP 下的一致性。\n2. Overhead 与性能 Perf Buffer： 每次写入触发 perf_event_output()，涉及较多内核路径（包括 IRQ context 切换、perf subsystem 调度等）。 在高吞吐场景下，容易成为瓶颈，尤其当多个 eBPF 程序同时写入时。 Ring Buffer： 更轻量：直接操作 shared memory，无需经过 perf subsystem。 用户态通过 epoll 或 busy-poll 方式消费，延迟更低。 实测表明，在相同负载下，Ring Buffer 的 CPU overhead 明显更低。 3. API 友好性与错误处理 Perf Buffer： 用户态需使用 libbpf 或 bcc 提供的封装，处理 per-CPU buffer 合并逻辑。 错误（如丢包）难以追踪。 Ring Buffer： 提供清晰的 reserve/submit/discard 接口，便于实现 backpressure 或采样策略。 用户态通过 ring_buffer__poll() 统一消费，代码更简洁。 支持“丢弃通知”（通过 BPF_RB_NO_WAKEUP / BPF_RB_FORCE_WAKEUP 控制唤醒行为）。 4. 功能扩展性 Ring Buffer 支持 带标志位的消息提交（如 BPF_RB_FORCE_WAKEUP），允许精细控制是否唤醒用户态消费者，这对低延迟或批处理场景非常有用。 Perf Buffer 无此类控制机制。 对比总结表 特性 Perf Buffer Ring Buffer 内存模型 Per-CPU 独立 buffer 全局共享 buffer（默认） 原子写入 ❌（大事件可能丢弃） ✅（reserve/commit 保证） 数据丢失处理 静默丢弃，难追踪 可检测、可处理 CPU Overhead 较高（走 perf 子系统） 较低（直接 mmap） API 复杂度 高（需合并多 CPU） 低（统一接口） 唤醒控制 无 支持精细控制 最低内核版本 ~4.4+ 5.8+ ","permalink":"https://kerolt.github.io/p/ringbuffer%E4%B8%8Eperfbuffer/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本篇文章由 AI 辅助完成\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在 eBPF（extended Berkeley Packet Filter）生态中，\u003cstrong\u003eRing Buffer\u003c/strong\u003e 和 \u003cstrong\u003ePerf Buffer\u003c/strong\u003e 都是用于将内核态采集的数据高效传递到用户态的机制。但从 Linux 5.8 开始引入的 \u003cstrong\u003eRing Buffer\u003c/strong\u003e 被认为在多数场景下优于传统的 \u003cstrong\u003ePerf Buffer\u003c/strong\u003e，主要原因包括以下几点：\u003c/p\u003e","title":"RingBuffer与PerfBuffer"},{"content":" 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 --- # 1. 扩大匹配范围，确保头文件也能享受 BPF 配置 If: PathMatch: [.*\\.bpf\\.c, .*/bpf/.*\\.c, .*\\.h] # 增加对 .h 的支持 CompileFlags: Add: - \u0026#34;-target\u0026#34; - \u0026#34;bpf\u0026#34; - \u0026#34;-x\u0026#34; - \u0026#34;c\u0026#34; - \u0026#34;-std=gnu11\u0026#34; - \u0026#34;-D__BPF_TRACING__\u0026#34; - \u0026#34;-D__TARGET_ARCH_x86\u0026#34; # 必须指定架构，否则 vmlinux.h 内部的部分条件编译会失效 - \u0026#34;-I.\u0026#34; # 确保当前目录的头文件优先级最高 - \u0026#34;-I/usr/include/bpf\u0026#34; # 指向 libbpf 头文件路径 - \u0026#34;-Wno-unknown-attributes\u0026#34; # 忽略不认识的属性 - \u0026#34;-ferror-limit=0\u0026#34; # 即使错误再多也不要停止解析，这对补全很重要 Remove: - \u0026#34;-std=c++*\u0026#34; - \u0026#34;-fexceptions\u0026#34; Diagnostics: # 2. 针对 vmlinux.h 的重定义问题进行精准打击 Suppress: - \u0026#34;redefinition\u0026#34; # 抑制所有的符号重定义 - \u0026#34;typedef_redefinition\u0026#34; # 特别针对 typedef 的冲突 - \u0026#34;too_many_errors\u0026#34; # 彻底关掉那个烦人的警告 - \u0026#34;err_expected_identifer\u0026#34; # 针对 vmlinux 内部属性解析失败的补救 --- # 3. 针对特定的 vmlinux.h 文件彻底关闭检查 If: PathMatch: .*vmlinux\\.h Diagnostics: Suppress: [\u0026#34;*\u0026#34;] ","permalink":"https://kerolt.github.io/p/%E6%9B%B4%E9%80%82%E5%90%88ebpf%E7%A8%8B%E5%BA%8F%E5%AE%9D%E5%AE%9D%E4%BD%93%E8%B4%A8%E7%9A%84clangd%E9%85%8D%E7%BD%AE/","summary":"\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nn\"\u003e---\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 1. 扩大匹配范围，确保头文件也能享受 BPF 配置\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eIf\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ePathMatch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003e.*\\.bpf\\.c, .*/bpf/.*\\.c, .*\\.h]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c\"\u003e# 增加对 .h 的支持\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eCompileFlags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-target\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;bpf\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-x\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;c\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-std=gnu11\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-D__BPF_TRACING__\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-D__TARGET_ARCH_x86\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"c\"\u003e# 必须指定架构，否则 vmlinux.h 内部的部分条件编译会失效\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-I.\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e                      \u003c/span\u003e\u003cspan class=\"c\"\u003e# 确保当前目录的头文件优先级最高\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-I/usr/include/bpf\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"c\"\u003e# 指向 libbpf 头文件路径\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-Wno-unknown-attributes\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# 忽略不认识的属性\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-ferror-limit=0\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e          \u003c/span\u003e\u003cspan class=\"c\"\u003e# 即使错误再多也不要停止解析，这对补全很重要\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eRemove\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-std=c++*\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;-fexceptions\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eDiagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c\"\u003e# 2. 针对 vmlinux.h 的重定义问题进行精准打击\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eSuppress\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;redefinition\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e              \u003c/span\u003e\u003cspan class=\"c\"\u003e# 抑制所有的符号重定义\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;typedef_redefinition\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"c\"\u003e# 特别针对 typedef 的冲突\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;too_many_errors\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e           \u003c/span\u003e\u003cspan class=\"c\"\u003e# 彻底关掉那个烦人的警告\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;err_expected_identifer\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# 针对 vmlinux 内部属性解析失败的补救\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nn\"\u003e---\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# 3. 针对特定的 vmlinux.h 文件彻底关闭检查\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eIf\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ePathMatch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e.*vmlinux\\.h\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eDiagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eSuppress\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;*\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"更适合eBPF程序宝宝体质的clangd配置"},{"content":"环境：Fedora 43，NetworkManager 管理网络，基于 nmcli（NetworkManager Command Line Interface）操作。\nWiFi 连接全流程 1. 确认网卡状态 1 nmcli device status 输出示例：\nDEVICE TYPE STATE CONNECTION wlan0 wifi disconnected -- eth0 ethernet connected Wired connection 1 如果 STATE 为 unmanaged，说明网卡未被 NetworkManager 接管：\n1 sudo nmcli device set wlan0 managed yes 2. 扫描可用网络 1 nmcli device wifi list 输出示例：\nIN-USE SSID MODE CHAN RATE SIGNAL BARS SECURITY MyHome_5G Infra 149 1300 Mbit/s 75 ▂▄▆_ WPA2 MyHome_2.4G Infra 6 1300 Mbit/s 90 ▂▄▆█ WPA3 Neighbor_WiFi Infra 1 195 Mbit/s 30 ▂___ WPA2 * Office_Guest Infra 36 540 Mbit/s 60 ▂▄▆_ WPA2 IN-USE 列带 * 表示当前已连接的网络。\n3. 连接 WiFi 基本连接（自动保存配置） 1 sudo nmcli device wifi connect \u0026#34;MyHome_5G\u0026#34; password \u0026#34;your_password\u0026#34; 成功输出：\nDevice \u0026#39;wlan0\u0026#39; successfully activated with \u0026#39;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\u0026#39; 此命令自动完成：生成 connection profile（保存在 /etc/NetworkManager/system-connections/）→ 触发 DHCP → 设置 autoconnect=yes。\n连接隐藏 SSID 1 sudo nmcli device wifi connect \u0026#34;Hidden_SSID\u0026#34; password \u0026#34;your_password\u0026#34; hidden yes 指定加密方式（避免协商不确定性） 1 sudo nmcli device wifi connect \u0026#34;MyHome_5G\u0026#34; password \u0026#34;your_password\u0026#34; wep-key-type key 4. 查看活动连接与 IP 1 2 3 nmcli connection show --active ip addr show wlan0 ping -c 3 8.8.8.8 5. 切换 WiFi（断旧连新一步完成） 1 sudo nmcli device wifi connect \u0026#34;New_SSID\u0026#34; password \u0026#34;new_password\u0026#34; nmcli device wifi connect 会自动断开当前连接，无需手动先行断开。\n6. 断开当前 WiFi 方式一：断开网卡（不会自动重连） 1 sudo nmcli device disconnect wlan0 方式二：禁用连接配置（autoconnect 可能触发重连） 1 sudo nmcli connection down \u0026#34;MyHome_5G\u0026#34; 方式三：彻底删除配置 1 sudo nmcli connection delete \u0026#34;MyHome_5G\u0026#34; nmcli 网卡操作参考 查看设备与连接 命令 用途 nmcli device status 查看所有网卡状态、类型、连接情况 nmcli device show wlan0 查看指定网卡详细信息（驱动、MAC、IP、DNS 等） nmcli connection show 列出所有 connection profile nmcli connection show --active 列出当前活动的连接 nmcli connection show \u0026quot;MyHome_5G\u0026quot; 查看指定连接配置详情 设备管理 命令 用途 nmcli device connect wlan0 让网卡尝试自动连接（根据已有 profile） nmcli device disconnect wlan0 断开网卡，不再自动连接 nmcli device set wlan0 managed yes 将网卡纳入 NetworkManager 管理 nmcli device set wlan0 managed no 将网卡排除出管理（变为 unmanaged） nmcli device reapply wlan0 重新应用当前连接配置 nmcli device wifi list 扫描并列出附近 WiFi nmcli device wifi rescan 强制重新扫描 nmcli device monitor wlan0 实时监控网卡状态变化 Connection Profile 管理 modify 常用示例 1 2 3 4 5 6 7 8 9 10 11 # 设置静态 IP sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; ipv4.addresses 192.168.1.100/24 sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; ipv4.gateway 192.168.1.1 sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; ipv4.dns \u0026#34;8.8.8.8,1.1.1.1\u0026#34; sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; ipv4.method manual # 关闭自动连接 sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; connection.autoconnect no # 设置连接优先级（数字越大越优先） sudo nmcli con modify \u0026#34;MyHome_5G\u0026#34; connection.autoconnect-priority 10 全局设置 命令 用途 nmcli general status 查看 NetworkManager 整体状态 nmcli general hostname 查看/设置主机名 nmcli general set wifi.on-demand on 仅在需要时启用 WiFi（省电/安全） nmcli general permissions 查看当前用户权限 配置文件操作 直接编辑后重载：\n1 2 sudo vim /etc/NetworkManager/system-connections/MyHome_5G.nmconnection sudo nmcli connection reload 配置文件为 INI 格式，手动修改后必须 reload 才能生效。注意设置权限：\n1 2 sudo chmod 600 /etc/NetworkManager/system-connections/*.nmconnection sudo chown root:root /etc/NetworkManager/system-connections/*.nmconnection ","permalink":"https://kerolt.github.io/p/%E4%BD%BF%E7%94%A8nmcli%E7%AE%A1%E7%90%86wi-fi%E8%BF%9E%E6%8E%A5/","summary":"\u003cp\u003e环境：Fedora 43，NetworkManager 管理网络，基于 \u003ccode\u003enmcli\u003c/code\u003e（NetworkManager Command Line Interface）操作。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"wifi-连接全流程\"\u003eWiFi 连接全流程\u003c/h2\u003e\n\u003ch3 id=\"1-确认网卡状态\"\u003e1. 确认网卡状态\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli device status\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e输出示例：\u003c/p\u003e","title":"使用nmcli管理Wi-Fi连接"},{"content":" 将 Go 的默认工作 Go 目录从 $HOME/go 改为 $HOME/.go ，这个行为由环境变量 GOPATH 控制\n核心原理：GOPATH 与现代 Go 的关系 自 Go 1.11 引入 Go Modules 后，项目源码不再强制依赖 GOPATH/src，但以下内容仍受 GOPATH 影响：\n路径 用途 是否受 GOPATH 影响 $GOPATH/bin go install 安装的可执行文件 ✅ 是 $GOPATH/pkg/mod Module 缓存（Go 1.16+ 由 GOMODCACHE 控制） ⚠️ 部分 $GOPATH/src 传统 GOPATH 模式源码 ❌ Modules 模式下基本不用 因此，修改 GOPATH 主要影响二进制安装路径和部分缓存位置，不影响已启用 modules 的项目构建。\n迁移步骤（Fedora 环境） 1. 设置环境变量 在 ~/.bashrc 或 ~/.zshrc 中添加：\n1 2 3 4 5 # Go workspace 配置 export GOPATH=\u0026#34;$HOME/.go\u0026#34; export GOBIN=\u0026#34;$GOPATH/bin\u0026#34; # 显式指定二进制安装目录 export GOMODCACHE=\u0026#34;$GOPATH/pkg/mod\u0026#34; # 可选：统一 module 缓存位置 export PATH=\u0026#34;$GOBIN:$PATH\u0026#34; # 确保安装的工具可直接执行 💡 提示：Fedora 默认使用 Bash，建议修改 ~/.bashrc；若使用 Zsh 则改 ~/.zshrc。\n使配置生效：\n1 source ~/.bashrc # 或 source ~/.zshrc 2. 迁移现有数据 1 2 3 4 5 6 7 8 9 10 # 创建新目录结构 mkdir -p ~/.go/{bin,pkg,src} # 迁移已有内容（保留原目录作为备份） mv ~/go/bin/* ~/.go/bin/ 2\u0026gt;/dev/null || true mv ~/go/pkg/* ~/.go/pkg/ 2\u0026gt;/dev/null || true mv ~/go/src/* ~/.go/src/ 2\u0026gt;/dev/null || true # 验证后可删除原目录 # rm -rf ~/go 3. 验证配置 1 2 3 4 5 go env GOPATH GOBIN GOMODCACHE # 应输出： # /home/yourname/.go # /home/yourname/.go/bin # /home/yourname/.go/pkg/mod 测试安装一个工具验证路径：\n1 2 3 go install golang.org/dl/go1.21@latest which go1.21 # 应返回：/home/yourname/.go/bin/go1.21 ","permalink":"https://kerolt.github.io/p/%E6%9B%B4%E6%94%B9go%E7%9A%84%E9%BB%98%E8%AE%A4%E5%AE%89%E8%A3%85%E4%BD%8D%E7%BD%AE/","summary":"\u003cblockquote\u003e\n\u003cp\u003e将 Go 的默认工作 Go 目录从 \u003ccode\u003e$HOME/go\u003c/code\u003e 改为 \u003ccode\u003e$HOME/.go\u003c/code\u003e ，这个行为由环境变量 \u003ccode\u003eGOPATH\u003c/code\u003e 控制\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"核心原理gopath-与现代-go-的关系\"\u003e核心原理：\u003ccode\u003eGOPATH\u003c/code\u003e 与现代 Go 的关系\u003c/h2\u003e\n\u003cp\u003e自 Go 1.11 引入 \u003cstrong\u003eGo Modules\u003c/strong\u003e 后，项目源码不再强制依赖 \u003ccode\u003eGOPATH/src\u003c/code\u003e，但以下内容仍受 \u003ccode\u003eGOPATH\u003c/code\u003e 影响：\u003c/p\u003e","title":"更改Go的默认安装位置"},{"content":"一、生成 SSH 密钥对（在本地机器上） 打开终端（Linux/macOS）或使用 PowerShell/WSL（Windows）。 执行以下命令生成密钥对： 1 ssh-keygen -t rsa -b 4096 -C \u0026#34;your_email@example.com\u0026#34; -t rsa：指定密钥类型为 RSA（也可以使用 -t ed25519 更安全）。 -b 4096：密钥长度（RSA 推荐 4096 位）。 -C：添加注释（通常是邮箱，可选）。 按提示操作： 保存路径：直接回车使用默认路径 ~/.ssh/id_rsa。 设置密码（passphrase）：可选，增强安全性。如果希望完全免密，直接回车留空。 生成后会在 ~/.ssh/ 目录下创建两个文件：\nid_rsa：私钥（不要泄露） id_rsa.pub：公钥（用于上传到服务器） 二、将公钥上传到远程服务器 方法一：使用 ssh-copy-id 1 ssh-copy-id username@server_ip 例如：\n1 ssh-copy-id user@192.168.1.100 系统会提示你输入远程用户的密码，验证通过后，公钥会被自动追加到服务器的 ~/.ssh/authorized_keys 文件中。\n⚠️ 如果提示 command not found，说明 ssh-copy-id 不存在（常见于 macOS 或某些 Linux 发行版），请使用方法二。\n方法二：手动复制公钥 查看本地公钥内容： 1 cat ~/.ssh/id_rsa.pub 复制输出的整段内容（以 ssh-rsa AAAAB3... 开头）。\n登录远程服务器： 1 ssh username@server_ip 在服务器上创建 .ssh 目录（如果不存在）： 1 mkdir -p ~/.ssh 将公钥内容写入 authorized_keys 文件： 1 echo \u0026#34;你复制的公钥内容\u0026#34; \u0026gt;\u0026gt; ~/.ssh/authorized_keys 注意替换“你复制的公钥内容”为实际内容。\n设置正确的权限（非常重要！SSH 要求严格权限）： 1 2 3 chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chown $USER:$USER ~/.ssh -R 三、配置别名 配置别名：编辑 ~/.ssh/config，简化连接命令： Host myserver HostName 192.168.1.100 User myuser IdentityFile ~/.ssh/id_rsa 之后只需运行：\n1 ssh myserver ","permalink":"https://kerolt.github.io/p/linux-ssh%E5%85%8D%E5%AF%86%E7%99%BB%E9%99%86/","summary":"\u003ch2 id=\"一生成-ssh-密钥对在本地机器上\"\u003e一、生成 SSH 密钥对（在本地机器上）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e打开终端（Linux/macOS）或使用 PowerShell/WSL（Windows）。\u003c/li\u003e\n\u003cli\u003e执行以下命令生成密钥对：\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003essh-keygen -t rsa -b \u003cspan class=\"m\"\u003e4096\u003c/span\u003e -C \u003cspan class=\"s2\"\u003e\u0026#34;your_email@example.com\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e-t rsa\u003c/code\u003e：指定密钥类型为 RSA（也可以使用 \u003ccode\u003e-t ed25519\u003c/code\u003e 更安全）。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e-b 4096\u003c/code\u003e：密钥长度（RSA 推荐 4096 位）。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e-C\u003c/code\u003e：添加注释（通常是邮箱，可选）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e按提示操作：\n\u003cul\u003e\n\u003cli\u003e保存路径：直接回车使用默认路径 \u003ccode\u003e~/.ssh/id_rsa\u003c/code\u003e。\u003c/li\u003e\n\u003cli\u003e设置密码（passphrase）：可选，增强安全性。如果希望完全免密，直接回车留空。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e生成后会在 \u003ccode\u003e~/.ssh/\u003c/code\u003e 目录下创建两个文件：\u003c/p\u003e","title":"Linux SSH免密登陆"},{"content":"引言 在性能分析和系统调优中，了解应用程序在运行时的调用栈信息至关重要。在 【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】 这篇文章中，其利用 Rust + eBPF 构建了一个 profiler 工具，抱着学习的目的，我使用 C++ 实现了相同的功能，并将所学知识总结成本文。\nperf_event_open 机制解析 perf 原理图：https://plantegg.github.io/2021/05/16/Perf_IPC%E4%BB%A5%E5%8F%8ACPU%E5%88%A9%E7%94%A8%E7%8E%87/\nperf_event_open 是 Linux 内核提供的性能监控接口，允许用户空间程序创建性能事件计数器。当用户态程序调用 sys_perf_event_open 系统调用时，内核会创建相应的性能监控实例。以下是基本的使用方式：\n1 2 3 4 5 6 7 8 #include \u0026lt;linux/perf_event.h\u0026gt; #include \u0026lt;sys/syscall.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int64_t perf_event_open(struct perf_event_attr* hw_event, pid_t pid, int cpu, int group_fd, unsigned long flags) { return syscall(SYS_perf_event_open, hw_event, pid, cpu, group_fd, flags); } 以文件描述符为中心的监控单元 Linux 遵循 \u0026quot; 一切皆文件 \u0026quot; 的设计哲学，perf_event_open 也不例外。每次调用该函数，内核都会创建一个独立的性能监控实例，并返回一个文件描述符。\n核心概念：\n1 fd = 1 Event：每个文件描述符对应一个具体的监控任务，例如 \u0026quot; 监控 CPU 0 的指令执行数 \u0026quot; 事件分组 (Group)：多个文件描述符可以绑定在一起形成事件组 分组的重要性： 假设需要计算 \u0026quot; 每条指令的缓存未命中率 \u0026ldquo;，需要同时监控 \u0026quot; 指令数 \u0026quot; 和 \u0026quot; 缓存未命中数 \u0026ldquo;。将它们放入同一事件组可以确保：\n两个事件同时开始、同时结束计数 硬件计数器同步采样，保证计算比例的准确性 减少系统调用开销 事件状态控制：ioctl 与 prctl 创建事件后，可以通过以下方式控制其运行状态：\nioctl (Input/Output Control)：针对单个文件描述符的操作 ioctl(fd, PERF_EVENT_IOC_ENABLE)：启用事件监控 ioctl(fd, PERF_EVENT_IOC_DISABLE)：暂停事件监控 prctl (Process Control)：针对当前进程所有事件的批量操作 适用于管理多个监控事件的场景，避免频繁的 ioctl 调用 设计优势：事件被禁用时，计数器暂停但数据保留。这种设计允许在程序特定阶段（如关键算法执行期间）精确开启监控，实现细粒度的性能分析。\n两种工作模式：计数 vs 采样 perf_event 支持两种基本工作模式，满足不同的性能分析需求：\n计数模式 (Counting Mode) 内核维护一个简单的计数器（通常是 u64 类型整数）。计数事件的结果通过 read 系统调用收集。\n特点：\n低开销：仅维护计数器，不记录详细上下文 结果简单：返回事件发生的总次数 应用场景：基准测试、性能统计 例如：\u0026rdquo; 这段代码执行消耗了多少 CPU 周期 \u0026quot; \u0026quot; 函数调用发生了多少次缓存未命中 \u0026quot; 采样模式 (Sampling Mode) 设置采样阈值（如每 1000 次缓存未命中采样一次）。当事件计数器溢出时，内核会捕获当前的执行上下文（包括指令指针、进程 ID、调用栈等）。\n工作原理：\n内核分配环形缓冲区用于存储采样数据 采样数据通过内存映射 (mmap) 暴露给用户空间 用户程序从缓冲区 \u0026quot; 消费 \u0026quot; 采样数据 优势：\n记录详细的执行上下文信息 支持调用栈回溯 应用场景：性能热点分析、火焰图生成、函数调用频率统计 结合 eBPF 实现 profiling 传统 perf 工具虽然功能强大，但在某些场景下存在局限性。eBPF 提供了更灵活、更低开销的性能监控方案。通过将 eBPF 程序挂载到 perf_event 上，我们可以在内核空间直接处理性能事件，实现高效的调用栈采集。\n核心思路：\n利用 perf_event 触发采样事件 eBPF 程序在内核中捕获事件并采集调用栈信息 通过 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 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 #include \u0026#34;vmlinux.h\u0026#34; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; char LICENSE[] SEC(\u0026#34;license\u0026#34;) = \u0026#34;Dual BSD/GPL\u0026#34;; #ifndef TASK_COMM_LEN #define TASK_COMM_LEN 16 #endif #ifndef MAX_STACK_DEPTH #define MAX_STACK_DEPTH 128 #endif typedef __u64 stack_trace_t[MAX_STACK_DEPTH]; struct stack_trace_event { __u32 pid; __u32 cpu_id; __u64 timestamp; char comm[TASK_COMM_LEN]; __s32 kstack_sz; __s32 ustack_sz; stack_trace_t kstack; stack_trace_t ustack; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(\u0026#34;.maps\u0026#34;); SEC(\u0026#34;perf_event\u0026#34;) int profile(void* ctx) { struct stack_trace_event* event = bpf_ringbuf_reserve(\u0026amp;events, sizeof(struct stack_trace_event), 0); if (!event) { return 1; } int pid = bpf_get_current_pid_tgid() \u0026gt;\u0026gt; 32; int cpu_id = bpf_get_smp_processor_id(); event-\u0026gt;pid = pid; event-\u0026gt;cpu_id = cpu_id; event-\u0026gt;timestamp = bpf_ktime_get_ns(); // 获取当前进程名 if (bpf_get_current_comm(event-\u0026gt;comm, sizeof(event-\u0026gt;comm))) { event-\u0026gt;comm[0] = 0; } // 获取内核栈、用户栈信息 event-\u0026gt;kstack_sz = bpf_get_stack(ctx, event-\u0026gt;kstack, sizeof(event-\u0026gt;kstack), 0); event-\u0026gt;ustack_sz = bpf_get_stack( ctx, event-\u0026gt;ustack, sizeof(event-\u0026gt;ustack), BPF_F_USER_STACK); bpf_ringbuf_submit(event, 0); return 0; } eBPF 程序执行流程：\n程序挂载到 perf_event，随性能计数器（CPU 周期、指令数等）或定时器触发 每次触发时，eBPF 程序在内核空间执行一次 采集关键性能数据并通过 Ring Buffer 传输到用户空间 采集的关键信息：\n进程标识：当前运行进程的 PID CPU 信息：执行任务的 CPU 核心编号 时间戳：事件发生的精确时间（纳秒级） 进程名：可执行文件名称 栈回溯：内核栈和用户栈的完整调用链 数据结构设计：\n1 2 3 4 5 6 7 8 9 10 struct stack_trace_event { __u32 pid; // 进程 ID __u32 cpu_id; // CPU 核心编号 __u64 timestamp; // 时间戳（纳秒） char comm[TASK_COMM_LEN]; // 进程名 __s32 kstack_sz; // 内核栈大小 __s32 ustack_sz; // 用户栈大小 stack_trace_t kstack; // 内核栈地址数组 stack_trace_t ustack; // 用户栈地址数组 }; 应用价值： 采集的数据经过处理后，可以生成 火焰图 (Flame Graphs)，直观展示：\nCPU 消耗分布：哪个进程/函数占用最多 CPU 时间 调用链分析：热点函数的完整调用路径 内核/用户态比例：系统调用与业务逻辑的时间分配 性能瓶颈定位：快速识别性能热点和优化机会 解析采集的栈回溯信息 API 设计 定义内核返回的 event 结构体：\n1 2 3 4 5 6 7 8 9 10 struct StacktraceEvent { uint32_t pid; uint32_t cpu_id; uint64_t timestamp; char comm[TASK_COMM_LEN]; int32_t kstack_size; int32_t ustack_size; uint64_t kstack[MAX_STACK_DEPTH]; uint64_t ustack[MAX_STACK_DEPTH]; }; 定义事件处理器：\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 enum class OutputFormat : uint8_t { Standard, FoldExtend }; class EventHandler { public: EventHandler(OutputFormat fmt) : format(fmt) { boot_time_ns = get_boot_time_ns(); } ~EventHandler() = default; auto handle(const uint8_t* data, size_t len) -\u0026gt; int; void show_stack_trace(const uint64_t* stack, uint32_t size, uint32_t pid); private: blaze::Symbolizer symbolizer_; OutputFormat format; uint64_t boot_time_ns; static auto get_boot_time_ns() -\u0026gt; uint64_t; // 符号化堆栈并返回字符串向量 auto symbolize_stack_to_vec(const uint64_t* stack, uint32_t stack_sz, uint32_t pid) -\u0026gt; std::vector\u0026lt;std::string\u0026gt;; void handle_standard(const StacktraceEvent* event); void handle_fold_extend(const StacktraceEvent* event); }; 符号化解析：将地址转换为函数名 eBPF 采集的调用栈信息是内存地址数组，需要转换为可读的函数名和源代码位置。这里使用 blazesym 库进行符号化解析，并封装为易用的 C++ 接口。\n符号化的重要性：\n原始地址（如 0xffffffff81000000）对人类不友好 需要转换为函数名（如 do_syscall_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 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 #ifndef BLAZE_H_ #define BLAZE_H_ #include \u0026lt;cstdint\u0026gt; #include \u0026lt;format\u0026gt; #include \u0026lt;variant\u0026gt; #include \u0026lt;blazesym.h\u0026gt; #include \u0026#34;utils.h\u0026#34; namespace blaze { struct CodeInfo { const blaze_symbolize_code_info* info_; CodeInfo(const blaze_symbolize_code_info* info) : info_(info) {} }; struct Syms { const blaze_syms* syms_; Syms(const blaze_syms* syms) : syms_(syms) {} ~Syms() { if (syms_) { blaze_syms_free(syms_); } } // 禁止拷贝，防止双重释放 Syms(const Syms\u0026amp;) = delete; auto operator=(const Syms\u0026amp;) -\u0026gt; Syms\u0026amp; = delete; }; // 符号源 using Source = std::variant\u0026lt;blaze_symbolize_src_process, blaze_symbolize_src_kernel\u0026gt;; struct Input { const uint64_t* addrs_; size_t cnt_; }; struct Symbolizer { blaze_symbolizer* symbolizer_; Symbolizer() { symbolizer_ = blaze_symbolizer_new(); } ~Symbolizer() { blaze_symbolizer_free(symbolizer_); } [[nodiscard]] auto symbolize(Source src, const Input\u0026amp; input) const -\u0026gt; Result\u0026lt;Syms\u0026gt; { if (!input.addrs_ || input.cnt_ == 0) { return Err\u0026lt;\u0026gt;{\u0026#34;Empty input addresses\u0026#34;}; } const blaze_syms* syms = nullptr; std::visit( Overloaded{ [\u0026amp;](blaze_symbolize_src_kernel\u0026amp; kern_src) -\u0026gt; void { syms = blaze_symbolize_kernel_abs_addrs( symbolizer_, \u0026amp;kern_src, input.addrs_, input.cnt_); }, [\u0026amp;](blaze_symbolize_src_process\u0026amp; proc_src) -\u0026gt; void { syms = blaze_symbolize_process_abs_addrs( symbolizer_, \u0026amp;proc_src, input.addrs_, input.cnt_); }, }, src); return syms ? Result\u0026lt;Syms\u0026gt;{syms} : Err\u0026lt;\u0026gt;{std::format(\u0026#34;Symbolization failed, errno is: {}\u0026#34;, static_cast\u0026lt;int16_t\u0026gt;(blaze_err_last()))}; } }; inline auto get_symbolize_source(uint32_t pid) -\u0026gt; blaze::Source { if (pid == 0) { blaze_symbolize_src_kernel src{ .type_size = sizeof(src), }; return blaze::Source{src}; } else { blaze_symbolize_src_process src{ .type_size = sizeof(src), .pid = pid, }; return blaze::Source{src}; } } } // namespace blaze #endif /* BLAZE_H_ */ 其中的 Result、Err、Overloaded 分别借鉴了 Rust 中的 Result、Err 和 match。\nSymbolizer::symbolize 用于把绝对地址数组符号化（转成符号/调试信息）：\n如果 input.addrs_ 为 null 或 input.cnt_ == 0，返回错误 Err\u0026lt;\u0026gt;{\u0026quot;Empty input addresses\u0026quot;}。 根据参数 src（Source 是 blaze_symbolize_src_process 或 blaze_symbolize_src_kernel 的 std::variant）调用不同的符号化接口： 内核源时调用 blaze_symbolize_kernel_abs_addrs(symbolizer_, \u0026amp;kern_src, input.addrs_, input.cnt_)； 进程源时调用 blaze_symbolize_process_abs_addrs(symbolizer_, \u0026amp;proc_src, input.addrs_, input.cnt_)。 使用 std::visit + Overloaded 选择分支。 结果处理：把返回的 const blaze_syms* syms 封装进 Syms（RAII，析构时会调用 blaze_syms_free）。若 syms 非空则返回 Result\u0026lt;Syms\u0026gt;{syms}；否则返回带有 blaze_err_last()（转换为整数）的错误信息。 事件处理 EventHandler::handle 接收的是一块“原始二进制缓冲区”（raw bytes）而不是已经解析好的结构体，所以用 const uint8_t* data, size_t len 更通用、安全，并且允许在用户态从 ring/payload、socket 或 perf buffer 等来源直接读取数据并检查长度后再解析（在这个项目中，数据从 ringbuf 中获取）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 auto EventHandler::handle(const uint8_t* data, size_t len) -\u0026gt; int { if (len != sizeof(StacktraceEvent)) { std::println(\u0026#34;Data length mismatch: expected {}, got {}\u0026#34;, sizeof(StacktraceEvent), len); return 1; } const auto* const event = reinterpret_cast\u0026lt;const StacktraceEvent*\u0026gt;(data); if (event-\u0026gt;kstack_size \u0026lt;= 0 \u0026amp;\u0026amp; event-\u0026gt;ustack_size \u0026lt;= 0) { return 1; } if (format == OutputFormat::Standard) { handle_standard(event); } else { handle_fold_extend(event); } return 0; } 标准输出格式：可读的逐帧展示 handle_standard 方法生成人类可读的调用栈输出，包含完整的时间戳、进程信息和源代码位置。这种格式适合直接查看和分析：\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 static void print_frame(const char* name, Option\u0026lt;AddrInfo\u0026gt; addr_info, const blaze_symbolize_code_info* code_info) { std::string code_str; if (code_info != nullptr) { // path if ((code_info-\u0026gt;dir != nullptr) \u0026amp;\u0026amp; (code_info-\u0026gt;file != nullptr)) { code_str = std::format(\u0026#34; {}/{})\u0026#34;, code_info-\u0026gt;dir, code_info-\u0026gt;file); } else if (code_info-\u0026gt;file != nullptr) { code_str = code_info-\u0026gt;file; } // line and column if (code_info-\u0026gt;line \u0026gt; 0) { code_str += std::format(\u0026#34;:{}\u0026#34;, code_info-\u0026gt;line); if (code_info-\u0026gt;column \u0026gt; 0) { code_str += std::format(\u0026#34;:{}\u0026#34;, code_info-\u0026gt;column); } } } if (addr_info.has_value()) { auto [input_addr, addr, offset] = *addr_info; std::println(\u0026#34;0x{:0\u0026gt;{}}: {} @ {:#x} + {:#x}{}\u0026#34;, input_addr, ADDR_WIDTH, (name != nullptr) ? name : \u0026#34;\u0026lt;unknown\u0026gt;\u0026#34;, addr, offset, code_str); } else { std::println(\u0026#34;{:\u0026gt;{}} {}{} [inlined]\u0026#34;, \u0026#34;\u0026#34;, ADDR_WIDTH, name, code_str); } } void EventHandler::show_stack_trace(const uint64_t* stack, uint32_t size, uint32_t pid) { blaze::Source src = blaze::get_symbolize_source(pid); size_t count = static_cast\u0026lt;size_t\u0026gt;(size) / sizeof(uint64_t); // 栈帧数量 auto result = symbolizer_.symbolize( src, blaze::Input{.addrs_ = stack, .cnt_ = count}); if (!result) { std::println(stderr, \u0026#34; Failed to symbolize stack trace. err: {}\u0026#34;, result.error()); return; } const auto* syms = result-\u0026gt;syms_; for (size_t i = 0; i \u0026lt; count; ++i) { if (i \u0026lt; syms-\u0026gt;cnt \u0026amp;\u0026amp; (syms-\u0026gt;syms[i].name != nullptr)) { print_frame( syms-\u0026gt;syms[i].name, Option\u0026lt;AddrInfo\u0026gt;(std::make_tuple( stack[i], syms-\u0026gt;syms[i].addr, syms-\u0026gt;syms[i].offset)), \u0026amp;syms-\u0026gt;syms[i].code_info); // 打印内联函数信息 for (size_t j = 0; j \u0026lt; syms-\u0026gt;syms[i].inlined_cnt; ++j) { print_frame(syms-\u0026gt;syms[i].inlined[j].name, std::nullopt, \u0026amp;syms-\u0026gt;syms[i].inlined[j].code_info); } } else { std::println(\u0026#34;{:\u0026gt;0{}}: \u0026lt;no-symbol\u0026gt;\u0026#34;, stack[i], ADDR_WIDTH); } } } void EventHandler::handle_standard(const StacktraceEvent* event) { uint64_t unix_ns = event-\u0026gt;timestamp + boot_time_ns; std::println(\u0026#34;[{}.{:09} COMM: {} (pid={}) @ CPU {}]\u0026#34;, unix_ns / 1\u0026#39;000\u0026#39;000\u0026#39;000, unix_ns % 1\u0026#39;000\u0026#39;000\u0026#39;000, event-\u0026gt;comm, event-\u0026gt;pid, event-\u0026gt;cpu_id); // 打印内核栈 if (event-\u0026gt;kstack_size \u0026gt; 0) { std::println(\u0026#34;Kernel:\u0026#34;); show_stack_trace(event-\u0026gt;kstack, event-\u0026gt;kstack_size, 0); } else { std::println(\u0026#34;Kernel: \u0026lt;no stack\u0026gt;\u0026#34;); } // 打印用户栈 if (event-\u0026gt;ustack_size \u0026gt; 0) { std::println(\u0026#34;Userspace:\u0026#34;); show_stack_trace(event-\u0026gt;ustack, event-\u0026gt;ustack_size, event-\u0026gt;pid); } else { std::println(\u0026#34;Userspace: \u0026lt;no stack\u0026gt;\u0026#34;); } std::println(); } 火焰图格式：机器可读的数据输出 handle_fold_extend 生成折叠行（comm-pid;frame;... 1），用于生成火焰图（FlameGraph）。handle_fold_extend 对每个栈结果做 reverse，把栈从“根”（程序入口）到“叶”（当前帧）排列，符合火焰图期望的父到子顺序。\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 auto EventHandler::symbolize_stack_to_vec(const uint64_t* stack, uint32_t stack_sz, uint32_t pid) -\u0026gt; std::vector\u0026lt;std::string\u0026gt; { if (stack_sz \u0026lt;= 0) { return {}; } blaze::Source src = blaze::get_symbolize_source(pid); size_t count = stack_sz / sizeof(uint64_t); auto result = symbolizer_.symbolize( src, blaze::Input{.addrs_ = stack, .cnt_ = count}); if (!result) { return {}; } const auto* syms = result-\u0026gt;syms_; std::vector\u0026lt;std::string\u0026gt; vec; if (syms == nullptr) { for (size_t i = 0; i \u0026lt; count; ++i) { vec.push_back(std::format(\u0026#34;0x{:x}\u0026#34;, stack[i])); } return vec; } for (size_t i = 0; i \u0026lt; syms-\u0026gt;cnt; ++i) { if (syms-\u0026gt;syms[i].name != nullptr) { vec.emplace_back(syms-\u0026gt;syms[i].name); } else { vec.push_back(std::format(\u0026#34;0x{:x}\u0026#34;, stack[i])); } } return vec; } void EventHandler::handle_fold_extend(const StacktraceEvent* event) { std::vector\u0026lt;std::string\u0026gt; stack_frames; // 为了让火焰图能够按进程聚合，将 \u0026#34;comm-pid\u0026#34; 作为栈底 stack_frames.push_back(std::format(\u0026#34;{}-{}\u0026#34;, event-\u0026gt;comm, event-\u0026gt;pid)); // 处理用户态 if (event-\u0026gt;ustack_size \u0026gt; 0) { auto user_frames = symbolize_stack_to_vec( event-\u0026gt;ustack, event-\u0026gt;ustack_size, event-\u0026gt;pid); for (const auto\u0026amp; frame : user_frames | std::views::reverse) { stack_frames.push_back(frame); } } // 处理内核态 if (event-\u0026gt;kstack_size \u0026gt; 0) { auto kern_frames = symbolize_stack_to_vec(event-\u0026gt;kstack, event-\u0026gt;kstack_size, 0); for (const auto\u0026amp; frame : kern_frames | std::views::reverse) { stack_frames.push_back(frame + \u0026#34;_[k]\u0026#34;); } } auto temp = stack_frames | std::views::join_with(\u0026#39;;\u0026#39;); // 输出格式：stack;frames 1 // FlameGraph 工具期望每行以空格和数字结尾 std::println(\u0026#34;{} 1\u0026#34;, temp | std::ranges::to\u0026lt;std::string\u0026gt;()); } 流程图 flowchart TD A[eBPF 程序（内核） 采集 stacktrace ] --\u003e B[Userspace reader 接收 StacktraceEvent] B --\u003e C[EventHandler::handle 校验 \u0026 选择输出格式] C --\u003e D{有 ustack?} C --\u003e E{有 kstack?} D -- yes --\u003e U1[\"构造 blaze::Source (process) 构造 blaze::Input(addrs,cnt)\"] E -- yes --\u003e K1[\"构造 blaze::Source (kernel) 构造 blaze::Input(addrs,cnt)\"] U1 --\u003e S1[\"Symbolizer::symbolize (std::visit -\u003e process API)\"] K1 --\u003e S2[\"Symbolizer::symbolize (std::visit -\u003e kernel API)\"] S1 --\u003e F{symbolize 成功?} S2 --\u003e F F -- yes --\u003e G[返回 Syms（RAII）] F -- no --\u003e H[\"返回 Err 记录 blaze_err_last() -\u003e fallback: 打印原始地址\"] G --\u003e P1[show_stack_trace 逐帧打印（含内联）] G --\u003e P2[symbolize_stack_to_vec 生成折叠帧向量] P2 --\u003e M[组装折叠行 -\u003e 输出给 FlameGraph] P1 --\u003e O[标准可读输出] H --\u003e O2[打印原始地址 / 记录错误日志] style U1 fill:#e6ffed,stroke:#2d9a3b style K1 fill:#e6f0ff,stroke:#2176d2 style H fill:#fff1f0,stroke:#d9534f 启动与事件循环 这一节描述用户态程序如何完成启动准备并进入事件轮询：解析命令行与日志级别、提升必要的资源限制、加载并 attach eBPF 程序、通过 ring buffer 接收内核事件、调用 EventHandler 进行解析输出，以及在接收到信号时优雅退出。\n关键依赖与权限 依赖项：libbpf（加载 BPF 对象与 maps）、spdlog（日志）、CLI11（命令行解析）、可选的内核 BTF / vmlinux（用于内核符号化）。\n运行权限：通常需要 root 或至少 CAP_BPF/CAP_SYS_ADMIN。另外必须提升 RLIMIT_MEMLOCK，否则内核会因为无法锁定内存而拒绝加载 BPF 对象。\n建议在启动前做最基本的环境检查：\n1 2 uname -r which bpftool 命令行与日志初始化 使用 CLI11 解析采样频率（-f）、PID 过滤（-p）、输出格式（-E）等选项，并使用 spdlog 配置默认日志器与日志等级。示例代码：\n1 2 3 4 5 6 7 8 9 10 11 CLI::App app{\u0026#34;A simple profiler using eBPF\u0026#34;}; Args args; app.add_option(\u0026#34;-f,--freq\u0026#34;, args.freq, \u0026#34;Sampling frequency\u0026#34;)-\u0026gt;default_val(10); app.add_flag(\u0026#34;-E,--fold-extend\u0026#34;, args.fold_extend, \u0026#34;Output in extended folded format\u0026#34;); CLI11_PARSE(app, argc, argv); using Level = spdlog::level::level_enum; Level level = (args.verbosity == 0) ? Level::warn : (args.verbosity==1 ? Level::info : Level::debug); auto console = spdlog::stdout_color_mt(\u0026#34;console\u0026#34;); spdlog::set_default_logger(console); spdlog::set_level(level); 将日志级别与命令行绑定，便于在调试时查看详细符号化错误或在生产时降低输出噪音。\n提升资源限制（RLIMIT_MEMLOCK） BPF 对象和 ring buffer 需要被内核锁定在内存。推荐将 RLIMIT_MEMLOCK 设为无限以避免加载失败：\n1 2 rlimit rl = {.rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY}; setrlimit(RLIMIT_MEMLOCK, \u0026amp;rl); 在受限环境下（容器、受限用户）应提前确认是否允许修改该限制。\n加载并管理 BPF 对象（ProfilerSkel） 通过 bpf 的 skeleton（例如 profiler_bpf）生成的封装，使用 RAII 管理生命周期：open/load 在构造时完成，资源在析构时释放。若加载失败，应立即记录错误并退出。\n1 2 3 4 5 6 7 8 9 struct ProfilerSkel { profiler_bpf* obj{nullptr}; ProfilerSkel() { obj = profiler_bpf::open_and_load(); if (obj == nullptr) spdlog::error(\u0026#34;Failed to open and load BPF object\u0026#34;); } ~ProfilerSkel() { if (obj) profiler_bpf__destroy(obj); } operator bool() const { return obj != nullptr; } auto operator-\u0026gt;() const -\u0026gt; profiler_bpf* { return obj; } }; 在加载成功后，还可能需要对 map 进行预初始化（例如设置 PID 过滤器）或为 symbolization 提供 vmlinux 路径。\n初始化 perf 事件并 attach 根据配置（采样频率、是否使用软件事件、是否按 PID 过滤等）创建 perf event 文件描述符并返回给调用方；随后将这些 fd attach 到 eBPF 程序的 entry（例如 profile）上。初始化失败时应当记录并退出。\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 // perf.cpp auto init_perf_monitor(uint64_t freq, bool sw_event, pid_t pid = -1) -\u0026gt; Result\u0026lt;std::vector\u0026lt;int\u0026gt;, libbpf_errno\u0026gt; { int cpus = libbpf_num_possible_cpus(); if (cpus \u0026lt; 0) { return Err(libbpf_errno::LIBBPF_ERRNO__INTERNAL); } perf_event_attr attr = { .type = sw_event ? PERF_TYPE_SOFTWARE : PERF_TYPE_HARDWARE, // 采样类型 .size = sizeof(perf_event_attr), .config = (sw_event ? static_cast\u0026lt;uint64_t\u0026gt;(PERF_COUNT_SW_CPU_CLOCK) : static_cast\u0026lt;uint64_t\u0026gt;(PERF_COUNT_HW_CPU_CYCLES)), .sample_freq = freq, // 设置频率，每秒 freq 次采样 .freq = 1, // 标志位：设为 1 表示使用 sample_freq }; std::vector\u0026lt;int\u0026gt; fds; for (int cpu = 0; cpu \u0026lt; cpus; ++cpu) { int fd = static_cast\u0026lt;int\u0026gt;(perf_event_open(\u0026amp;attr, pid, cpu, -1, 0)); if (fd \u0026lt; 0) { return Err(libbpf_errno::LIBBPF_ERRNO__INTERNAL); } fds.push_back(fd); } return fds; } auto attach_perf_events(const std::vector\u0026lt;int\u0026gt;\u0026amp; fds, bpf_program* prog) -\u0026gt; std::vector\u0026lt;Result\u0026lt;bpf_link*, libbpf_errno\u0026gt;\u0026gt; { std::vector\u0026lt;Result\u0026lt;bpf_link*, libbpf_errno\u0026gt;\u0026gt; links; for (int fd : fds) { auto* link = bpf_program__attach_perf_event(prog, fd); if (libbpf_get_error(link) != 0) { links.emplace_back(Err(libbpf_errno::LIBBPF_ERRNO__INTERNAL)); } links.emplace_back(link); } return links; } // --------------------------------------------------------------- // main.cpp auto perf_fds = init_perf_monitor(freq, args.sw_event, args.pid); if (!perf_fds) { spdlog::error(\u0026#34;Failed to initialize perf monitor\u0026#34;); return 1; } attach_perf_events(perf_fds.value(), obj-\u0026gt;progs.profile); attach 成功后，内核采样事件触发时会进入 eBPF 程序，程序会把数据写入之前声明的 ring buffer map。\nRing Buffer 与事件回调 用户态通过 ring buffer 从内核读取事件样本。回调函数负责类型安全地把原始字节传递给 EventHandler::handle，由其完成长度检查、解析与输出。\n1 2 3 4 5 6 7 8 9 10 static auto handle_event_wrapper(void* ctx, void* data, size_t data_sz) -\u0026gt; int { auto* handler = static_cast\u0026lt;EventHandler*\u0026gt;(ctx); return handler-\u0026gt;handle(static_cast\u0026lt;const uint8_t*\u0026gt;(data), data_sz); } RingBuffer rb{bpf_map__fd(obj-\u0026gt;maps.events), handle_event_wrapper, \u0026amp;event_handler, nullptr}; if (!rb) { spdlog::error(\u0026#34;Failed to create ring buffer\u0026#34;); return 1; } 要点：\n回调签名使用 const uint8_t* data, size_t data_sz，有利于在用户态对来自不同来源（ringbuf、perf buffer、socket 等）的原始字节流做统一处理和边界校验； ring buffer 的容量应根据采样频率与处理速率调整，避免丢失样本。 主循环、轮询与优雅退出 主循环负责轮询 ring buffer，并在接收到中断信号时优雅退出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static volatile bool exiting = false; static void sig_handler(int sig) { exiting = true; } ... signal(SIGINT, sig_handler); while (!exiting) { int err = rb.poll(100); if (err == -EINTR) { // Interrupted by signal, continue to check exiting flag continue; } if (err \u0026lt; 0) { spdlog::error(\u0026#34;Error polling ring buffer: {}\u0026#34;, err); break; } } auto r = close_perf_events(perf_fds.value()); if (!r) { spdlog::error(\u0026#34;Failed to close perf events, error message is: {}\u0026#34;, static_cast\u0026lt;int\u0026gt;(r.error())); return 1; } 退出流程要点：\nsignal handler 仅设置原子/易变标志，实际清理在主线程完成； 调用 close_perf_events 关闭 perf fds，确保內核计数器停止并释放资源； 依赖 RAII 的 BPF skeleton 在作用域结束时销毁 BPF 对象和 maps。 这样可以保证在收到 SIGINT / SIGTERM 时，程序能够尽量把已经到达的样本处理完并正确释放所有内核资源。\n对 eBPF 程序挂载方式的思考 [!question] 为什么不用 bpf skeleton 中的 bpf_object__attach_skeleton 来挂载，而是使用 bpf_program__attach_perf_event ？\n在使用 libbpf 开发 BPF 程序时，通常 skeleton 会提供的自动 attach 机制 bpf_object__attach_skeleton，但在实际实现 perf 采样监控（例如 init_perf_monitor 这类逻辑）的过程中，最终选择的是手动创建 perf_event fd，并使用 bpf_program__attach_perf_event 进行挂载。这并不是因为 skeleton 不好，而是两者适用的层级和场景并不相同。\nskeleton attach 是什么，它解决了什么问题？ skeleton attach（本质是 bpf_object__attach_skeleton 或生成代码中的 xxx__attach()）是一种高层抽象：\n在 BPF ELF 中通过 section 描述程序的 attach 类型 libbpf 根据这些元信息，自动把所有 program 挂载到对应的 hook 点 用户态几乎不需要关心 attach 的细节 它非常适合下面这些场景：\nattach 类型是 静态的、编译期就确定的 比如 tracepoint、kprobe、raw_tp 等 希望快速把程序跑起来，而不是写大量样板代码 在这些情况下，skeleton attach 是几乎没有理由拒绝的。但当 attach 对象变成 perf event 时，情况就不太一样了。\nperf 场景下的不同 perf 事件并不是一个“天然存在的 hook 点”，而是需要用户态显式创建的：\n需要调用 perf_event_open 需要构造 perf_event_attr 需要决定： 是按 CPU 还是按 PID 采样频率 / period 是否 per-CPU 创建多个 fd 这些信息天然属于运行时配置，而不是 ELF 元信息的一部分。\n而 skeleton attach 的设计假设是：\nattach 行为已经在 BPF 程序中声明好，用户态只负责“一次性挂上去”。\n因而这里选择了另一条路径：\n在用户态显式创建 perf_event fd 将单个 bpf_program 附加到指定的 perf fd 上 也就是使用：\n1 bpf_program__attach_perf_event(prog, perf_fd); 这种方式带来的好处，主要体现在控制权完全回到用户态。\nprogram attach 挂载的好处 （1）attach 粒度更细\nskeleton attach： 以整个 skeleton 为单位 自动 attach 多个 program / link program attach： 明确地控制“哪个 program → 哪个 perf fd” 这在 perf 场景下非常重要，因为 fd 本身就是策略的一部分。\n（2）perf 参数可以在运行时自由配置\n通过手动创建 perf fd，可以在用户态精确控制：\n采样频率（freq / period） perf event 类型 绑定到哪个 CPU 或 PID 是否为每个 CPU 创建独立 fd 这些配置很难优雅地放进 skeleton 的自动 attach 流程中，但却是 perf 采样中最核心的部分。\n（3）天然支持 per-CPU / 多 fd 模型\n一个很典型的模式是：\n为每个 CPU 创建一个 perf fd 把同一个 BPF 程序附加到所有这些 fd 上 这在 program attach 模型下是顺理成章的事情：\n1 2 3 4 for_each_cpu(cpu) { perf_fd = perf_event_open(..., cpu, ...); link[cpu] = bpf_program__attach_perf_event(prog, perf_fd); } 而 skeleton attach 并不擅长表达这种 “一对多、运行时生成”的 attach 关系。\n（4）错误处理逻辑更清晰\n手动管理 perf fd 和 link，也意味着：\nperf_event_open 失败时可以： 跳过某些 CPU 降级采样策略 做重试或 fallback attach 失败时可以单独处理，而不是整体失败 对比小结 需要强调的一点是，底层其实并没有“谁更高级”：\n无论是 skeleton attach 还是 bpf_program__attach_perf_event，最终内核里做的事情本质上是一样的——建立 bpf_link，把程序绑定到事件上。\n对当前的 perf 采样监控场景来说：\n采样频率、CPU 绑定、fd 数量都是运行时决策 需要 per-CPU 创建 perf fd 需要清晰的错误处理和资源管理路径 因此在用户态创建 perf_event fd，并使用 bpf_program__attach_perf_event 逐个 attach，是更自然、也更可控的做法。\n参考 eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析 eBPF 性能分析实战 - eBPF 在性能分析中的应用 Perf IPC 与 CPU 利用率 - perf 原理深入解析 perf_event_open 手册页 - 官方文档参考 FlameGraph 工具 - 火焰图生成工具 blazesym 符号化库 - 高性能符号化库 ","permalink":"https://kerolt.github.io/p/%E5%88%A9%E7%94%A8ebpf%E4%B8%8Eperf_event%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AAprofiling%E5%B7%A5%E5%85%B7/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003e在性能分析和系统调优中，了解应用程序在运行时的调用栈信息至关重要。在 \u003ca href=\"https://eunomia.dev/zh/tutorials/12-profile/\"\u003e【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】\u003c/a\u003e 这篇文章中，其利用 Rust + eBPF 构建了一个 profiler 工具，抱着学习的目的，我使用 C++ 实现了相同的功能，并将所学知识总结成本文。\u003c/p\u003e","title":"利用eBPF与perf_event构建一个profiling工具"},{"content":"首先在 layouts/partials 下添加 mermaid.html：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; import mermaid from \u0026#34;https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs\u0026#34;; mermaid.initialize({ startOnLoad: true }); \u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; // Replace mermaid pre.code to div Array.from(document.getElementsByClassName(\u0026#34;language-mermaid\u0026#34;)).forEach( (el) =\u0026gt; { el.parentElement.outerHTML = `\u0026lt;div class=\u0026#34;mermaid\u0026#34;\u0026gt;${el.innerHTML}\u0026lt;/div\u0026gt;`; }, ); \u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; /* Set svg to center */ .mermaid svg { display: block; margin: auto; } \u0026lt;/style\u0026gt; 然后在 layouts/partials/_default 下的 single.html 中添加 mermaid 组件的引入：\n1 2 \u0026lt;!-- mermaid --\u0026gt; {{- partial \u0026#34;mermaid.html\u0026#34; . -}} 最后设置配置文件中对于语法高亮中的 guessSyntax 为 false：\n1 2 3 markup: highlight: gussSyntax: false 至此，对于在 markdown 中的 mermaid 代码就可以正确渲染了。\n","permalink":"https://kerolt.github.io/p/%E4%B8%BAhugo-papermod%E6%B7%BB%E5%8A%A0mermaid%E6%B8%B2%E6%9F%93/","summary":"\u003cp\u003e首先在 \u003ccode\u003elayouts/partials\u003c/code\u003e 下添加 \u003ccode\u003emermaid.html\u003c/code\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;module\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003emermaid\u003c/span\u003e \u003cspan class=\"nx\"\u003efrom\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003emermaid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003einitialize\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003estartOnLoad\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e// Replace mermaid pre.code to div\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetElementsByClassName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;language-mermaid\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)).\u003c/span\u003e\u003cspan class=\"nx\"\u003eforEach\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nx\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eparentElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eouterHTML\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sb\"\u003e`\u0026lt;div class=\u0026#34;mermaid\u0026#34;\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003eel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003einnerHTML\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sb\"\u003e\u0026lt;/div\u0026gt;`\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003escript\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e/* Set svg to center */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003emermaid\u003c/span\u003e \u003cspan class=\"nt\"\u003esvg\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edisplay\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003eblock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003emargin\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003eauto\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e然后在 \u003ccode\u003elayouts/partials/_default\u003c/code\u003e 下的 \u003ccode\u003esingle.html\u003c/code\u003e 中添加 mermaid 组件的引入：\u003c/p\u003e","title":"为Hugo PaperMod添加mermaid渲染"},{"content":"需求：在 Docker 环境下启用 IPv6，使基于 nginx 的容器服务可通过 IPv6 访问。\n为网络启用 IPv6 添加 enable_ipv6: true 并配置 ipam。\n设置子网前缀为 fd00:abcd::/64。\n1 2 3 4 5 6 7 8 networks: my-network: driver: bridge enable_ipv6: true ipam: driver: default config: - subnet: \u0026#34;fd00:abcd::/64\u0026#34; 在项目前端的 nginx 配置中，增加 IPv6 监听：\n1 2 listen 80; listen [::]:80 ipv6only=on; enable_ipv6: true + ipam.subnet：告诉 Docker 网络为容器分配 IPv6 地址并（可选）使用指定前缀。 nginx 增加 listen [::]:80：容器内 nginx 需要监听 IPv6 地址，否则即使容器有 IPv6 也无法响应 IPv6 请求。 配置 Docker Engine 必须在 Docker 引擎启用 IPv6，否则容器不会拿到 IPv6 地址。在 /etc/docker/daemon.json 中加入：\n1 2 3 4 { \u0026#34;ipv6\u0026#34;: true, \u0026#34;fixed-cidr-v6\u0026#34;: \u0026#34;fd00:abcd::/80\u0026#34; } 然后重启 docker：\n1 2 sudo systemctl daemon-reload sudo systemctl restart docker 重新启动容器与测试 重建并启动：\n1 2 docker compose down docker compose up -d --build 查看网络与容器 IPv6：\n1 2 docker network inspect 网络名 docker inspect -f \u0026#34;{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}\u0026#34; 容器名 访问（强制 IPv6）：\n1 curl -6 http://[容器_IPV6]/ ","permalink":"https://kerolt.github.io/p/%E5%9C%A8-docker-compose-%E4%B8%AD%E4%B8%BA%E9%A1%B9%E7%9B%AE%E5%90%AF%E7%94%A8-ipv6-%E5%B9%B6%E5%8F%AF%E9%80%9A%E8%BF%87-ipv6-%E5%8F%AF%E8%AE%BF%E9%97%AE/","summary":"\u003cp\u003e\u003cstrong\u003e需求\u003c/strong\u003e：在 Docker 环境下启用 IPv6，使基于 nginx 的容器服务可通过 IPv6 访问。\u003c/p\u003e\n\u003ch2 id=\"为网络启用-ipv6\"\u003e为网络启用 IPv6\u003c/h2\u003e\n\u003cp\u003e添加 \u003ccode\u003eenable_ipv6: true\u003c/code\u003e 并配置 \u003ccode\u003eipam\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e设置子网前缀为 \u003ccode\u003efd00:abcd::/64\u003c/code\u003e。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003enetworks\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003emy-network\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edriver\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003ebridge\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eenable_ipv6\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eipam\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003edriver\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e- \u003cspan class=\"nt\"\u003esubnet\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;fd00:abcd::/64\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e在项目前端的 nginx 配置中，增加 IPv6 监听：\u003c/p\u003e","title":"在 Docker Compose 中为项目启用 IPv6 并可通过 IPv6 可访问"},{"content":"epel-release 是一个 RPM 软件包，用于在基于 Red Hat 的 Linux 发行版（如 RHEL、CentOS、Rocky Linux、AlmaLinux 和 Oracle Linux）上启用 EPEL（Extra Packages for Enterprise Linux） 软件仓库。\nEPEL 是一个由 Fedora 项目维护的社区驱动的额外软件包仓库，提供不在标准 RHEL 或其衍生发行版基础仓库中的高质量开源软件包。\n通常安装方法如下：\n1 2 sudo dnf install epel-release -y sudo dnf makecache ","permalink":"https://kerolt.github.io/p/rockylinux%E5%90%AF%E7%94%A8epel%E8%BD%AF%E4%BB%B6%E4%BB%93%E5%BA%93/","summary":"\u003cp\u003e\u003ccode\u003eepel-release\u003c/code\u003e 是一个 \u003ccode\u003eRPM\u003c/code\u003e 软件包，用于在基于 \u003ccode\u003eRed Hat\u003c/code\u003e 的 \u003ccode\u003eLinux\u003c/code\u003e 发行版（如 \u003ccode\u003eRHEL、CentOS、Rocky Linux、AlmaLinux 和 Oracle Linux\u003c/code\u003e）上启用 \u003ccode\u003eEPEL（Extra Packages for Enterprise Linux\u003c/code\u003e） 软件仓库。\u003c/p\u003e","title":"RockyLinux启用EPEL软件仓库"},{"content":"背景 最近根据 【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】 这篇文章，写了一个性能分析的小工具。不过文章中的实现是 rust，但我还不怎么熟悉 rust，因而打算参考其代码使用 c++ 来实现相同的功能。项目地址：profiler。\nrust 中使用 cargo 就能非常方便的管理依赖与构建项目，c++ 中我使用 CMake 来构建项目，Conan 来管理依赖。在使用期间，我对于这二者的协同工作原理感到好奇，因此有了这篇笔记。\n文中的出现的“依赖”、“包”、“库”等名词都是指代同一个概念，只是不同的说法而已~\nCMake 查找依赖 在 CMake 中查找和管理项目依赖，主要是通过内置的 find_package 命令完成，其会在系统中寻找指定的库。\n1 find_package(\u0026lt;PackageName\u0026gt; [version] [REQUIRED] [COMPONENTS components...]) PackageName: 库的名称（区分大小写，如 OpenCV、Qt5）。 REQUIRED: 如果找不到该库，CMake 会直接报错并停止配置。 COMPONENTS: 查找库中的特定模块（例如 Qt5 里的 Widgets）。 CMake 有两种查找库的逻辑：\nModule 模式 (查找 Find\u0026lt;PackageName\u0026gt;.cmake)\nCMake 预置了一系列脚本（在 /usr/share/cmake/Modules 下）。它会寻找名为 FindGSL.cmake 这样的文件。\n适用场景: 较老或不直接支持 CMake 的库（如 CURL, ZLIB）。 Config 模式 (查找 \u0026lt;PackageName\u0026gt;Config.cmake 或 \u0026lt;lower-case-pkg\u0026gt;-config.cmake)\n这是现代库推荐的方式。库在安装时会自带一个配置文件。\n适用场景: OpenCV, Qt, Protobuf 等现代库。 查找路径: 库的安装路径、CMAKE_PREFIX_PATH 或环境变量。 CMAKE_PREFIX_PATH 当在 CMake 中使用 find_package、find_library 或 find_path 等指令去寻找一个外部库时，CMake 并不知道这个库安装在哪里。CMAKE_PREFIX_PATH 就是用来告诉 CMake 去这些目录下找找看。\n这里有三种设置的方法：\n命令行设置\n1 cmake -DCMAKE_PREFIX_PATH=\u0026#34;/path/to\u0026#34; CMakeLists.txt\n1 list(APPEND CMAKE_PREFIX_PATH \u0026#34;/path/to\u0026#34;) 环境变量\n1 export CMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH:/path/to Conan Generator 在一个比较简单的项目里，通常使用 conanfile.txt 来管理依赖即可，例如：\n1 2 3 4 5 6 7 8 9 10 [requires] cli11/2.6.0 spdlog/1.17.0 [generators] CMakeDeps CMakeToolchain [layout] cmake_layout 注意 [generators] 中的 CMakeDeps 和 CMakeToolchain，他们在 Conan 与 CMake 集成使用中起了至关重要的作用。\nCMakeDeps 当我们安装了 spdlog 这样的库后，需要 CMake 能看到它。CMakeDeps 就是这样一个依赖关系生成器，它会为每一个依赖库生成一个标准的 spdlog-config.cmake 文件。\n将 CMakeDeps 生成的 spdlog-config.cmake 目录位置加入 CMAKE_PREFIX_PATH 后，就可以在 CMakeLists.txt 中写 find_package(spdlog REQUIRED) 来查找依赖了。\nCMakeToolchain CMakeToolchain 负责将你在 Conan Profile（配置文件）中定义的设置（如 compiler=gcc, build_type=Release）注入给 CMake。同时，其生成 conan_toolchain.cmake，并自动设置 CMAKE_PREFIX_PATH。\n重要的一点就是：它会自动把 CMakeDeps 生成的配置文件目录加入搜索路径。\n当执行 conan install 后，这两个生成器会接力完成任务：\nConan 下载库到缓存。 CMakeDeps 生成 spdlog-config.cmake 等文件，描述如何链接这些库。 CMakeToolchain 生成 conan_toolchain.cmake，里面有一行代码告诉 CMake：“去 CMakeDeps 生成的那个文件夹里找包”。 例如我的项目里， ~/Workspace/profiler-cpp/build/Debug/generators/conan_toolchain.cmake 中的一部分如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ########## \u0026#39;find_paths\u0026#39; block ############# # Define paths to find packages, programs, libraries, etc. if(EXISTS \u0026#34;${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake\u0026#34;) message(STATUS \u0026#34;Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake\u0026#34;) include(\u0026#34;${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake\u0026#34;) else() set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) # Definition of CMAKE_MODULE_PATH # the generators folder (where conan generates files, like this toolchain) list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) # Definition of CMAKE_PREFIX_PATH, CMAKE_XXXXX_PATH # The Conan local \u0026#34;generators\u0026#34; folder, where this toolchain is saved. list(PREPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_LIST_DIR} ) list(PREPEND CMAKE_LIBRARY_PATH \u0026#34;/home/kerolt/.conan2/p/b/spdlo964bc6492d317/p/lib\u0026#34; \u0026#34;/home/kerolt/.conan2/p/b/fmtc7b4bad61f66e/p/lib\u0026#34;) list(PREPEND CMAKE_INCLUDE_PATH \u0026#34;/home/kerolt/.conan2/p/cli115c29056d7e51f/p/include\u0026#34; \u0026#34;/home/kerolt/.conan2/p/b/spdlo964bc6492d317/p/include\u0026#34; \u0026#34;/home/kerolt/.conan2/p/b/fmtc7b4bad61f66e/p/include\u0026#34;) set(CONAN_RUNTIME_LIB_DIRS \u0026#34;/home/kerolt/.conan2/p/b/spdlo964bc6492d317/p/lib\u0026#34; \u0026#34;/home/kerolt/.conan2/p/b/fmtc7b4bad61f66e/p/lib\u0026#34; ) endif() CMake 工具链文件 工具链文件是一个以 .cmake 结尾的脚本，通过在配置阶段指定变量 CMAKE_TOOLCHAIN_FILE 来加载：\n1 cmake -DCMAKE_TOOLCHAIN_FILE=my_toolchain.cmake .. 它通常在 project() 指令执行 之前 被加载，用于初始化编译器路径、目标平台信息等关键底层变量。\n现在通过 Conan 的 CMakeDeps 和 CMakeToolchain，直接引入 Conan 自动生成的工具链文件，就能无感地使用下载的依赖了。\n例如直接在命令里使用：\n1 cmake -DCMAKE_TOOLCHAIN_FILE=/home/kerolt/Workspace/profiler-cpp/build/Debug/generators/conan_toolchain.cmake ... 或者通过 CMakePresets（CMake \u0026gt;= 3.19）来使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { \u0026#34;version\u0026#34;: 8, \u0026#34;configurePresets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;debug\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;Profiler Debug 构建\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;正在使用编译器: C = /usr/bin/gcc, CXX = /usr/bin/g++\u0026#34;, \u0026#34;binaryDir\u0026#34;: \u0026#34;${sourceDir}/build/Debug\u0026#34;, // 就是这个 toolchainFile ~ \u0026#34;toolchainFile\u0026#34;: \u0026#34;${sourceDir}/build/Debug/generators/conan_toolchain.cmake\u0026#34;, \u0026#34;cacheVariables\u0026#34;: { \u0026#34;CMAKE_BUILD_TYPE\u0026#34;: \u0026#34;Debug\u0026#34;, \u0026#34;CMAKE_CXX_STANDARD\u0026#34;: \u0026#34;23\u0026#34;, \u0026#34;CMAKE_EXPORT_COMPILE_COMMANDS\u0026#34;: \u0026#34;ON\u0026#34; } } ] } 总结 理解了 CMake 是如何查找依赖包后，二者的协同工作原理就明了了：Conan 通过自动生成的配置能告诉 CMake 在哪里找依赖包后，就能直接在 CMakeLists.txt 中使用 find_package 来查找对应的依赖了。\n","permalink":"https://kerolt.github.io/p/cmake%E4%B8%8Econan%E5%8D%8F%E5%90%8C%E5%B7%A5%E4%BD%9C%E7%90%86%E8%A7%A3/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e最近根据 \u003ca href=\"https://eunomia.dev/zh/tutorials/12-profile/\"\u003e【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】\u003c/a\u003e 这篇文章，写了一个性能分析的小工具。不过文章中的实现是 rust，但我还不怎么熟悉 rust，因而打算参考其代码使用 c++ 来实现相同的功能。项目地址：\u003ca href=\"https://github.com/kerolt/profiler\"\u003eprofiler\u003c/a\u003e。\u003c/p\u003e","title":"CMake与Conan协同工作理解"},{"content":"在 【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】 这篇文章中， profiler 工具的实现是使用了 rust 编写的 blazesym 库，而我现在打算使用 C++ 实现，还好这个库提供了 C API，但是需要我们将这个 Rust 库的构建集成到 CMake 中。\nCorrosion 可将 Rust 集成到现有 CMake 项目中，它是一个 CMake 工具链，旨在让 CMake 像对待原生 C++ 子项目一样对待 Rust 的 Cargo 项目。Corrosion 将手动调用 cargo build，处理复杂的跨平台路径、构建配置（Debug/Release）以及繁琐的链接参数这些过程完全自动化了。\nCorrosion 的核心功能 Corrosion 的工作原理是在 CMake 层面为 Cargo 包装了一层“外壳”，其主要功能包括：\n自动处理构建类型：CMake 的 Release 或 Debug 模式会自动映射到 Cargo 的 --release 或默认模式。 目标导入：将 Rust 的 staticlib 或 cdylib 导入为标准的 CMake 目标（Targets），你可以直接对它们使用 target_link_libraries。 跨平台交叉编译：它能自动将 CMake 的交叉编译设置（如 Android, iOS, 嵌入式等）传递给 Rust 的目标三元组（Target Triple）。 多包支持：支持 Cargo 工作区（Workspaces）和单个 Crate。 项目结构 在我的项目 profiler 中使用 Corrosion 时，项目布局如下：\n1 2 3 4 5 6 profiler/ ├── CMakeLists.txt ├── src ├── third_party/ │ └── blazesym └── cmake/ # 存放 Corrosion 脚本 如何在 CMake 中使用 Corrosion （1）通过 FetchContent 自动下载并集成 1 2 3 4 5 6 7 8 include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git GIT_TAG v0.6.0 ) FetchContent_MakeAvailable(Corrosion) （2）导入 Rust 项目 1 2 3 4 5 # 这里的路径指向包含 Cargo.toml 的目录 corrosion_import_crate( MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/third_party/blazesym/Cargo.toml NO_AUTO_EXE ) （3）配置静态库 在 blazesym 的 C API 构建时，文档中说明了需要链接的 C 库：\n1 -lrt -ldl -lpthread -lm 为了方便后面的构建目标使用 blazesym 库，我的做法是将其制作的静态库和头文件“打包”，方便构建目标通过 target_link_libraries 使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 find_package(Threads REQUIRED) find_library(LIB_RT rt) find_library(LIB_DL dl) find_library(LIB_M m) # 定义并配置静态库目标 add_library(blazesym STATIC IMPORTED GLOBAL) set_target_properties(blazesym PROPERTIES IMPORTED_LOCATION \u0026#34;${CMAKE_CURRENT_BINARY_DIR}/libblazesym_c.a\u0026#34; INTERFACE_INCLUDE_DIRECTORIES \u0026#34;${CMAKE_CURRENT_SOURCE_DIR}/third_party/blazesym/capi/include\u0026#34; INTERFACE_LINK_LIBRARIES \u0026#34;Threads::Threads;${LIB_RT};${LIB_DL};${LIB_M}\u0026#34; ) （4）使用静态库 可以将刚刚的三步的代码合并成一个 cmake 文件中放在项目根目录下的 cmake 目录中（例如我这里的 cmake/SetupBlazesym.cmake），然后在项目的 CMakelists.txt 中 通过 include 使用：\n1 2 3 4 5 include(\u0026#34;${CMAKE_CURRENT_SOURCE_DIR}/cmake/SetupBlazesym.cmake\u0026#34;) add_executable(profiler src/...) target_link_libraries(profiler PRIVATE blazesym) ","permalink":"https://kerolt.github.io/p/%E5%A6%82%E4%BD%95%E5%9C%A8cmake%E5%B7%A5%E7%A8%8B%E4%B8%AD%E4%BD%BF%E7%94%A8rust%E5%BA%93blazesym/","summary":"\u003cp\u003e在 \u003ca href=\"https://eunomia.dev/zh/tutorials/12-profile/\"\u003e【eBPF 入门实践教程十二：使用 eBPF 程序 profile 进行性能分析】\u003c/a\u003e 这篇文章中， profiler 工具的实现是使用了 rust 编写的 \u003ccode\u003eblazesym\u003c/code\u003e 库，而我现在打算使用 C++ 实现，还好这个库提供了 C API，但是需要我们将这个 Rust 库的构建集成到 CMake 中。\u003c/p\u003e","title":"如何在CMake工程中使用Rust库blazesym"},{"content":" [!note] 注意！ 以下代码需要支持 C++23 的编译器\n这里利用 std::expected 和 std::unexpected 进行模仿：\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 #include \u0026lt;print\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;expected\u0026gt; template \u0026lt;typename E = std::string\u0026gt; using Err = std::unexpected\u0026lt;E\u0026gt;; template \u0026lt;typename T, typename E = std::string\u0026gt; using Result = std::expected\u0026lt;T, E\u0026gt;; struct Inner { int a, b; }; struct X { Inner* i_; X(Inner* i) { std::println(\u0026#34;X()\u0026#34;); i_ = i; } ~X() { std::println(\u0026#34;~X() delete inner obj\u0026#34;); delete i_; } }; auto foo(bool flag) -\u0026gt; Result\u0026lt;X\u0026gt; { if (!flag) { return Err\u0026lt;\u0026gt;(\u0026#34;Flag is false!\u0026#34;); } Inner* i = new Inner(); i-\u0026gt;a = 1; i-\u0026gt;b = 2; return Result\u0026lt;X\u0026gt;{i}; } int main() { std::println(\u0026#34;main--1\u0026#34;); { std::println(\u0026#34;main--2\u0026#34;); auto x = foo(true); std::println(\u0026#34;main--3\u0026#34;); } { std::println(\u0026#34;main--4\u0026#34;); auto x = foo(false); std::println(\u0026#34;Error message is: {}\u0026#34;, x.error()); std::println(\u0026#34;main--5\u0026#34;); } std::println(\u0026#34;main--6\u0026#34;); } 输出为：\n1 2 3 4 5 6 7 8 9 main--1 main--2 X() main--3 ~X() delete inner obj main--4 Error message is: Flag is false! main--5 main--6 ","permalink":"https://kerolt.github.io/p/c-%E5%AF%B9rust%E4%B8%ADresult%E7%9A%84%E7%AE%80%E5%8D%95%E6%A8%A1%E4%BB%BF/","summary":"\u003cblockquote\u003e\n\u003cp\u003e[!note] 注意！\n以下代码需要支持 C++23 的编译器\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这里利用 \u003ccode\u003estd::expected\u003c/code\u003e 和 \u003ccode\u003estd::unexpected\u003c/code\u003e 进行模仿：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e56\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e57\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e58\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;print\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;string\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;expected\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etypename\u003c/span\u003e \u003cspan class=\"n\"\u003eE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"n\"\u003eErr\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eunexpected\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eE\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etypename\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003etypename\u003c/span\u003e \u003cspan class=\"n\"\u003eE\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eE\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"nc\"\u003eInner\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"nc\"\u003eX\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eInner\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ei_\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eInner\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;X()\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ei_\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e~\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;~X() delete inner obj\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003edelete\u003c/span\u003e \u003cspan class=\"n\"\u003ei_\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eauto\u003c/span\u003e \u003cspan class=\"nf\"\u003efoo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eflag\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eflag\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eErr\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Flag is false!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eInner\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eInner\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eX\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--2\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eauto\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efoo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--3\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--4\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eauto\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efoo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Error message is: {}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--5\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;main--6\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e输出为：\u003c/p\u003e","title":"C++对Rust中Result的简单模仿"},{"content":"1、Windhawk 通过一些插件来改善体验。\n其中“Disable grouping on the taskbar”非常有用，可以让一个应用开多个窗口时任务栏图标不会堆叠，而是每个窗口一个图标。\n2、右键扩展 关闭右键扩展：\n1 2 reg add \u0026#34;HKCU\\Software\\Classes\\CLSID\\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\\InprocServer32\u0026#34; /f /ve taskkill /f /im explorer.exe \u0026amp; start explorer.exe 如果想要恢复右键扩展：\n1 2 reg delete \u0026#34;HKCU\\Software\\Classes\\CLSID\\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\u0026#34; /f taskkill /f /im explorer.exe \u0026amp; start explorer.exe 3、ContextMenuManager 链接：https://github.com/BluePointLilac/ContextMenuManager\n右键菜单内容管理工具。\n4、Dism++ Windows 系统清理、配置项调整。\n5、用户名调整为英文 简单来说就是重命名一个目录 + 更新一个对应的注册表项。\n具体看知乎的文章：https://zhuanlan.zhihu.com/p/509804656\n6、修改默认目录名为英文 系统路径内的目录不仅有图标还显示中文名称，但是打开路径的时候显示的却是英文，如何修改呢？\n（1）显示受保护的操作系统文件\n先要显示受保护的操作系统文件，设置完成之后将会看到 desktop.ini 文件（ desktop.ini 在每个目录内都有一个），如果没看到，就要勾选“查看”-\u0026gt;“显示”-\u0026gt;“隐藏的项目”。\n（2）编辑 desktop.ini\n如果要设置哪个目录的别名就找到哪个目录内的 desktop.ini 文件，并编辑下图红框中的内容：\n将 LocalizedResourceName 最后的数字修改为想要的目录名称，设置完成之后保存 刷新就 OK 了，设置完成注意要 隐藏受保护的操作系统文件。\n好像不同版本后面的数字不同，在修改前记得保存。\n7、用户目录下创建数据盘的目录软连接 这样做的目的是我个人想要让用户目录和 Linux 中的 home 目录比较一致，但软件、图片、文档我又不想保存在 C 盘，而是用软连接连接到数据盘（例如 D 盘）。\n操作很简单，就是使用 mklink 命令：\n1 mklink /D \u0026#34;C:\\Users\\\u0026lt;用户名\u0026gt;\\\u0026lt;目录名称\u0026gt;\u0026#34; \u0026#34;D:\\\u0026lt;目录名称\u0026gt;\u0026#34; 其中 /D 表示创建的是目录符号链接（软连接）。 8、无拓展名文件设置默认打开方式 管理员状态下运行 cmd，使用 assoc 查看无拓展的关联类型（assoc 用来关联后缀与文件类型）\n1 2 C:\\Users\\kerolt\u0026gt;assoc . 没有为扩展名 . 找到文件关联 定义无后缀文件对应的文件类型（此处为 No Extension，可以自定义），设置关联：\n1 2 C:\\Users\\kerolt\u0026gt;assoc .=No Extension .=No Extension 而 ftype 用来关联文件类型和打开其的应用程序：\n1 2 C:\\Users\\kerolt\u0026gt;ftype \u0026#34;No Extension\u0026#34;=\u0026#34;D:\\Application\\Notepad3\\Notepad3.exe\u0026#34; \u0026#34;%1\u0026#34; No Extension=\u0026#34;D:\\Application\\Notepad3\\Notepad3.exe\u0026#34;\u0026#34;%1 以上几步设置好后，无后缀文件的默认打开方式就变成了 Notepad3，并且默认图标也不再是一个丑丑的空白图标了，变成了 Notepad3 的相关图标。\n","permalink":"https://kerolt.github.io/p/windows-%E4%B8%AA%E4%BA%BA%E8%A3%85%E6%9C%BA%E9%85%8D%E7%BD%AE/","summary":"\u003ch2 id=\"1windhawk\"\u003e1、Windhawk\u003c/h2\u003e\n\u003cp\u003e通过一些插件来改善体验。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://obsidian-image-oss.oss-cn-shanghai.aliyuncs.com/Pasted%20image%2020260106152210.png\"\u003e\u003c/p\u003e\n\u003cp\u003e其中“Disable grouping on the taskbar”非常有用，可以让一个应用开多个窗口时任务栏图标不会堆叠，而是每个窗口一个图标。\u003c/p\u003e","title":"Windows 个人装机配置"},{"content":"Nginx 作为反向代理服务器，需要精确匹配客户端请求路径，并将请求转发到后端服务。在这个过程中：\nlocation 指令：决定哪些请求会被当前配置块处理 proxy_pass 指令：决定如何将请求转发到后端 而斜杠在这两个指令中的不同位置，会改变 Nginx 的匹配逻辑和路径传递行为，配置不当可能导致 404 错误、路径污染或安全风险。\n1、Location 配置：/api/vs /api 的核心差异 1.1 语法对比 配置 匹配模式 精确要求 示例匹配结果 location /api/ 前缀匹配（带斜杠） 必须以 /api/ 开头 /api/user✅ /api❌ /api-test❌ location /api 前缀匹配（无斜杠） 以 /api 开头即可 /api/user✅ /api✅ /api-test✅ 1.2 语义解释 /api/：表示一个 \u0026quot; 目录 \u0026quot; 路径，Nginx 会将其视为完整的路径段，只匹配以 /api/ 开头的请求。这种配置更精确，避免了意外匹配。\n/api：表示一个 \u0026quot; 前缀字符串 \u0026ldquo;，匹配所有以 /api 开头的请求，包括 /api 本身、/api/xxx 以及 /api-anything。这种配置更宽泛，可能产生误匹配。\n1.3 实际场景分析 场景 1：API 接口路由（推荐使用 /api/）\n1 2 3 4 5 6 7 8 9 10 11 # 推荐配置：精确匹配API目录 location /api/ { proxy_pass http://backend; # 只处理/api/下的所有请求，不会误匹配/api-test } # 不推荐：可能误匹配其他路径 location /api { # 会匹配/api、/api/、/api-test、/api-v2等 # 如果后端没有/api-test接口，可能返回404或错误数据 } 场景 2：静态资源目录（必须使用 /xxx/）\n1 2 3 4 5 6 7 8 9 10 11 # 静态文件目录，必须带斜杠 location /static/ { alias /path/to/static/; # 确保只匹配/static/目录下的文件 } # 错误配置：可能误匹配静态文件 location /static { # 会匹配/static、/static/、/static-file.txt # 如果存在/static-file.txt文件，可能被错误处理 } 1.4 优先级与冲突处理 当同时存在多个 location 时，Nginx 按最长前缀匹配原则。但需要注意：\nlocation /api/ 和 location /api 可能同时匹配 /api/user，此时 /api/ 优先级更高（更长匹配） 建议避免同时配置，以免产生预期外的匹配结果 2、Proxy_pass 配置：斜杠在 URL 末尾的影响 2.1 核心区别对比 配置方式 后端接收的路径 URI 传递行为 适用场景 proxy_pass http://127.0.0.1:8080 保留原始 URI 传递完整路径 常规代理，需要保留路径上下文 proxy_pass http://127.0.0.1:8080/ 去除 location 匹配部分 只传递 location 后的部分 路径重写，去除前缀 2.2 具体行为演示 假设配置：\n1 2 3 4 5 6 7 location /api/ { # 配置1：不带斜杠 proxy_pass http://127.0.0.1:8080; # 配置2：带斜杠 # proxy_pass http://127.0.0.1:8080/; } 请求路径映射结果：\n客户端请求 配置 1（不带/）→ 后端路径 配置 2（带/）→ 后端路径 /api/user /api/user /user /api/user/list /api/user/list /user/list /api/ /api/ / 关键规则：\nproxy_pass 末尾无斜杠：将 location 匹配的完整路径传递给后端 proxy_pass 末尾有斜杠：将 location 匹配部分去除，只传递剩余部分 2.3 实际应用场景 场景 1：后端服务需要完整路径（推荐配置 1）\n1 2 3 4 5 6 # 后端Spring Boot应用：@RequestMapping(\u0026#34;/api\u0026#34;) location /api/ { proxy_pass http://127.0.0.1:8080; # 请求/api/user → 后端收到/api/user # Controller的@GetMapping(\u0026#34;/user\u0026#34;)可以正常匹配 } 场景 2：后端服务无前缀（使用配置 2）\n1 2 3 4 5 6 # 后端服务直接处理/user路径 location /api/ { proxy_pass http://127.0.0.1:8080/; # 请求/api/user → 后端收到/user # 后端Controller：@GetMapping(\u0026#34;/user\u0026#34;)直接匹配 } 场景 3：路径重写场景\n1 2 3 4 5 # 将/static映射到后端的/static目录 location /static/ { proxy_pass http://127.0.0.1:8080/static/; # 请求/static/css/style.css → 后端收到/static/css/style.css } 2.4 常见误区 误区 1：认为 proxy_pass 的斜杠只是格式问题，实际效果相同\n误区 2：混淆 location 斜杠和 proxy_pass 斜杠的作用\nlocation /api/ 的斜杠：影响路径匹配规则 proxy_pass http://backend/ 的斜杠：影响 URI 传递行为 两者是独立的配置项 3、综合配置：Location 与 Proxy_pass 的协同工作 3.1 推荐的最佳实践组合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 场景：API网关，精确匹配API路径并保留完整上下文 location /api/ { proxy_pass http://127.0.0.1:8080; # 匹配规则：只处理/api/开头的请求 # 路径传递：保留完整路径（/api/user → /api/user） # 后端服务可以正常处理带前缀的路径 } # 场景：静态资源代理 location /static/ { proxy_pass http://127.0.0.1:8080/static/; # 匹配规则：只处理/static/目录 # 路径传递：保留相对路径（/static/css/style.css → /static/css/style.css） } 3.2 错误配置示例 1 2 3 4 5 6 7 8 9 10 11 12 13 # 问题配置：可能产生路径污染 location /api { proxy_pass http://127.0.0.1:8080/; # 问题1：会匹配/api-test等非API路径 # 问题2：路径被重写（/api/user → /user） # 如果后端没有/user接口，返回404 } # 正确改进方案 location /api/ { proxy_pass http://127.0.0.1:8080; # 或根据后端需求选择是否带斜杠 } 3.3 调试与验证方法 方法 1：查看 Nginx 访问日志\n1 2 # 在nginx.conf中配置日志格式 log_format main \u0026#39;$remote_addr - $request - $upstream_addr\u0026#39;; 方法 2：后端服务日志查看\n查看后端应用日志，确认实际接收的请求路径\n方法 3：使用 curl 测试\n1 2 3 # 测试不同路径的响应 curl -v http://nginx-host/api/user curl -v http://nginx-host/api 方法 4：添加调试 header\n1 2 3 4 5 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header X-Original-URI $request_uri; # 在header中查看原始URI } 4、总结与最佳实践 4.1 核心要点总结 配置项 关键规则 推荐做法 location /api/vs /api /api/ 是目录匹配，更精确；/api 是前缀匹配，更宽泛 API 路由优先用 /api/，避免误匹配 proxy_pass 末尾斜杠 无斜杠保留完整路径，有斜杠去除前缀 根据后端需求选择，默认建议保留完整路径 组合使用 两者独立作用，需协同考虑 明确业务需求，测试验证路径映射 4.2 配置原则 明确性优先：配置应明确表达意图，避免模糊匹配 测试驱动：部署前用真实请求测试所有路径 文档注释：在配置文件中添加注释说明匹配逻辑和路径传递规则 团队约定：建立统一的配置规范，减少理解成本 4.3 常见问题速查 问题现象 可能原因 解决方案 访问 /api 返回 404 location /api/ 不匹配 /api 添加 location = /api 或重定向 /api-test 被错误代理 使用了 location /api 改为 location /api/ 后端收到错误路径 proxy_pass 斜杠配置错误 根据后端需求调整斜杠 路径污染 无斜杠前缀匹配过宽 使用带斜杠的精确匹配 写在最后 Nginx 配置中的斜杠看似简单，实则体现了“精确配置”的工程哲学。在实际项目中，建议：\n理解原理：不要死记硬背，理解每个斜杠的语义含义 小步验证：每次修改配置后，用真实请求验证效果 监控告警：配置日志监控，及时发现异常匹配 版本控制：将 Nginx 配置纳入版本管理，便于追溯和回滚 ","permalink":"https://kerolt.github.io/p/nginxlocation%E4%B8%8Eproxy_pass%E9%85%8D%E7%BD%AE%E4%B8%AD%E7%9A%84%E6%96%9C%E6%9D%A0%E5%B7%AE%E5%BC%82/","summary":"\u003cp\u003eNginx 作为反向代理服务器，需要精确匹配客户端请求路径，并将请求转发到后端服务。在这个过程中：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003elocation\u003c/code\u003e 指令\u003c/strong\u003e：决定哪些请求会被当前配置块处理\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eproxy_pass\u003c/code\u003e 指令\u003c/strong\u003e：决定如何将请求转发到后端\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e而斜杠在这两个指令中的不同位置，会改变 Nginx 的匹配逻辑和路径传递行为，配置不当可能导致 404 错误、路径污染或安全风险。\u003c/p\u003e","title":"【Nginx】location与proxy_pass配置中的斜杠差异"},{"content":"首先确定网络是用什么工具管理：\n1 2 $ systemctl is-active NetworkManager active 说明 网络由 NetworkManager 管理，这种情况修改 IP 通常使用 nmcli：\n修改静态 IP 看当前连接名 1 nmcli connection show 输出类似：\n1 2 NAME UUID TYPE DEVICE Wired connection 1 2d3e1f6a-4e87-4a76-8f23-2f459c8c112b ethernet enp60s0 这里的 NAME（如 Wired connection 1）就是连接名，后面命令要用到。\n修改为静态 IP 假设你想把 IP 设置为：\nIP：192.168.1.100/24 网关：192.168.1.1 DNS：8.8.8.8, 1.1.1.1 执行：\n1 2 3 4 sudo nmcli con mod \u0026#34;Wired connection 1\u0026#34; ipv4.addresses 192.168.1.100/24 sudo nmcli con mod \u0026#34;Wired connection 1\u0026#34; ipv4.gateway 192.168.1.1 sudo nmcli con mod \u0026#34;Wired connection 1\u0026#34; ipv4.dns \u0026#34;8.8.8.8,1.1.1.1\u0026#34; sudo nmcli con mod \u0026#34;Wired connection 1\u0026#34; ipv4.method manual 注意 \u0026quot;Wired connection 1\u0026quot; 需要替换为你自己的连接名。 如果名字里有空格，必须加引号。\n应用修改（立即生效） 1 2 sudo nmcli con down \u0026#34;Wired connection 1\u0026#34; sudo nmcli con up \u0026#34;Wired connection 1\u0026#34; 验证 1 2 ip addr show enp60s0 ip route 你应该能看到：\n1 2 inet 192.168.1.100/24 ... default via 192.168.1.1 ... 改回 DHCP 自动获取 如果想恢复为 DHCP：\n1 2 sudo nmcli con mod \u0026#34;Wired connection 1\u0026#34; ipv4.method auto sudo nmcli con up \u0026#34;Wired connection 1\u0026#34; DHCP 重新获取 IP 检查是否是固定分配 IP 1 2 3 4 sudo nmcli device disconnect enp60s0 sudo dhclient -r enp60s0 # 释放旧租约（告诉 DHCP 我不要这个 IP 了） sudo pkill dhclient # 确保没有旧 dhclient 残留 sudo nmcli device connect enp60s0 # 重新请求新 IP 如果上面的操作无法获得新的 IP，那就说明 DHCP 服务器端 是按 MAC 地址固定分配。\n获取原本的 MAC 地址 可以临时更改 MAC 来解决，首先获取一下原本的 MAC：\n1 2 3 4 ip link show enp60s0 # 或者 nmcli device show enp60s0 | grep -i hwaddr 修改临时 MAC 修改为临时 MAC：\n1 2 3 sudo nmcli connection modify \u0026#34;Wired connection 1\u0026#34; 802-3-ethernet.cloned-mac-address random sudo nmcli connection down \u0026#34;Wired connection 1\u0026#34; sudo nmcli connection up \u0026#34;Wired connection 1\u0026#34; 这会让 DHCP 服务器以为是一个新设备，从而分配新 IP。 完成后可以恢复原 MAC：\n1 sudo nmcli connection modify \u0026#34;Wired connection 1\u0026#34; 802-3-ethernet.cloned-mac-address \u0026#34;YOUR_MAC\u0026#34; 实测可以获取到新的 IP。\n","permalink":"https://kerolt.github.io/p/linux%E4%BF%AE%E6%94%B9ip/","summary":"\u003cp\u003e首先确定网络是用什么工具管理：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ systemctl is-active NetworkManager\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eactive\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e说明 \u003cstrong\u003e网络由 NetworkManager 管理\u003c/strong\u003e，这种情况修改 IP 通常使用 \u003ccode\u003enmcli\u003c/code\u003e：\u003c/p\u003e\n\u003ch2 id=\"修改静态-ip\"\u003e修改静态 IP\u003c/h2\u003e\n\u003ch3 id=\"看当前连接名\"\u003e看当前连接名\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003enmcli connection show\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e输出类似：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNAME                UUID                                  TYPE      DEVICE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eWired connection \u003cspan class=\"m\"\u003e1\u003c/span\u003e  2d3e1f6a-4e87-4a76-8f23-2f459c8c112b  ethernet  enp60s0\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e这里的 \u003ccode\u003eNAME\u003c/code\u003e（如 \u003ccode\u003eWired connection 1\u003c/code\u003e）就是连接名，后面命令要用到。\u003c/p\u003e","title":"Linux修改IP"},{"content":"容器网络最典型的基础模型 —— 两个不同的命名空间通过 bridge 通信 。\nDocker 默认的 bridge 模式就是基于此原理,创建 docker0 bridge，容器 veth 对一端连接容器，一端连接 docker0，提供 NAT 功能让容器可以访问外网。\n一个简单的结构如下👇：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 +---------------------+ | root NS | (宿主机 / default netns) | bridge: br0(10.0.0.254/24) | | | veth0 ─────────┐ | \u0026lt;- veth0 在 root，master=br0 | │ | | veth2 ─────────┴──┘ \u0026lt;- veth2 在 root，master=br0 +---------------------+ │ ┌─────────────────┴─────────────────┐ │ │ veth0 \u0026lt;-\u0026gt; veth1 veth2 \u0026lt;-\u0026gt; veth3 (root, peer) (in ns1) (root, peer) (in ns2) ns1 namespace ns2 namespace ┌───────────────┐ ┌───────────────┐ │ veth1 │ │ veth3 │ │ 10.0.0.1/24 │ │ 10.0.0.2/24 │ └───────────────┘ └───────────────┘ [!note] veth (Virtual Ethernet Device) 虚拟以太网设备，总是成对出现，类似管道两端\n一端发送的数据会在另一端接收到 主要用途：连接不同网络命名空间，实现网络隔离环境间的通信 [!note] bridge\n虚拟交换机，工作在数据链路层（二层） 可以连接多个网络接口，在它们之间转发数据帧 Linux bridge 支持 STP、VLAN 等交换机构造特性 [!note] 网络命名空间 (netns)\n提供完全隔离的网络栈，包括接口、路由表、iptables 规则等 每个容器通常运行在独立的网络命名空间中 实现步骤如下：\n（1）创建两个命名空间\n1 2 ip netns add ns1 ip netns add ns2 （2）创建两对 veth\n每个命名空间各分配一个端口。\n1 2 ip link add veth0 type veth peer name veth1 ip link add veth2 type veth peer name veth3 现在有：\n1 2 veth0 \u0026lt;-\u0026gt; veth1 veth2 \u0026lt;-\u0026gt; veth3 （3）veth1 和 veth3 移入命名空间\n1 2 ip link set veth1 netns ns1 ip link set veth3 netns ns2 （4）创建 bridge（虚拟交换机）\n1 2 ip link add name br0 type bridge ip link set br0 up （5）把宿主机侧的 veth（veth0、veth2）加入 bridge\n1 2 3 4 ip link set veth0 master br0 ip link set veth2 master br0 ip link set veth0 up ip link set veth2 up （6）配置命名空间内部 IP\nns1:\n1 2 ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1 ip netns exec ns1 ip link set veth1 up ns2:\n1 2 ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth3 ip netns exec ns2 ip link set veth3 up 测试一下连通性：\n1 ip netns exec ns1 ping 10.0.0.2 如果一切正常，应该能 ping 通，因为这两个 namespace 的 \u0026quot; 网线 \u0026quot; 都插在同一个虚拟交换机 br0 上。\n（7）让宿主机也能参与通信\n给宿主机也分配 ip 在同一个网段：\n1 ip addr add 10.0.0.254/24 dev br0 然后：\n1 ip netns exec ns1 ping 10.0.0.254 也能通。这样，通过 veth pair + bridge + netns，可以模拟 Docker 那样是如何搭建一个多容器虚拟局域网。\n","permalink":"https://kerolt.github.io/p/linux%E4%B8%AD%E7%9A%84veth%E4%B8%8Ebridge/","summary":"\u003cp\u003e容器网络最典型的基础模型 —— \u003cstrong\u003e两个不同的命名空间通过 bridge 通信\u003c/strong\u003e 。\u003c/p\u003e\n\u003cp\u003eDocker 默认的 bridge 模式就是基于此原理,创建 \u003ccode\u003edocker0\u003c/code\u003e bridge，容器 veth 对一端连接容器，一端连接 docker0，提供 NAT 功能让容器可以访问外网。\u003c/p\u003e","title":"Linux中的veth与bridge"},{"content":"服务器是用旧笔记本装的 Debian13，然后用 ddns-go 做动态域名解析，但是装好之后貌似由于时钟没有校准因此 ddns-go 无法更新域名解析（使用的 aliyun），以下是解决方法。\n首先我使用 timedatectl 的结果如下：\n1 2 3 4 5 6 7 8 9 ~ $ timedatectl Local time: 六 2025-11-08 00:20:49 CST Universal time: 五 2025-11-07 16:20:49 UTC RTC time: 五 2025-11-07 16:20:49 Time zone: Asia/Shanghai (CST, +0800) System clock synchronized: no NTP service: n/a RTC in local TZ: no 使用 systemd-timesyncd 可以让系统自动校时：\n1 sudo timedatectl set-ntp true 如果显示：\n1 Failed to set ntp: NTP not supported 说明当前系统里没有可用的 NTP 同步服务（比如 systemd-timesyncd、chronyd 或 ntpd）被启用或支持，可以执行：\n1 2 sudo apt install systemd-timesyncd sudo systemctl enable --now systemd-timesyncd.service 之后再执行 timedatectl 可以看到：\n1 2 System clock synchronized: yes NTP service: active 这样就 ok 了。\n","permalink":"https://kerolt.github.io/p/linux%E6%A0%A1%E5%87%86%E6%97%B6%E9%97%B4/","summary":"\u003cp\u003e服务器是用旧笔记本装的 Debian13，然后用 ddns-go 做动态域名解析，但是装好之后貌似由于时钟没有校准因此 ddns-go 无法更新域名解析（使用的 aliyun），以下是解决方法。\u003c/p\u003e","title":"Linux校准时间"},{"content":"一、什么是“用户态”和“内核态” CPU 有不同的 特权级（Privilege Level）：\n用户态（User Mode）： 应用程序在这里运行，权限受限，比如不能直接访问硬件、不能修改页表等。\n内核态（Kernel Mode）： 操作系统内核运行在这里，拥有完全的访问权限，可以管理内存、设备、中断等。\n二、什么是“上下文切换（Context Switch）” “上下文”就是 CPU 当前正在执行的任务的所有状态，包括：\n寄存器内容（RIP、RSP、RAX 等） 程序计数器（Program Counter） 栈指针 内存映射（页表） 调度信息（优先级、时间片等） 上下文切换指的是 CPU 从一个执行上下文切换到另一个（比如进程 A → 进程 B）。\n三、内核态与用户态切换 ≠ 进程切换，但都属于“上下文切换” 这两种是不同层次的“切换”：\n类型 示例 是否涉及调度 开销大小 备注 用户态 → 内核态 系统调用、I/O、中断 否 小（几十到几百纳秒） 同一线程，只是 CPU 特权级变化 进程上下文切换 从进程 A → 进程 B 是 大（微秒级） 不仅换栈，还要换虚拟内存上下文 四、为什么内核/用户态切换有“开销” 内核态切换的代价来自几个部分：\n1. CPU 特权级变化 切换时 CPU 会：\n保存当前寄存器状态； 改变特权级（从 ring3 → ring0）； 切换到内核栈（每个线程有独立内核栈）； 执行系统调用入口代码（syscall、sysenter 指令）； 执行完后再反向恢复回用户态。 这些过程虽然不是“线程切换”，但都需要 CPU 做额外操作。\n2. 缓存污染（Cache/TLB flush） 在切换时，可能触发：\n指令缓存（I-Cache）和数据缓存（D-Cache）失效 TLB（页表缓存）失效 这会让下一次访问内存时性能下降。尤其是跨页表切换（进程切换）时，TLB 必须刷新。\n3. 管线冲刷（Pipeline Flush） 现代 CPU 使用深流水线和乱序执行，切换到内核态后，这些指令流需要被中断、清空、重新加载，浪费了几十个周期。\n4. 安全隔离检查（比如 KPTI） 在 Spectre/Meltdown 漏洞后，Linux 内核加了 KPTI（Kernel Page Table Isolation），在用户态和内核态之间切换时需要切换页表来隔离内核地址空间，进一步增加了 TLB flush 和页表切换开销。\n五、开销有多大？ 大致数量级（不同架构差异很大）：\n操作 典型开销 一次函数调用 1~5 ns 一次系统调用（空） 100~500 ns 一次进程上下文切换 1~5 µs 一次磁盘 I/O 系统调用 1 ms 以上 比如：\n1 2 $ strace -c ls # 可以看到每个系统调用耗时几十到几百纳秒 六、举个例子：read() 系统调用 当用户调用：\n1 read(fd, buf, size); 实际发生的事：\n程序在 用户态 调用 read； CPU 执行 syscall 指令，切到 内核态； 内核检查参数、文件描述符合法性； 调用文件系统层 → 设备驱动； 如果是磁盘 I/O，线程可能被阻塞，调度其他任务； 数据准备好后再切回用户态。 这整个过程涉及 多次用户态 ↔ 内核态切换 + 潜在调度切换。\n七、如何减少这种开销 性能优化中，常见的做法是减少切换频率：\n技术 思路 批处理系统调用 一次调用处理多个请求（如 readv, writev） 零拷贝 I/O 减少数据在内核与用户空间之间的复制 IO_uring / eBPF / XDP 通过内核接口减少 syscalls 次数或在内核内直接处理 epoll / io_uring 异步 I/O，减少阻塞导致的频繁切换 用户态网络栈（DPDK） 完全绕过内核态，直接用户态驱动网卡 ","permalink":"https://kerolt.github.io/p/%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E5%86%85%E6%A0%B8%E6%80%81%E4%B8%8E%E7%94%A8%E6%88%B7%E6%80%81%E5%88%87%E6%8D%A2%E7%9A%84%E4%B8%8A%E4%B8%8B%E6%96%87%E5%BC%80%E9%94%80/","summary":"\u003ch2 id=\"一什么是用户态和内核态\"\u003e一、什么是“用户态”和“内核态”\u003c/h2\u003e\n\u003cp\u003eCPU 有不同的 \u003cstrong\u003e特权级（Privilege Level）\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e用户态（User Mode）\u003c/strong\u003e：\n应用程序在这里运行，权限受限，比如不能直接访问硬件、不能修改页表等。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e内核态（Kernel Mode）\u003c/strong\u003e：\n操作系统内核运行在这里，拥有完全的访问权限，可以管理内存、设备、中断等。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"如何理解内核态与用户态切换的上下文开销"},{"content":" 环境：在 VMWare 上，启动了两个 Ubuntu 24.04.3 LTS，配置都为 2C2G。一台作为 master，一台作为 worker，需要为两台机器设置不同的 hostname。\nPrepare 安装好 Docker 后，设置一下 cgroup 的驱动：\n1 2 3 4 5 6 7 8 cat \u0026lt;\u0026lt;EOF | sudo tee /etc/docker/daemon.json { \u0026#34;exec-opts\u0026#34;: [\u0026#34;native.cgroupdriver=systemd\u0026#34;], \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;100m\u0026#34; }, \u0026#34;storage-driver\u0026#34;: \u0026#34;overlay2\u0026#34; } EOF 然后重启 docker：\n1 2 3 sudo systemctl enable docker sudo systemctl daemon-reload sudo systemctl restart docker 为了让 Kubernetes 能够检查、转发网络流量，需要修改 iptables 的配置,启 用 br_netfilter 模块:\n1 2 3 4 5 6 7 8 9 10 11 cat \u0026lt;\u0026lt;EOF | sudo tee /etc/modules-load.d/k8s.conf br_netfilter EOF cat \u0026lt;\u0026lt;EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward=1 # better than modify /etc/sysctl.conf EOF sudo sysctl --system 修改“/etc/fstab”，关闭 Linux 的 swap 分区，提升 Kubernetes 的性能：\n1 2 sudo swapoff -a sudo sed -ri \u0026#39;/\\sswap\\s/s/^#?/#/\u0026#39; /etc/fstab 安装 kubeadm 1 2 3 4 5 6 7 8 sudo apt install -y apt-transport-https ca-certificates curl curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add cat \u0026lt;\u0026lt;EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main EOF sudo apt update 然后安装 kubeadm、kubelet、kubectl：\n1 2 3 4 sudo apt install -y kubeadm=1.23.3-00 kubelet=1.23.3-00 kubectl=1.23.3-00 # 最好使用 apt-mark hold 锁定版本 sudo apt-mark hold kubeadm kubelet kubectl kubeadm 把 apiserver、etcd、scheduler 等组件都打包成了镜像,以容器的方式启动 Kubernetes。使用命令 kubeadm config images list 可以查看安装 Kubernetes 所需的镜像列表,参数 --kubernetes-version 可以指定版本号:\n1 kubeadm config images list --kubernetes-version v1.23.3 最好先想办法把列出的镜像下到本地。\n安装 Master 节点 kubeadm 安装 master 节点非常简单，只需要指定一下几个参数：\n--pod-network-cidr：设置集群里 Pod 的 IP 地址段 --apiserver-advertise-address：设置 apiserver 的 IP 地址，可以指定 apiserver 在哪个网卡上对外提供服务 --kubernetes-version：指定 Kubernetes 的版本号 1 2 3 4 sudo kubeadm init \\ --pod-network-cidr=10.10.0.0/16 \\ --apiserver-advertise-address=192.168.10.210 \\ --kubernetes-version=v1.23.3 完成后，它会提示接下来要干什么：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Alternatively, if you are the root user, you can run: export KUBECONFIG=/etc/kubernetes/admin.conf You should now deploy a pod network to the cluster. Run \u0026#34;kubectl apply -f [podnetwork].yaml\u0026#34; with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.116.129:6443 --token e7m92u.o3namkgyg9f5n3qw \\ --discovery-token-ca-cert-hash sha256:8d15d3ef5536d14706b8efba99b760212a61eccb3a18c179c307045bcea4e950 首先拷⻉ kubectl 的配置文件：\n1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config 完成后，运行 kubectl get node 会发现 master 节点的 STATUS 为 NotReady，这是因为我们还没有添加网络插件，集群的内部网络还没有正常运作。\n安装 Fannel 网络插件 保存 https://github.com/flannel-io/flannel/blob/master/Documentation/kube-flannel.yml ，然后运行修改其中的 net-conf.json 字段把 Network 改成之前参数 --pod-network-cidr 设置的地址段：\n1 2 3 4 5 6 7 net-conf.json: | { \u0026#34;Network\u0026#34;: \u0026#34;10.10.0.0/16\u0026#34;, \u0026#34;Backend\u0026#34;: { \u0026#34;Type\u0026#34;: \u0026#34;vxlan\u0026#34; } } 然后\n1 kubectl apply -f kube-flannel.yml 稍等一会儿 master 节点的状态就变为了 Ready。\nFlannel 所处的命名空间在 kube-flannel 中，可以通过 kubectl get pods -n kube-flannel 查看插件是否运行成功\n安装 Worker 节点 安装好了 master 节点后，worker 节点只需要执行：\n1 2 sudo kubeadm join 192.168.116.129:6443 --token e7m92u.o3namkgyg9f5n3qw \\ --discovery-token-ca-cert-hash sha256:8d15d3ef5536d14706b8efba99b760212a61eccb3a18c179c307045bcea4e950 它会连接 master 节点，然后拉取镜像，安装网络插件，最后把节点加入集群。\n如果需要重新显示 join 信息，可以运行：\n1 sudo kubeadm token create --print-join-command 安装完后，执行 kubectl get node，就会看到两个节点都是“Ready”状态。\n碰到的小问题 之前在安装网络插件时，会碰到安装完毕后 master 节点的状态始终还是“NotReady”，然后试着把 $HOME/.kube 目录删掉，然后：\nsudo kubeadm reset 重新 kubeadm init 重新安装 Fannel 插件 就 ok 了~\n","permalink":"https://kerolt.github.io/p/%E5%88%A9%E7%94%A8kubeadm%E6%90%AD%E5%BB%BA%E9%9B%86%E7%BE%A4/","summary":"\u003cblockquote\u003e\n\u003cp\u003e环境：在 VMWare 上，启动了两个 Ubuntu 24.04.3 LTS，配置都为 2C2G。一台作为 master，一台作为 worker，\u003cstrong\u003e需要为两台机器设置不同的 hostname\u003c/strong\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"prepare\"\u003ePrepare\u003c/h2\u003e\n\u003cp\u003e安装好 Docker 后，设置一下 cgroup 的驱动：\u003c/p\u003e","title":"利用kubeadm搭建集群"},{"content":" 在 eBPF 中，什么时候应该用 bpf_htons、bpf_htonl、bpf_ntohs、bpf_ntohl？\n这些 bpf_* 函数是 eBPF 程序中用于字节序转换的辅助函数。它们的作用与标准的 C 库中的 htons、htonl、ntohs、ntohl 类似，但针对 BPF 环境进行了优化或封装。它们用于在主机字节序（Host Byte Order）和网络字节序（Network Byte Order）之间进行转换。\n简而言之，当在 BPF 程序中处理网络协议头部（如 IP、TCP、UDP）中的多字节字段时，就需要使用这些函数。\n网络协议标准（例如，IPv4、TCP、UDP）规定所有多字节数值（如端口号、IP 地址、校验和等）都必须以网络字节序（大端序，Big-Endian）传输。\n[!note] bpf_ntohs (Network To Host Short) \u0026amp; bpf_ntohl (Network To Host Long)\n其将一个来自网络数据包的 16 位（s - short）或 32 位（l - long）数值从网络字节序转换为主机字节序。\n使用场景：\n读取网络包数据时： 当需要从接收到的网络数据包中提取字段（如 TCP/UDP 端口号、IP 地址、序列号、窗口大小等）并在 BPF 程序内部进行数值比较或计算时。 原因： BPF 程序运行在内核中，默认以主机字节序（通常是小端序，但在大端系统上可能是大端序）进行操作和计算。为了正确解释网络数据包中按大端序存储的值，必须进行转换。 [!note] bpf_htons (Host To Network Short) \u0026amp; bpf_htonl (Host To Network Long)\n其将一个 16 位或 32 位数值从主机字节序转换为网络字节序。\n使用场景：\n修改或生成网络包数据时： 当您的 BPF 程序（例如 XDP/TC 程序）需要修改数据包头部中的多字节字段（如修改目标端口号），或者创建新的网络数据包头部时。 原因： 任何写入到数据包中准备发送到网络上的多字节字段，都必须遵循网络标准，以网络字节序（大端序）存储。 函数 作用 字节数 使用场景 bpf_ntohs 网络 \u0026ndash;\u0026gt; 主机 2 字节（short） 读取端口号、校验和等。 bpf_ntohl 网络 \u0026ndash;\u0026gt; 主机 4 字节（long） 读取 IP 地址、序列号等。 bpf_htons 主机 \u0026ndash;\u0026gt; 网络 2 字节（short） 写入/修改端口号等。 bpf_htonl 主机 \u0026ndash;\u0026gt; 网络 4 字节（long） 写入/修改 IP 地址等。 ","permalink":"https://kerolt.github.io/p/ebpf%E4%B8%AD%E4%BD%95%E6%97%B6%E4%BD%BF%E7%94%A8%E5%AD%97%E8%8A%82%E5%BA%8F%E8%BD%AC%E6%8D%A2%E5%87%BD%E6%95%B0/","summary":"\u003cblockquote\u003e\n\u003cp\u003e在 eBPF 中，什么时候应该用 bpf_htons、bpf_htonl、bpf_ntohs、bpf_ntohl？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这些 \u003ccode\u003ebpf_*\u003c/code\u003e 函数是 eBPF 程序中用于\u003cstrong\u003e字节序转换\u003c/strong\u003e的辅助函数。它们的作用与标准的 C 库中的 \u003ccode\u003ehtons\u003c/code\u003e、\u003ccode\u003ehtonl\u003c/code\u003e、\u003ccode\u003entohs\u003c/code\u003e、\u003ccode\u003entohl\u003c/code\u003e 类似，但针对 BPF 环境进行了优化或封装。它们用于在\u003cstrong\u003e主机字节序\u003c/strong\u003e（Host Byte Order）和\u003cstrong\u003e网络字节序\u003c/strong\u003e（Network Byte Order）之间进行转换。\u003c/p\u003e","title":"eBPF中何时使用字节序转换函数"},{"content":"Nginx 负载均衡基准测试 1 2 3 4 5 6 7 8 9 / # wrk -c100 \u0026#34;http://172.17.0.5\u0026#34; Running 10s test @ http://172.17.0.5 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 12.31ms 12.66ms 84.49ms 82.31% Req/Sec 5.71k 4.96k 12.98k 63.00% 113583 requests in 10.04s, 17.87MB read Requests/sec: 11313.86 Transfer/sec: 1.78MB Latency（延迟）\nAvg 12.31ms：平均每个请求的响应时间为 12.31 毫秒 Stdev 12.66ms：标准差说明响应时间波动较大 Max 84.49ms：最长请求花了 84.49 毫秒 82.31% +/- Stdev：82% 的请求延迟在 ±1 标准差范围内（说明大部分请求分布较广） Req/Sec（每秒请求数）\nAvg 5.71k：平均每个线程每秒发出约 5710 个请求 Stdev 4.96k：标准差说明不同时间点吞吐波动较大 Max 12.98k：单个线程的最高瞬时请求速率为 12.98k/s 63.00% +/- Stdev：约 63% 的样本在这个范围内 接下来我们利用套接字 eBPF 程序优化\n理解 Nginx 负载均衡的本质 Nginx 作为反向代理或负载均衡器，本质上是：\n从客户端接收请求（通过 accept() 建立 TCP 连接） 选择一个上游后端（upstream server） 在用户态发起一个新的 TCP 连接 到该后端 在两个 socket 之间中转数据（前端 \u0026lt;-\u0026gt; 后端） 关键点：Nginx 的负载均衡策略（如 round robin、ip_hash）在用户态决定；而实际的数据流是两个独立的 socket，中间的数据转发也在用户态进行（效率上有损）。\n当我们引入 eBPF（extended Berkeley Packet Filter） 时，目标往往是：\n把负载均衡逻辑下放到内核态 减少 Nginx 用户态转发开销 实现更快的流量分发（零拷贝、低延迟） 例如：\n用 eBPF 程序在 XDP 或 TC ingress hook 阶段直接选择后端； 或者使用 sockmap/sockhash 在内核中直接“转发”两个 socket 之间的数据。 使用 socket 映射转发网络包 socket 映射（socket map）是 eBPF 中的一种 BPF map 类型，它可以在内核中保存 socket 的引用（即 TCP 连接对象）， 从而允许 eBPF 程序直接操作和转发这些连接上的数据。它有两种形式：\n类型 名称 特点 BPF_MAP_TYPE_SOCKMAP sockmap 数组结构，按索引存 socket BPF_MAP_TYPE_SOCKHASH sockhash 哈希结构，可按 key 查找 socket 可以理解为：sockmap/sockhash = 一个“连接路由表”，存放 \u0026lt;key, socket\u0026gt; 映射关系。 让 eBPF 程序在内核中知道“哪个 key 对应哪个 socket”。\n传统用户态转发的性能瓶颈，以 Nginx 为例：\nclient socket → Nginx 用户态 → upstream socket 数据流需要经过：\n2 次系统调用（recv + send） 2 次上下文切换（内核↔用户态） 1 次数据拷贝（内核缓冲区↔用户缓冲区） 当并发量很高（比如 wrk 10k QPS）时，这个过程就成为瓶颈。\n于是 Linux 引入了 socket 映射 + SK_MSG eBPF 程序可以直接在内核中完成转发：\n把所有连接（client 和 upstream）的 socket 引用注册进 sockmap； eBPF 程序在发送路径中（BPF_PROG_TYPE_SK_MSG）执行； 程序从 sockmap 中查找目标 socket； 内核直接把数据从一个 socket 发送到另一个 socket —— 零拷贝转发。 在 eBPF 优化体系中：\n控制面（Control Plane）：决定怎么转发（谁转发给谁） → 用户态（Nginx） 数据面（Data Plane）：实际执行转发 → 内核态（eBPF） socket 映射就是两者的“桥梁”：Nginx 负责把 socket 注册进 sockmap（告诉内核：client_fd 对应 upstream_fd），eBPF 程序负责从 sockmap 中查找目标并完成转发。\n这种模式的好处是：\neBPF 不需要自己维护连接； 用户态应用可以动态调整映射； 内核可以高性能地完成数据转发。 数据流简化示意图如下：\n传统 Nginx 转发 [Client Socket] ↓ recv() [用户态缓冲区] ↓ send() [Upstream Socket] eBPF + socket 映射优化后 [Client Socket] ↓ eBPF 程序 (SK_MSG) ↘︎ 查找 sockmap[key] ↓ [Upstream Socket] 整个转发在内核中完成，无需进入 Nginx 用户态。\n那优化的步骤如下：\n创建套接字映射； 在 BPF_PROG_TYPE_SOCK_OPS 类型的 eBPF 程序中，将新创建的套接字存入套接字映射中； 在流解析类的 eBPF 程序 (如 BPF_PROG_TYPE_SK_SKB 或 BPF_PROG_TYPE_SK_MSG ) 中，从套接字映射中提取套接字信息，并调用 BPF 辅助函数转发网络包; 加载并挂载 eBPF 程序到套接字事件。 socket 映射 这里使用 socket 映射（socket map）中的一种 BPF Map 类型：BPF_MAP_TYPE_SOCKHASH，它的值总是套接字文件描述符，而键则需要我们去定义。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct { __u32 source_ip; __u32 dest_ip; __u16 source_port; __u16 dest_port; __u32 protocol; } SockKey; struct { __uint(type, BPF_MAP_TYPE_SOCKHASH); __uint(key_size, sizeof(SockKey)); __uint(value_size, sizeof(int)); __uint(max_entries, 65535); __uint(map_flags, 0); } sock_ops_map SEC(\u0026#34;.maps\u0026#34;); BPF_PROG_TYPE_SOCK_OPS 是 内核套接字操作回调类型的 eBPF 程序，它能在 TCP 连接的生命周期事件被触发时执行，包括：\nBPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB：被动连接建立（如服务器 accept） BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB：主动连接建立（如客户端 connect） BPF_SOCK_OPS_TCP_CLOSE_CB：连接关闭 BPF_SOCK_OPS_STATE_CB：状态变化 也就是说，它可以跟踪 TCP socket 的整个生命周期，在连接建立/关闭时获取 socket 的五元组信息，并把 socket 存入 sockhash，从而让 内核态 eBPF 数据面程序（如 SK_MSG）可以直接高效地查找和转发 socket 数据。\n需要注意的是，BPF_PROG_TYPE_SOCK_OPS 程序跟踪了所有类型的套接字操作，我们只需要把新创建的套接字更新到映射中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SEC(\u0026#34;sockops\u0026#34;) int bpf_sockmap(struct bpf_sock_ops* sockops) { // 只处理 IPv4 连接 if (sockops-\u0026gt;family != AF_INET) { return BPF_OK; } // 只处理已建立的连接 if (sockops-\u0026gt;op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB \u0026amp;\u0026amp; sockops-\u0026gt;op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) { return BPF_OK; } SockKey key = { .dest_ip = sockops-\u0026gt;remote_ip4, .source_ip = sockops-\u0026gt;local_ip4, .dest_port = sockops-\u0026gt;remote_port, .source_port = bpf_htonl(sockops-\u0026gt;local_port), .protocol = sockops-\u0026gt;family, }; bpf_sock_hash_update(sockops, \u0026amp;sock_ops_map, \u0026amp;key, BPF_NOEXIST); return BPF_OK; } 转发 socket 转发 socket 可以使用 BPF_PROG_TYPE_SK_MSG 类型的 eBPF 程序，它在内核中的定义是这样的：\n1 2 BPF_PROG_TYPE(BPF_PROG_TYPE_SK_MSG, sk_msg, struct sk_msg_md, struct sk_msg) 捕获 socket 中的发送数据包，并根据之前的 socket 映射进行转发：\n1 2 3 4 5 6 7 8 9 10 11 12 13 SEC(\u0026#34;sk_msg\u0026#34;) int bpf_sock_redirect(struct sk_msg_md* msg) { SockKey key = { .source_ip = msg-\u0026gt;remote_ip4, .dest_ip = msg-\u0026gt;local_ip4, .source_port = msg-\u0026gt;remote_port, .dest_port = bpf_htonl(msg-\u0026gt;local_port), .protocol = msg-\u0026gt;family, }; bpf_msg_redirect_hash(msg, \u0026amp;sock_ops_map, \u0026amp;key, BPF_F_INGRESS); return SK_PASS; } 测试结果 1 2 3 4 5 6 7 8 9 / # wrk -c100 \u0026#34;http://172.17.0.5\u0026#34; Running 10s test @ http://172.17.0.5 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.88ms 6.22ms 55.43ms 68.53% Req/Sec 7.03k 4.99k 15.03k 69.00% 140073 requests in 10.05s, 22.04MB read Requests/sec: 13940.22 Transfer/sec: 2.19MB 吞吐提升 ≈ 23.2%。 平均延迟下降 ≈ 36.0%。 ","permalink":"https://kerolt.github.io/p/ebpf%E5%A2%9E%E5%BC%BAnginx%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/","summary":"\u003ch2 id=\"nginx-负载均衡基准测试\"\u003eNginx 负载均衡基准测试\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/ \u003cspan class=\"c1\"\u003e# wrk -c100 \u0026#34;http://172.17.0.5\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRunning 10s \u003cspan class=\"nb\"\u003etest\u003c/span\u003e @ http://172.17.0.5\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"m\"\u003e2\u003c/span\u003e threads and \u003cspan class=\"m\"\u003e100\u003c/span\u003e connections\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Thread Stats   Avg      Stdev     Max   +/- Stdev\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    Latency    12.31ms   12.66ms  84.49ms   82.31%\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    Req/Sec     5.71k     4.96k   12.98k    63.00%\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"m\"\u003e113583\u003c/span\u003e requests in 10.04s, 17.87MB \u003cspan class=\"nb\"\u003eread\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRequests/sec:  11313.86\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eTransfer/sec:      1.78MB\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eLatency（延迟）\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAvg 12.31ms\u003c/strong\u003e：平均每个请求的响应时间为 \u003cstrong\u003e12.31 毫秒\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStdev 12.66ms\u003c/strong\u003e：标准差说明响应时间波动较大\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMax 84.49ms\u003c/strong\u003e：最长请求花了 84.49 毫秒\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e82.31% +/- Stdev\u003c/strong\u003e：82% 的请求延迟在 ±1 标准差范围内（说明大部分请求分布较广）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eReq/Sec（每秒请求数）\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAvg 5.71k\u003c/strong\u003e：平均每个线程每秒发出约 \u003cstrong\u003e5710 个请求\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStdev 4.96k\u003c/strong\u003e：标准差说明不同时间点吞吐波动较大\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMax 12.98k\u003c/strong\u003e：单个线程的最高瞬时请求速率为 12.98k/s\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e63.00% +/- Stdev\u003c/strong\u003e：约 63% 的样本在这个范围内\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e","title":"eBPF增强Nginx负载均衡"},{"content":"前端\n虽然我不是一个专门搞前端的，但也会写点点 Vue。最近实验室项目写完后需要部署一下，这里我就使用了 Docker Compose 来部署。由于前端项目有前台用户端访问和后台管理端两个，而老师申请的服务器只开了 80 端口，因此我就打算使用 nginx 来反向代理：\nhttp://example.com 为前台 http://example.com/admin 为后台 ok，那我就用了一个 nginx 容器做网关，其 nginx.conf 如下：\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 server { listen 80; listen [::]:80; server_name _; # 后台管理 location /admin/ { proxy_pass http://frontend-admin; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 后端接口 location /api { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # minio location /images/ { proxy_pass http://minio:9000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 前台用户 location / { proxy_pass http://frontend-user/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location = /admin { return 301 /admin/; } } docker compose up -d 后，用户端能访问并且接口没问题，但是一到管理端 http://xx.xx.xx.xx/admin，就会是白屏，我检查了一下，管理端页面的标签栏标题是正确的，也就是 nginx 反向代理没问题，获取的 index-xxx.js 和 index-yyy.css 也没问题，那就奇了个怪了。\n研究了一番才发现，修改了 base 配置后，创建路由时也需要使用 BASE_URL（或等效值）的原因是为了保证前端路由与部署路径的一致性。而我在 vite.config.js 中的 base 如下：\n1 2 3 export default defineConfig({ base: \u0026#39;/admin/\u0026#39;, }) 在 vite.config.js 中设置的 base 选项，表示：\n所有静态资源（JS、CSS、图片等）的公共基础路径； 构建后的 HTML 文件中引用资源时会自动加上这个前缀； 构建后，所有资源路径会变成 /admin/assets/xxx.js。\nVue Router 默认假设应用部署在域名根路径（即 /）。如果你把应用部署在子路径下（如 https://example.com/admin/），而路由仍以 / 为基准，就会出现：\n页面刷新后 404； 路由跳转错误或无法匹配。 因此，Vue Router 需要知道“应用实际挂载的路径前缀”，这就是 history 配置中的 base 参数。\n在 Vite 项目中，import.meta.env.BASE_URL 是一个由 Vite 自动注入的变量，它的值就是 vite.config.js 中配置的 base。\n所以，在创建路由时这样写：\n1 2 3 4 5 6 7 // router/index.js import { createRouter, createWebHistory } from \u0026#39;vue-router\u0026#39; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [...] }) 这样就能确保：\n开发环境（base: '/'）和生产环境（base: '/admin/'）使用相同的代码； 路由前缀与静态资源前缀保持一致； 避免部署到子目录时出现空白页或路由错乱。 ","permalink":"https://kerolt.github.io/p/vue3%E8%B7%AF%E7%94%B1%E4%B8%8Evite%E7%9A%84base/","summary":"\u003cp\u003e前端\u003c/p\u003e\n\u003cp\u003e虽然我不是一个专门搞前端的，但也会写点点 Vue。最近实验室项目写完后需要部署一下，这里我就使用了 Docker Compose 来部署。由于前端项目有前台用户端访问和后台管理端两个，而老师申请的服务器只开了 80 端口，因此我就打算使用 nginx 来反向代理：\u003c/p\u003e","title":"Vue3路由与Vite的base"},{"content":"添加软件仓库 Debian12 1 2 3 4 5 6 7 8 9 10 11 # 1. 添加阿里的 Docker 镜像仓库证书；这里具体的url根据自身修改 curl -fsSL https://mirrors.aliyun.com/docker-ce/debian/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/aliyun-docker.gpg # 2. 添加仓库 echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/aliyun-docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian \\ $(lsb_release -cs) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null # 3. 安装 sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin Docky Linux 10 1 2 3 4 5 sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo sudo wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/rhel/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 加入 docker 用户组 安装完成后，通常我们要将自己加入 docker 用户组：\n1 usermod -aG docker ${USER} 配置镜像 之后还需配置一下国内镜像（/etc/docker/daemon.json）：\n1 2 3 4 5 6 7 8 { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://dockerproxy.net\u0026#34;, \u0026#34;https://hub-mirror.c.163.com\u0026#34;, \u0026#34;https://mirror.ccs.tencentyun.com\u0026#34;, \u0026#34;https://mirrors.aliyun.com\u0026#34; ] } 然后重启 docker：\n1 sudo systemctl restart docker 其他操作可以参考阿里云的文档：安装并使用Docker和Docker Compose\n","permalink":"https://kerolt.github.io/p/%E5%9B%BD%E5%86%85%E5%AE%89%E8%A3%85docker-ce/","summary":"\u003ch2 id=\"添加软件仓库\"\u003e添加软件仓库\u003c/h2\u003e\n\u003ch3 id=\"debian12\"\u003eDebian12\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 1. 添加阿里的 Docker 镜像仓库证书；这里具体的url根据自身修改\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl -fsSL https://mirrors.aliyun.com/docker-ce/debian/ubuntu/gpg \u003cspan class=\"p\"\u003e|\u003c/span\u003e sudo gpg --dearmor -o /etc/apt/keyrings/aliyun-docker.gpg\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 2. 添加仓库\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s2\"\u003e\u0026#34;deb [arch=\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003edpkg --print-architecture\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e signed-by=/etc/apt/keyrings/aliyun-docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003elsb_release -cs\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e stable\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 3. 安装\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt-get update\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"docky-linux-10\"\u003eDocky Linux 10\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo dnf config-manager --add-repo\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttps://download.docker.com/linux/centos/docker-ce.repo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/rhel/docker-ce.repo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"加入-docker-用户组\"\u003e加入 docker 用户组\u003c/h2\u003e\n\u003cp\u003e安装完成后，通常我们要将自己加入 docker 用户组：\u003c/p\u003e","title":"国内安装Docker CE"},{"content":" 对 https://docs.kernel.org/networking/napi.html 的总结\nNAPI (New API，但现已无特定含义) 是 Linux 内核中用于高效处理网络数据包的一种机制，旨在减少高负载下的中断开销。\n其​目的​​是为了解决传统基于中断的包处理在高流量下的性能问题（“中断活锁”）。通过混合​​中断​​和​​轮询​​模式，在低负载时使用中断保证低延迟，在高负载时切换到轮询保证高吞吐量。\n设备通过中断通知有新数据包 -\u0026gt; 内核调度对应的 NAPI 实例 -\u0026gt; 在软中断上下文（或内核线程）中轮询处理多个数据包 -\u0026gt; 处理完毕后再打开中断。\n核心数据结构与 API ​​struct napi_struct​​：\n核心数据结构，代表一个 NAPI 实例，保存其状态信息。 通常与一个中断或一个队列（RX/TX）相关联。 ​​控制 API (初始化和状态管理)​​：\nnetif_napi_add() / netif_napi_del(): 向系统添加/删除一个 NAPI 实例（通常附加到网络设备上）。 napi_enable() / napi_disable(): 启用/禁用 NAPI 实例。禁用状态下实例不会被调度，poll 方法不会被调用。​​注意​​：API 非幂等，错误调用顺序可能导致死锁或竞态。 ​​数据路径 API (调度与处理)​​：\nnapi_schedule(): ​​核心调度函数​​。通常在设备的中断处理程序中调用，通知内核有数据需要处理，并获取 NAPI 实例的所有权。 napi_schedule_irqoff(): napi_schedule() 的变体，用于已知在中断上下文中调用的情况，可优化中断屏蔽操作。 napi_complete_done(): ​​完成处理函数​​。当驱动程序的 poll 方法处理完所有事件后调用此函数，释放实例的所有权。​​警告​​：budget 为 0 时绝不能调用；若处理恰好用完 budget 且工作已完成，需谨慎返回 budget - 1 或等待下次调用。 驱动程序实现要点 ​​poll 方法​​：\n驱动必须实现的回调函数，由内核调度执行实际的数据包处理工作。 ​​参数 budget​​：限制一次 poll 调用最多可处理的​​接收​​（RX）数据包数量（发送 TX 处理无此限制）。若返回值为 budget，表示还有工作未完成，内核会再次调度；若小于 budget，表示本轮处理已完成。 ​​中断屏蔽策略​​：\n调度 NAPI 后应​​屏蔽设备中断​​，防止不必要的重复中断，直到 napi_complete_done() 被调用后再解除屏蔽。 推荐使用 napi_schedule_prep() 检查 + __napi_schedule() 调用的组合，确保在屏蔽中断后调度，避免竞态条件。 高级特性与配置 ​​实例映射​​：\n现代网卡多队列：通常一个 NAPI 实例对应一个中断源和一个队列对（RX+TX）。但映射关系灵活，可一对多或多对一。 ​​软件中断合并 (Coalescing)​​：\n可通过 gro_flush_timeout 和 napi_defer_hard_irqs 参数配置 NAPI 在数据包处理完后启动一个定时器，延迟解除中断屏蔽，以合并更多数据包，减少中断次数，提升吞吐量。可通过 sysfs 或 Netlink (netdev-genl) ​​按每个 NAPI 实例​​进行配置。\nGRO（Generic Receive Offload，通用接收卸载）是 Linux 内核中的一种网络数据包处理机制，主要用于​​在接收数据时将多个小数据包合并成一个大包​​，再上传给网络协议栈处理，以此减少 CPU 的开销，提升网络吞吐性能\n​​忙轮询 (Busy Polling)​​：\n允许用户态进程主动轮询设备，在中断触发前检查并处理数据包，​​以 CPU 资源换取极低延迟​​。 启用方式：套接字选项 SO_BUSY_POLL、系统参数 net.core.busy_poll、io_uring API 或基于 epoll 的配置。 ​​基于 epoll 的忙轮询​​：\n可从 epoll_wait 调用中直接触发 NAPI 处理。 要求：添加到同一 epoll 上下文的所有文件描述符应具有相同的 NAPI ID（可通过 SO_INCOMING_NAPI_ID 获取）。 可通过 ioctl (EPIOCSPARAMS) 设置 epoll_params 结构，精细控制每个 epoll 上下文的忙轮询超时 (busy_poll_usecs)、预算 (busy_poll_budget) 和偏好 (prefer_busy_poll)。 ​​IRQ 缓解 (Mitigation) 与挂起 (Suspension)​​：\n​​IRQ 缓解​​：使用 SO_PREFER_BUSY_POLL，承诺应用程序会定期忙轮询，驱动可​​长时间屏蔽中断​​。由 gro_flush_timeout 作为安全机制防止停滞。 ​​IRQ 挂停​​：更激进的机制。当 epoll_wait 成功获取事件时，启动一个 irq-suspend-timeout 定时器来​​挂起（屏蔽）IRQ​​。只要应用程序持续处理，IRQ 就保持挂起状态，避免任何中断干扰。超时或处理空闲后 fallback 到常规模式。需要与 gro_flush_timeout 等配合使用。 ​​线程化 NAPI​​：\n可配置 NAPI 在​​独立的内核线程​​中运行，而不是软中断上下文。可针对每个网络设备或每个 NAPI 实例进行配置（通过 sysfs 或 Netlink）。 优点：可能提供更可预测的调度或利于实时性（PREEMPT_RT）内核。 建议：将 NAPI 线程固定到与处理其中断相同的 CPU 上。 用户空间交互 ​​NAPI ID​​：每个 NAPI 实例有唯一 ID，用户空间可通过 SO_INCOMING_NAPI_ID 套接字选项获取。 ​​查询与配置​​：可通过 Netlink (netdev-genl 家族) 查询设备队列信息（包括 NAPI ID）和配置每个 NAPI 实例的参数（如合并参数、线程化开关）。可使用 tools/net/ynl/ 下的工具（如 cli.py）进行操作。 ​核心思想​​：中断通知 + 轮询处理，平衡延迟与吞吐。\n实现 poll 方法，正确使用 napi_schedule() 和 napi_complete_done()，管理好中断屏蔽。通过软件中断合并、忙轮询、IRQ 缓解/挂起等机制针对低延迟或高吞吐场景进行精细配置。支持多种处理上下文（软中断、线程），映射关系（实例 - 中断 - 队列）灵活。\n","permalink":"https://kerolt.github.io/p/linux-napi%E6%9C%BA%E5%88%B6%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93/","summary":"\u003cblockquote\u003e\n\u003cp\u003e对 \u003ca href=\"https://docs.kernel.org/networking/napi.html\"\u003ehttps://docs.kernel.org/networking/napi.html\u003c/a\u003e 的总结\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eNAPI (New API，但现已无特定含义) 是 Linux 内核中用于高效处理网络数据包的一种机制，旨在减少高负载下的中断开销。\u003c/p\u003e\n\u003cp\u003e其​目的​​是为了解决传统基于中断的包处理在高流量下的性能问题（“中断活锁”）。通过混合​\u003cstrong\u003e​中断​\u003c/strong\u003e​和​\u003cstrong\u003e​轮询​\u003c/strong\u003e​模式，在低负载时使用中断保证低延迟，在高负载时切换到轮询保证高吞吐量。\u003c/p\u003e","title":"Linux NAPI机制知识点总结"},{"content":"在完成 Rustdesk 自建服务器 的搭建后，我又想到：由于实验室服务器的公网 IPv6 地址 并非永久不变，直接使用 IP 进行远程连接（如 SSH 或 Rustdesk）并不现实，如何确保远程访问的稳定性和便利性呢。为了解决这一痛点，我决定利用 动态域名解析（DDNS） 技术，配合 ddns-go 和 阿里云 DNS 服务，将服务器的动态 IP 地址实时绑定到一个易于记忆的域名上。\n本文将记录我在配置过程中遇到的四个主要问题及其解决方案。\n问题 1：IPv6 临时地址导致域名解析不稳定 在启用 ddns-go 服务后，我发现尽管程序运行正常，但我的域名解析记录却频繁失效。经过排查，我意识到罪魁祸首是 IPv6 临时地址（Temporary Address）。这是一种为了增强用户隐私而设计的特性，系统会定期生成新的、临时的 IPv6 地址用于出站连接。当服务器的地址发生变化时，DDNS 服务来不及同步更新，就会导致域名无法解析到正确的 IP 地址。\n解决方案：禁用 IPv6 临时地址 要解决这个问题，最直接的方法就是从系统层面禁用 IPv6 临时地址功能，强制使用稳定的、非临时的地址。\n编辑 sysctl 配置文件： 使用 vim 或其他编辑器打开 sysctl.conf 文件，该文件用于在系统启动时配置内核参数。 1 sudo vim /etc/sysctl.conf 添加配置项： 在文件末尾添加以下两行，分别用于全局禁用和默认禁用所有网络接口的 IPv6 临时地址功能。 1 2 3 # 禁用 IPv6 临时地址 net.ipv6.conf.all.use_tempaddr = 0 net.ipv6.conf.enp4s0.use_tempaddr = 0 注意：如果只想针对特定网卡（例如 enp4s0）进行配置，可以添加 net.ipv6.conf.enp4s0.use_tempaddr = 0。你可以通过 ip a 命令查看你机器的网卡名称。\n应用配置： 执行以下命令，使新配置即时生效。 1 sudo sysctl -p 验证配置是否生效： 最后，可以通过查看特定参数的值来验证修改是否成功。如果返回 0，则表示临时地址已成功禁用。 1 2 cat /proc/sys/net/ipv6/conf/enp4s0/use_tempaddr # 预期输出: 0 问题 2：ddns-go 无法通过 systemd 启动 利用 systemctl status ddns-go 查看 ddns-go 运行状态时，看到：\n1 2 Unable to locate executable \u0026#39;/usr/local/bin/ddns-go\u0026#39;: Permission denied Failed at step EXEC spawning /usr/local/bin/ddns-go: Permission denied 但是对应的文件的权限我都给了，问了问 AI 后说可能造成的原因之一为SELinux 阻止执行（在 Fedora 上默认开启）。输入：\n1 sudo ausearch -m avc -ts recent | grep ddns-go 看到有拒绝记录，说明是 SELinux 问题。\n临时解决方案 1 sudo setenforce 0 永久解决方案 为该二进制打上正确的 SELinux 标签\n1 sudo restorecon -v /usr/local/bin/ddns-go 之后再重启 ddns-go 服务，就 OK 了。\n[!NOTE] 为什么会有 SELinux 相关问题？\nLinux 传统权限模型（用户/组/other + rwx）是 自主访问控制（DAC）：用户对自己文件有完全控制权；程序继承用户权限 → 一旦用户被攻破，攻击者可为所欲为\nSELinux 的目标是：即使程序或用户被攻破，也要限制它能做的事\n我是把 ddns-go 的可执行文件放在/usr/local/bin/下（复制过去的）\n而 SELinux 有预定义的“文件上下文”：\n/usr/bin/* → bin_t（可执行程序） /usr/local/bin/* → 默认可能不是 bin_t，尤其是手动复制进去的文件 所以即使文件有 chmod +x，SELinux 仍可能阻止执行\n问题 3：ddns-go 无法正确更新 IPv6 地址记录 解决了 IPv6 地址变化的问题后，我发现 ddns-go 服务依然无法正常工作，并抛出大量 DNS 相关的超时错误，例如：\n1 2 3 2025/08/29 10:01:37 查询域名信息发生异常! Get \u0026#34;[https://alidns.aliyuncs.com/?AccessKeyId=xxxxxx](https://alidns.aliyuncs.com/?AccessKeyId=xxxxxx)\u0026#34;: dial tcp: lookup alidns.aliyuncs.com on x.x.x.x:53: read udp x.x.x.x:59439-\u0026gt;x.x.x.x:53: i/o timeout 通过分析日志，我发现服务在尝试向外部 DNS 服务器（如 8.8.8.8 或 114.114.114.114）发起 DNS 请求时失败了。这通常是由于校园网或实验室网络的防火墙策略，限制了对外部公共 DNS 服务器的访问，强制所有 DNS 请求都通过内部服务器。\n解决方案：指定 ddns-go 使用可访问的 DNS 服务器 由于 ddns-go 默认使用系统 DNS，而系统 DNS 可能被强制转发到内部服务器，因此我们需要在启动命令中显式指定可访问的公共 DNS 服务器。\n编辑 ddns-go 服务文件：\n打开 /etc/systemd/system/ddns-go.service 文件。\n1 sudo vim /etc/systemd/system/ddns-go.service 修改 ExecStart 参数：\n在 ExecStart 行的末尾，添加 -dns 参数，指定一个或多个在国内访问稳定的公共 DNS，如阿里云的 223.5.5.5 和腾讯云的 119.29.29.29。\n1 2 3 4 5 6 7 [Service] StartLimitInterval=5 StartLimitBurst=10 ExecStart=/usr/local/bin/ddns-go \u0026#34;-l\u0026#34; \u0026#34;:9876\u0026#34; \u0026#34;-f\u0026#34; \u0026#34;300\u0026#34; \u0026#34;-cacheTimes\u0026#34; \u0026#34;5\u0026#34; \u0026#34;-c\u0026#34; \u0026#34;/root/.ddns_go_config.yaml\u0026#34; \u0026#34;-dns\u0026#34; \u0026#34;223.5.5.5\u0026#34; \u0026#34;-dns\u0026#34; \u0026#34;119.29.29.29\u0026#34; [Install] ... 重新加载并重启服务：\n保存文件后，重新加载 systemd 配置并重启 ddns-go 服务，使新参数生效。\n1 2 sudo systemctl daemon-reload sudo systemctl restart ddns-go 问题 4：电脑睡眠后网络中断，无法远程连接 解决了域名解析问题后，我又发现一个新情况：如果服务器闲置一段时间，SSH 连接就会断开。当物理唤醒电脑后，网络连接需要一段时间才能恢复。这是因为 系统默认的电源管理策略 会在电脑进入睡眠状态后，挂起或断开部分硬件（如网卡）以节省电量。\n解决方案：禁用系统休眠与闲置操作 1 sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 为了确保服务器 24/7 在线，我们需要修改电源管理配置，禁用自动休眠。\n编辑 systemd-logind 配置文件：\n打开 /etc/systemd/logind.conf 文件，该文件用于配置用户登录和电源管理行为。\n1 sudo vim /etc/systemd/logind.conf 修改配置参数：\n取消以下几行的注释（如果被注释），并将它们的值修改为 ignore 或 never。\n1 2 3 4 5 6 [Login] # 禁用闲置自动操作 IdleAction=ignore IdleActionSec=never # 禁用合盖自动操作 HandleLidSwitch=ignore 重启服务：\n修改完成后，重启 systemd-logind 服务使配置生效。\n1 sudo systemctl restart systemd-logind 解决完上述问题后，无论是在校内还是校外，我都可以通过一个固定的域名来使用服务器了。\n","permalink":"https://kerolt.github.io/p/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95ddns-go%E4%B8%8Eipv6%E7%BD%91%E7%BB%9C%E9%85%8D%E7%BD%AE/","summary":"\u003cp\u003e在完成 \u003ca href=\"https://zhuanlan.zhihu.com/p/1943305270387275457\"\u003eRustdesk 自建服务器\u003c/a\u003e 的搭建后，我又想到：由于实验室服务器的公网 \u003cstrong\u003eIPv6 地址\u003c/strong\u003e 并非永久不变，直接使用 IP 进行远程连接（如 SSH 或 Rustdesk）并不现实，如何确保远程访问的稳定性和便利性呢。为了解决这一痛点，我决定利用 \u003cstrong\u003e动态域名解析（DDNS）\u003c/strong\u003e 技术，配合 \u003cstrong\u003eddns-go\u003c/strong\u003e 和 \u003cstrong\u003e阿里云 DNS\u003c/strong\u003e 服务，将服务器的动态 IP 地址实时绑定到一个易于记忆的域名上。\u003c/p\u003e","title":"【问题记录】ddns-go与IPv6网络配置"},{"content":"对于现在常见的远程控制软件，例如 ToDesk、向日葵，其免费版本总有诸多限制（帧率低、分辨率低、控制时间有限制），而 RustDesk 的公共服务器现在在国内也不可用，因此我就想到了 RustDesk 的自建服务器。现在我们实验室有几台空闲的电脑，并且这些电脑都有 IPv6，这就省去了我购买云服务器的花费，可以实现零成本搭建远程服务😁\n安装 RustDesk Server 我使用 https://github.com/sshpc/rustdesktool 这里的脚本来一键安装，安装完后，RustDesk Server 的默认安装目录为：\n1 /usr/local/rustdesk-sever 开放端口 我们需要放行防火墙 TCP \u0026amp; UDP 端口 21115-21119，其中\n21115 是 hbbs 用作 NAT 类型测试 21116/UDP 是 hbbs 用作 ID 注册与心跳服务 21116/TCP 是 hbbs 用作 TCP 打洞与连接服务 21117 是 hbbr 用作中继服务 1 2 3 4 5 6 7 # 允许 TCP 端口 sudo ufw allow 21115:21119/tcp # 允许 UDP 端口 sudo ufw allow 21115:21119/udp sudo ufw enable 然后执行\n1 sudo ufw status 如果有如下类似输出，表明端口已经放行并且防火墙正在运行。\n1 2 3 4 5 6 7 8 状态：活动 至 动作 来自 -- -- -- 21115:21119/tcp ALLOW Anywhere 21115:21119/udp ALLOW Anywhere 21115:21119/tcp (v6) ALLOW Anywhere (v6) 21115:21119/udp (v6) ALLOW Anywhere (v6) 启动服务 在我们用脚本一键安装后，服务安装目录为：\n1 2 /usr/lib/systemd/system/RustDeskHbbr.service /usr/lib/systemd/system/RustDeskHbbs.service 可以通过 systemd 启动服务：\n1 2 3 4 5 sudo systemctl start RustDeskHbbs sudo systemctl start RustDeskHbbr # 或者重载systemd配置 # sudo systemctl daemon-reload 配置客户端 在 RustDesk 的客户端的 设置 -\u0026gt; 网络 -\u0026gt; ID/中继服务器 中：\n第一第二栏中打马赛克的地方填入服务器的 ip，由于实验室电脑有 IPv6，因此我这里使用的是 IPv6；key 栏填入默认安装目录下的 id_ed25519.pub 中的值。\n确认后，左下角显示 就绪 就说明配置完成了。\n在自己所需的电脑上都进行这一步的客户端配置即可开连！尝试了一下分辨率比 ToDesk 高多了，延迟也还好。\n","permalink":"https://kerolt.github.io/p/rustdesk%E8%87%AA%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8/","summary":"\u003cp\u003e对于现在常见的远程控制软件，例如 ToDesk、向日葵，其免费版本总有诸多限制（帧率低、分辨率低、控制时间有限制），而 RustDesk 的公共服务器现在在国内也不可用，因此我就想到了 RustDesk 的自建服务器。现在我们实验室有几台空闲的电脑，并且这些电脑都有 \u003cstrong\u003eIPv6\u003c/strong\u003e，这就省去了我购买云服务器的花费，可以实现零成本搭建远程服务😁\u003c/p\u003e","title":"Rustdesk自建服务器"},{"content":" 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 #include \u0026lt;iostream\u0026gt; auto ASSERT(bool flag, const char* msg) { if (!flag) { std::cerr \u0026lt;\u0026lt; msg \u0026lt;\u0026lt; std::endl; } } // READ: 枚举类型 \u0026lt;https://zh.cppreference.com/w/cpp/language/enum\u0026gt; // `enum` 是 C 的兼容类型，本质上其对应类型的常量。 // 在 `enum` 中定义标识符等价于定义 constexpr 常量， // 这些标识符不需要前缀，可以直接引用。 // 因此 `enum` 定义会污染命名空间。 enum ColorEnum : unsigned char { COLOR_RED = 31, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, }; // 有作用域枚举型是 C++ 引入的类型安全枚举。 // 其内部标识符需要带前缀引用，如 `Color::Red`。 // 作用域枚举型可以避免命名空间污染，并提供类型安全保证。 enum class Color : int { Red = COLOR_RED, Green, Yellow, Blue, }; ColorEnum convert_by_pun(Color c) { // READ: \u0026lt;https://zh.cppreference.com/w/cpp/language/union\u0026gt; // `union` 表示在同一内存位置存储的不同类型的值。 // 其常见用法是实现类型双关转换，即将一种类型的值转换为另一种无关类型的值。 // 但这种写法实际上仅在 C 语言良定义，在 C++ 中是未定义行为。 // 这是比较少见的 C++ 不与 C 保持兼容的特性。 // READ: 类型双关 \u0026lt;https://tttapa.github.io/Pages/Programming/Cpp/Practices/type-punning.html\u0026gt; union TypePun { ColorEnum e; Color c; }; TypePun pun; pun.c = c; return pun.e; } int main(int argc, char **argv) { ASSERT(convert_by_pun(Color::Red) == COLOR_RED, \u0026#34;Type punning conversion\u0026#34;); ASSERT(convert_by_pun(Color::Green) == COLOR_GREEN, \u0026#34;Type punning conversion\u0026#34;); ASSERT(convert_by_pun(Color::Yellow) == COLOR_YELLOW, \u0026#34;Type punning conversion\u0026#34;); ASSERT(convert_by_pun(Color::Blue) == COLOR_BLUE, \u0026#34;Type punning conversion\u0026#34;); return 0; } ","permalink":"https://kerolt.github.io/p/cc-%E7%B1%BB%E5%9E%8B%E5%8F%8C%E5%85%B3%E8%BD%AC%E6%8D%A2/","summary":"\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e50\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e51\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e52\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e53\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e54\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e55\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e56\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;iostream\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eauto\u003c/span\u003e \u003cspan class=\"nf\"\u003eASSERT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eflag\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eflag\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003ecerr\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003emsg\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eendl\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// READ: 枚举类型 \u0026lt;https://zh.cppreference.com/w/cpp/language/enum\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// `enum` 是 C 的兼容类型，本质上其对应类型的常量。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 在 `enum` 中定义标识符等价于定义 constexpr 常量，\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 这些标识符不需要前缀，可以直接引用。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 因此 `enum` 定义会污染命名空间。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eenum\u003c/span\u003e \u003cspan class=\"nc\"\u003eColorEnum\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCOLOR_RED\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e31\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCOLOR_GREEN\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCOLOR_YELLOW\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCOLOR_BLUE\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 有作用域枚举型是 C++ 引入的类型安全枚举。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 其内部标识符需要带前缀引用，如 `Color::Red`。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 作用域枚举型可以避免命名空间污染，并提供类型安全保证。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eenum\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eColor\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRed\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCOLOR_RED\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eGreen\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eYellow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eBlue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eColorEnum\u003c/span\u003e \u003cspan class=\"nf\"\u003econvert_by_pun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eColor\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// READ: \u0026lt;https://zh.cppreference.com/w/cpp/language/union\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// `union` 表示在同一内存位置存储的不同类型的值。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 其常见用法是实现类型双关转换，即将一种类型的值转换为另一种无关类型的值。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 但这种写法实际上仅在 C 语言良定义，在 C++ 中是未定义行为。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 这是比较少见的 C++ 不与 C 保持兼容的特性。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// READ: 类型双关 \u0026lt;https://tttapa.github.io/Pages/Programming/Cpp/Practices/type-punning.html\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eunion\u003c/span\u003e \u003cspan class=\"nc\"\u003eTypePun\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eColorEnum\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eColor\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eTypePun\u003c/span\u003e \u003cspan class=\"n\"\u003epun\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003epun\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003epun\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eargc\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e**\u003c/span\u003e\u003cspan class=\"n\"\u003eargv\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eASSERT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econvert_by_pun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eColor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eRed\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eCOLOR_RED\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Type punning conversion\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eASSERT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econvert_by_pun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eColor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eGreen\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eCOLOR_GREEN\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Type punning conversion\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eASSERT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econvert_by_pun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eColor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eYellow\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eCOLOR_YELLOW\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Type punning conversion\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eASSERT\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econvert_by_pun\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eColor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eBlue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eCOLOR_BLUE\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Type punning conversion\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"C\u0026C++类型双关转换"},{"content":"有时候我们会在多个平台上编辑文件、代码，此时我们希望所有文件，包括你在 Windows 上编辑的文件（Windows 采用的是 CRLF），都使用 LF 换行符，那么可以修改 .gitattributes 文件来强制执行这个规则：\n1 2 3 4 5 6 7 # 强制所有文件使用 LF 换行符 * text eol=lf # 避免对二进制文件进行换行符转换 *.png binary *.jpg binary *.pdf binary 在保存提交 .gitattributes 后，可能因为之前在 Windows 上编辑的文件可能已经变成了 CRLF 换行符，这时需要需要将它们转换回 LF（这里可以使用 VSCode）。\n接着可以执行以下命令，让 Git 重新处理工作目录中的文件并按照新的规则来转换：\n1 2 git rm --cached -r . git reset --hard * text eol=lf：这条规则告诉 Git，将仓库中的所有文件都视为文本文件，并且强制它们使用 LF 作为行结束符。当你从仓库检出文件时，Git 会将其转换为 CRLF（在 Windows 上），但当你提交时，它会确保所有文件都以 LF 格式存储在仓库中。\ngit rm --cached -r . 和 git reset --hard：这些命令会强制 Git 重新将你的工作目录与仓库同步，并在此过程中应用 .gitattributes 文件中设置的换行符规则，从而确保所有文件都符合 LF 规范。\n","permalink":"https://kerolt.github.io/p/git%E4%B8%AD%E6%96%87%E4%BB%B6%E6%8D%A2%E8%A1%8C%E7%AC%A6%E9%83%BD%E4%BD%BF%E7%94%A8lf/","summary":"\u003cp\u003e有时候我们会在多个平台上编辑文件、代码，此时我们希望所有文件，包括你在 Windows 上编辑的文件（Windows 采用的是 CRLF），都使用 LF 换行符，那么可以修改 \u003ccode\u003e.gitattributes\u003c/code\u003e 文件来强制执行这个规则：\u003c/p\u003e","title":"Git中文件换行符都使用LF"},{"content":"什么是内存对齐？为什么需要它？ 内存对齐（Memory Alignment）是计算机系统中数据在内存中存储的一种规则：​​数据在内存中的起始地址必须是其自身大小的整数倍​​。例如，一个 4 字节的整型变量（int），其起始地址必须是 4 的倍数（如地址 0x0000、0x0004、0x0008 等）。\n而需要内存对齐主要基于以下三个原因：\n​​硬件访问效率​​： CPU 通过内存总线从内存读取数据时，通常以固定大小的“块”为单位（例如 4 字节或 8 字节）。如果数据对齐，CPU 一次读取操作即可获取完整数据。 ​​非对齐示例​​：假设一个 int 变量（4 字节）存储在地址 0x0001（非 4 的倍数），CPU 需要分两次读取：先读取 0x0000-0x0003（包含前 3 字节），再读取 0x0004-0x0007（包含最后 1 字节），最后拼接数据。这会显著降低性能。 ​​硬件兼容性​​： 部分架构（如 ARM、MIPS）的 CPU 无法直接访问非对齐内存。尝试访问时会导致硬件异常（如“总线错误”）。对齐保证了代码的跨平台兼容性。 ​​缓存效率优化​​： 现代 CPU 使用缓存行（Cache Line，通常 64 字节）预加载数据。对齐的数据更可能完整地位于单个缓存行中。若数据跨缓存行存储，会引发两次缓存访问，降低效率。 alignas 和 alignof c++11 以后引入两个关键字 alignas 与 alignof。\nalignas 用于显式设置变量、类成员或类型的内存对齐要求；而 alignof 用于获取类型或变量的内存对齐要求。例如：\n1 2 3 4 5 6 7 8 struct Test1 {}; struct alignas(4) Test2 {}; static_assert(sizeof(Test1) == 1); static_assert(sizeof(Test2) == 4); static_assert(alignof(Test1) == 1); static_assert(alignof(Test2) == 4); alignas 支持三种语法形式：\nalignas(expression)：expression 必须是计算结果为零的整数常量表达式，或者是对齐或扩展对齐的有效值（2 的倍数）。 alignas(type-id)：等效于 alignas(alignof(type-id)) alignas(pack...)：等效于应用于同一声明的多个 alignas 说明符，参数包的每个成员对应一个说明符，可以是类型参数包或常量参数包 [!NOTE] 注意：若指定的对齐值小于编译器默认对齐要求，部分编译器可能忽略该设置。\n结构体字节对齐 结构体对齐的基本原则如下：\n每个成员在其自身大小的整数倍地址上对齐 结构体本身的对齐方式等于其最大成员的对齐要求 结构体的大小是其对齐方式的整数倍（尾部可能填充） 举个🌰：\n1 2 3 4 5 6 7 8 struct Student { char name[5]; int num; short score; }; cout \u0026lt;\u0026lt; sizeof(Student) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 16 cout \u0026lt;\u0026lt; alignof(Student); // 4 成员 大小 对齐要求（alignment） char[5] 5 字节 1 字节 int 4 字节 4 字节 short 2 字节 2 字节 结构体整体对齐通常等于 最大对齐成员的对齐要求 → max(1, 4, 2) = 4 字节。\n其内存布局如下：\n1 2 3 4 5 Byte Offset: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field: | n0| n1| n2| n3| n4| | | | num (4 bytes) | s0| s1| | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Meaning: | name[5] | padding | num | score |padding| 如果采用 __attribute__((__packed__)) （GCC/Clang 编译器提供的一个属性）进行“单字节对齐”，则会去除所有自动添加的 padding，每个成员从它前一个成员紧挨着的下一个字节开始。\n1 2 3 4 5 6 7 8 struct Student { char name[5]; int num; short score; } __attribute__((__packed__)); cout \u0026lt;\u0026lt; sizeof(Student) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 11 cout \u0026lt;\u0026lt; alignof(Student); // 1 1 2 3 4 5 Byte Offset: 0 1 2 3 4 5 6 7 8 9 10 +---+---+---+---+---+---+---+---+---+---+---+ Field: | n0| n1| n2| n3| n4| m0| m1| m2| m3| s0| s1| +---+---+---+---+---+---+---+---+---+---+---+ Meaning: | name[5] | num | score | 这种情况通常用于网络协议头、二进制文件结构、设备寄存器映射等。\n位域 “位域“是把一个字节中的二进位划分为几个不同的区域，并说明每个区域的位数，每个域有一个域名，允许在程序中按位域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。\n位域的对齐规则为：\n位域的基础类型决定了分配单位，比如 unsigned int : 3 表示分配在 int 的机器字上（通常是 4 字节） 多个位域会尽量共享同一个机器字，但： 如果一个字段放不下了，会拆到下一个对齐单元 不同基础类型之间可能强制对齐 结构体大小和对齐也会根据最大基础类型来对齐 对于下面这个🌰：\n1 2 3 4 5 6 7 8 9 10 11 struct BitField { int a1 : 1; int a2 : 5; int a3 : 29; int a4 : 6; char a5 : 2; char a6; }; cout \u0026lt;\u0026lt; sizeof(BitField) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 12 cout \u0026lt;\u0026lt; alignof(BitField) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 4 所有 int 位域（a1 ~ a4）以 int（4 字节 = 32 位）为分配单元。 多个 int 位域可以共用同一个 int 单元，如果剩余位数够用。 一旦放不下一个字段，就会换下一个 4 字节单元。 如果换了基础类型（比如从 int → char），通常会进行类型对齐。 非位域成员如 char a6 仍需按其类型对齐（通常是 1 字节，但结构体对齐会以最大成员为准）。 其内存布局如下：\n1 2 3 4 Byte Offset: 0 1 2 3 4 5 6 7 8 9 10 11 +---------------------------------------------------------------------------------------+ | a1:1, a2:5, a3前26位，共32位 | a3后3位, a4:6, 剩余未用 bits |a5:2...| a6:8 | pad | +---------------------------------------------------------------------------------------+ ","permalink":"https://kerolt.github.io/p/%E5%85%B3%E4%BA%8E%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90%E4%BD%8D%E5%9F%9F%E7%9A%84%E6%80%9D%E8%80%83/","summary":"\u003ch2 id=\"什么是内存对齐为什么需要它\"\u003e什么是内存对齐？为什么需要它？\u003c/h2\u003e\n\u003cp\u003e内存对齐（Memory Alignment）是计算机系统中数据在内存中存储的一种规则：​\u003cstrong\u003e​数据在内存中的起始地址必须是其自身大小的整数倍​\u003c/strong\u003e​。例如，一个 4 字节的整型变量（\u003ccode\u003eint\u003c/code\u003e），其起始地址必须是 4 的倍数（如地址 0x0000、0x0004、0x0008 等）。\u003c/p\u003e","title":"关于内存对齐、位域的思考"},{"content":"__uint, __type, __array, __ulong 这些宏主要用于 eBPF Map 的定义 中，帮助 LLVM 编译器和 BPF CO-RE (Compile Once – Run Everywhere) 理解和提取 eBPF Map 的元数据信息。它们通常与 BPF_MAP_DEF 或 struct 配合使用，在 eBPF 程序中用于告诉内核如何创建 BPF map。\n常用的几个宏 1. __uint(name, val) 1 #define __uint(name, val) int (*name)[val] 用途：定义一个名为 name 的字段，其值是一个大小为 val 的整数数组指针。\n1 __uint(max_entries, 1024); 等价于：\n1 int (*max_entries)[1024]; 作用：\n用于在 eBPF Map 的定义中指定 max_entries 这一属性（即 map 的最大容量）。这种“类型欺骗”的方式让 clang 编译器能保留这些信息在 ELF 文件的 BTF（BPF Type Format）部分里，从而被 bpftool、libbpf 提取并传给内核。\n2. __type(name, val) 1 #define __type(name, val) typeof(val) *name 用途：定义一个名为 name 的字段，其类型是 val 的指针。\n1 2 __type(key, u32); __type(value, struct my_val); 等价于：\n1 2 u32 *key; struct my_val *value; 作用：\n告诉内核 key 和 value 类型分别是什么。这对于 map 创建时至关重要，编译器生成的 BTF 信息里会包含 key/value 类型。\n3. __array(name, val) 1 #define __array(name, val) typeof(val) *name[] 用途：定义一个名为 name 的数组指针，元素类型是 val。\n用于某些 map 类型，比如 BPF_MAP_TYPE_PROG_ARRAY，你可能会写：\n1 __array(values, int); 等价于：\n1 int *values[]; 作用：\n表示 map 的值是一个函数指针数组或类似结构。这个字段用于描述 array-of-pointer 类型的结构（比如跳转表）。\n4. __ulong(name, val) 1 #define __ulong(name, val) enum { ___bpf_concat(__unique_value, __COUNTER__) = val } name 这个稍微复杂，它依赖另一个宏 ___bpf_concat（通常是这样定义的）：\n1 #define ___bpf_concat(a, b) a##b 因此：\n1 __ulong(pinning, 2); 会展开成：\n1 enum { __unique_value123 = 2 } pinning; 作用：\n在不引入实际变量的前提下，让这个 pinning 名字能被识别为枚举类型，其值为 2。 用于设置 map 的属性值，比如 BPF_F_PIN_BY_NAME 之类的 enum 值。 __COUNTER__ 保证名字唯一，防止 enum 名冲突。 这些宏的目的 宏名 用途 实际展开 用于定义 eBPF Map 的哪些属性 __uint 设置整型常量字段 int (*name)[val] max_entries, map_flags, type 等 __type 设置类型信息 typeof(val) *name key, value 等 __array 表示数组类型 typeof(val) *name[] prog_array 类型的值等 __ulong 设置枚举值但不创建变量 enum { unique_name = val } pinning, BPF_F_... 选项等 🌰 定义一个简单的 Hash Map 1 2 3 4 5 6 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, u32); __type(value, long); } my_map SEC(\u0026#34;.maps\u0026#34;); my_map 在编译时不会真正分配变量空间，而是借助宏保留元信息给 BTF section，供 libbpf 使用自动加载、创建 map 时提取。\n为什么这些宏要被定义为数组指针形式 eBPF 程序中的这些 __uint、__type 定义，本质上是为了将配置元信息嵌入到 BTF (BPF Type Format) 中。\n[!NOTE]\n也就是说：我们不是在运行时使用这些变量，而是“伪装”为有值的变量，利用它们的类型和结构，在编译阶段生成元数据，供 eBPF 加载器使用。\n比如：\n1 __uint(max_entries, 1024); 展开后：\n1 int (*max_entries)[1024]; 这并不是真的声明了一个数组，也不会访问它，它只是声明了一个指针，其指向的类型是长度为 1024 的数组，这一信息正好可以被 BTF 捕获并保留。\n利用这一点，可以让内核从 BTF 中知道：\nmax_entries 是一个整数类型 值为 1024 那为什么不直接定义一个整型常量呢？因为 C 语言中：\n1 int x = 123; 这种方式会在程序中实际生成一个变量 x 占用空间。eBPF 是一种非常严格的环境，要求尽可能小的指令数和变量空间，而且 map 元数据本质上并不需要运行时访问。\n所以我们想让这些“变量”：\n不真的存在于运行时（不分配空间） 但它们的“类型信息”和“值信息”能被保留下来 而使用数组指针这种写法的优点是：\n不会生成实际变量或初始化语句（编译器优化掉） val（如 1024）被嵌入到了类型中，方便 BTF 保留 能被 clang/libbpf 识别为元信息字段 可以区分不同的字段名（因为是结构体成员） ","permalink":"https://kerolt.github.io/p/ebpf-map%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E6%88%90%E5%91%98%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%AE%8F%E5%AE%9A%E4%B9%89/","summary":"\u003cp\u003e\u003ccode\u003e__uint\u003c/code\u003e, \u003ccode\u003e__type\u003c/code\u003e, \u003ccode\u003e__array\u003c/code\u003e, \u003ccode\u003e__ulong\u003c/code\u003e 这些宏主要用于 \u003cstrong\u003eeBPF Map 的定义\u003c/strong\u003e 中，帮助 LLVM 编译器和 BPF CO-RE (Compile Once – Run Everywhere) 理解和提取 eBPF Map 的元数据信息。它们通常与 \u003ccode\u003eBPF_MAP_DEF\u003c/code\u003e 或 \u003ccode\u003estruct\u003c/code\u003e 配合使用，在 eBPF 程序中用于告诉内核如何创建 BPF map。\u003c/p\u003e","title":"eBPF Map中的数据成员类型的宏定义"},{"content":"通过解耦编译时依赖和运行时重定位，实现 BPF 程序对内核版本与配置差异的自动适配。vmlinux.h 之所以无需严格匹配你的内核配置或版本，关键在于以下机制：\nBTF（BPF Type Format）提供统一类型描述 vmlinux.h 本质是内核 BTF 的 C 语言翻译：它由 bpftool btf dump file /sys/kernel/btf/vmlinux format c \u0026gt; vmlinux.h 命令生成，完整包含内核所有数据结构的类型定义（如结构体、枚举）。 独立于具体配置：BTF 记录了内核的最终内存布局（包括因配置选项如 CONFIG_COMPAT 导致的字段偏移变化）。因此，编译时使用的 vmlinux.h 只需是某一有效内核的 BTF 快照，无需与目标内核完全一致。 生成 vmlinux.h 1 bpftool btf dump file /sys/kernel/btf/vmlinux format c \u0026gt; vmlinux.h Clang 编译时记录“重定位元信息” 当使用 Clang（≥10）编译 BPF 程序时，对内核结构的访问（如 task_struct-\u0026gt;pid）会被转换为 CO-RE 重定位项。这些项记录： 访问的字段名（如 pid） 类型路径（如 struct task_struct） 预期偏移/大小等。 此过程依赖 vmlinux.h 提供类型合法性检查，但不绑定具体偏移。 libbpf 运行时动态重定位 加载 BPF 程序时，libbpf 执行关键两步： 查询目标内核的 BTF（/sys/kernel/btf/vmlinux），获取实际结构布局； 比对编译时的重定位项，修正字段偏移、处理字段增减（如通过 bpf_core_field_exists() 判断成员是否存在）。 例如：若目标内核的 struct user_arg_ptr 因 CONFIG_COMPAT=y 新增 is_compat 字段，libbpf 会自动调整 native 字段的访问偏移。 CO-RE 辅助函数实现条件兼容 BPF 代码可通过以下函数动态适应内核差异： bpf_core_read(dst, src)：替代直接指针访问，按运行时偏移安全读取字段； BPF_CORE_READ(src, field)：处理嵌套结构； bpf_core_field_exists(field)：条件执行兼容逻辑（如选择参数索引）。 这些函数在编译时生成重定位项，在运行时由 libbpf 解析为正确操作。 实际限制与注意事项 限制 说明 目标内核需开启 BTF 必须配置 CONFIG_DEBUG_INFO_BTF=y（多数发行版已默认开启）。 旧内核无 BTF 支持 若无 BTF，需手动提供等效的 BTF 文件（如通过 BTFHub）。 宏与函数签名变化 BTF 不记录宏或函数参数列表变化，需通过 #ifdef 或运行时检测处理。 BPF CO-RE 将类型描述（vmlinux.h）、编译时元信息（Clang 重定位项）、运行时适配器（libbpf）三者分离，使 vmlinux.h 仅需作为“合法类型模板”，而非精确内存布局参考。最终由 libbpf 借助目标内核的 BTF 完成字段偏移、存在性的动态修正，实现跨版本/配置的运行兼容。这一机制大幅降低了 eBPF 程序的移植成本，使其真正成为“一次编译，到处运行”的内核可编程工具。\n","permalink":"https://kerolt.github.io/p/co-re%E6%9C%BA%E5%88%B6%E4%B8%8Evmlinux.h/","summary":"\u003cp\u003e通过解耦编译时依赖和运行时重定位，实现 BPF 程序对内核版本与配置差异的自动适配。vmlinux.h 之所以无需严格匹配你的内核配置或版本，关键在于以下机制：\u003c/p\u003e","title":"CO-RE机制与vmlinux.h"},{"content":"curl 是一个强大的命令行工具，用于传输数据，支持多种协议（HTTP、HTTPS、FTP 等）。以下是 curl 的基本使用方法和常见示例。\n基本语法 curl [options] [URL...] 常用选项 选项 描述 -X 指定 HTTP 请求方法 (GET, POST, PUT, DELETE 等) -H 添加请求头 -d 发送 POST 请求数据 -F 发送表单数据 (multipart/form-data) -G 将 -d 数据作为 GET 请求的查询参数 -o 将输出保存到文件 -O 将输出保存到文件，使用远程文件名 -L 跟随重定向 -v 显示详细输出 (verbose) -u 指定用户名和密码 -A 设置 User-Agent -k 允许不安全的 SSL 连接 -I 只获取响应头 -s 静默模式 (不显示进度或错误信息) --data-urlencode URL 编码 POST 数据 常见用法示例 1. 发送 GET 请求 curl https://example.com 2. 发送 POST 请求 curl -X POST https://example.com/api -d \u0026#39;name=value\u0026#39; 3. 发送 JSON 数据 curl -X POST https://example.com/api \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;key1\u0026#34;:\u0026#34;value1\u0026#34;, \u0026#34;key2\u0026#34;:\u0026#34;value2\u0026#34;}\u0026#39; 4. 发送表单数据 curl -X POST https://example.com/form \\ -d \u0026#39;username=admin\u0026#39; \\ -d \u0026#39;password=123456\u0026#39; 5. 上传文件 curl -X POST https://example.com/upload \\ -F \u0026#34;file=@/path/to/file.txt\u0026#34; 6. 设置请求头 curl -H \u0026#34;Authorization: Bearer token\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ https://example.com/api 7. 下载文件 curl -O https://example.com/file.zip 8. 跟随重定向 curl -L https://example.com/redirect 9. 使用基本认证 curl -u username:password https://example.com 10. 保存 cookie 并发送 # 保存 cookie curl -c cookies.txt https://example.com/login -d \u0026#34;user=name\u0026amp;pass=123\u0026#34; # 使用 cookie curl -b cookies.txt https://example.com/dashboard 11. 测试 API 响应时间 curl -o /dev/null -s -w \u0026#39;Total: %{time_total}s\\n\u0026#39; https://example.com 12. 限制下载速度 curl --limit-rate 100K -O https://example.com/largefile.zip 高级用法 发送多部分请求 curl -X POST https://example.com/upload \\ -F \u0026#34;file=@image.jpg\u0026#34; \\ -F \u0026#34;description=My image\u0026#34; 使用代理 curl -x http://proxy.example.com:8080 https://example.com 调试请求 curl -v https://example.com 只获取响应头 curl -I https://example.com 使用自定义 User-Agent curl -A \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64)\u0026#34; https://example.com 注意事项 在脚本中使用 curl 时，考虑添加 -s 或 -sS 选项（-S 显示错误） 处理 JSON 数据时，可以使用 jq 工具进行格式化 对于复杂的 API 调用，考虑将请求保存为文件并使用 -K 选项 在 Windows 上，使用双引号 \u0026quot; 而不是单引号 ' ","permalink":"https://kerolt.github.io/p/curl%E4%BD%BF%E7%94%A8/","summary":"\u003cp\u003ecurl 是一个强大的命令行工具，用于传输数据，支持多种协议（HTTP、HTTPS、FTP 等）。以下是 curl 的基本使用方法和常见示例。\u003c/p\u003e\n\u003ch2 id=\"基本语法\"\u003e基本语法\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl [options] [URL...]\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"常用选项\"\u003e常用选项\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e选项\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e描述\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-X\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e指定 HTTP 请求方法 (GET, POST, PUT, DELETE 等)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-H\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e添加请求头\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-d\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e发送 POST 请求数据\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-F\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e发送表单数据 (multipart/form-data)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-G\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e将 -d 数据作为 GET 请求的查询参数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e将输出保存到文件\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-O\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e将输出保存到文件，使用远程文件名\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-L\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e跟随重定向\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-v\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e显示详细输出 (verbose)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-u\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e指定用户名和密码\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-A\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e设置 User-Agent\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-k\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e允许不安全的 SSL 连接\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-I\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e只获取响应头\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e-s\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e静默模式 (不显示进度或错误信息)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e--data-urlencode\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eURL 编码 POST 数据\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"常见用法示例\"\u003e常见用法示例\u003c/h2\u003e\n\u003ch3 id=\"1-发送-get-请求\"\u003e1. 发送 GET 请求\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2-发送-post-请求\"\u003e2. 发送 POST 请求\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST https://example.com/api -d \u0026#39;name=value\u0026#39;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"3-发送-json-数据\"\u003e3. 发送 JSON 数据\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST https://example.com/api \\\n  -H \u0026#34;Content-Type: application/json\u0026#34; \\\n  -d \u0026#39;{\u0026#34;key1\u0026#34;:\u0026#34;value1\u0026#34;, \u0026#34;key2\u0026#34;:\u0026#34;value2\u0026#34;}\u0026#39;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"4-发送表单数据\"\u003e4. 发送表单数据\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST https://example.com/form \\\n  -d \u0026#39;username=admin\u0026#39; \\\n  -d \u0026#39;password=123456\u0026#39;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"5-上传文件\"\u003e5. 上传文件\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST https://example.com/upload \\\n  -F \u0026#34;file=@/path/to/file.txt\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"6-设置请求头\"\u003e6. 设置请求头\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -H \u0026#34;Authorization: Bearer token\u0026#34; \\\n  -H \u0026#34;Content-Type: application/json\u0026#34; \\\n  https://example.com/api\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"7-下载文件\"\u003e7. 下载文件\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -O https://example.com/file.zip\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"8-跟随重定向\"\u003e8. 跟随重定向\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -L https://example.com/redirect\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"9-使用基本认证\"\u003e9. 使用基本认证\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -u username:password https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"10-保存-cookie-并发送\"\u003e10. 保存 cookie 并发送\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 保存 cookie\ncurl -c cookies.txt https://example.com/login -d \u0026#34;user=name\u0026amp;pass=123\u0026#34;\n\n# 使用 cookie\ncurl -b cookies.txt https://example.com/dashboard\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"11-测试-api-响应时间\"\u003e11. 测试 API 响应时间\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -o /dev/null -s -w \u0026#39;Total: %{time_total}s\\n\u0026#39; https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"12-限制下载速度\"\u003e12. 限制下载速度\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl --limit-rate 100K -O https://example.com/largefile.zip\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"高级用法\"\u003e高级用法\u003c/h2\u003e\n\u003ch3 id=\"发送多部分请求\"\u003e发送多部分请求\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -X POST https://example.com/upload \\\n  -F \u0026#34;file=@image.jpg\u0026#34; \\\n  -F \u0026#34;description=My image\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"使用代理\"\u003e使用代理\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -x http://proxy.example.com:8080 https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"调试请求\"\u003e调试请求\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -v https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"只获取响应头\"\u003e只获取响应头\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -I https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"使用自定义-user-agent\"\u003e使用自定义 User-Agent\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ecurl -A \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64)\u0026#34; https://example.com\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"注意事项\"\u003e注意事项\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e在脚本中使用 curl 时，考虑添加 \u003ccode\u003e-s\u003c/code\u003e 或 \u003ccode\u003e-sS\u003c/code\u003e 选项（-S 显示错误）\u003c/li\u003e\n\u003cli\u003e处理 JSON 数据时，可以使用 \u003ccode\u003ejq\u003c/code\u003e 工具进行格式化\u003c/li\u003e\n\u003cli\u003e对于复杂的 API 调用，考虑将请求保存为文件并使用 \u003ccode\u003e-K\u003c/code\u003e 选项\u003c/li\u003e\n\u003cli\u003e在 Windows 上，使用双引号 \u003ccode\u003e\u0026quot;\u003c/code\u003e 而不是单引号 \u003ccode\u003e'\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e","title":"Curl使用"},{"content":" 本文是对 cilium/ebpf examples: fentry 的一个学习记录\n在这里，我们将会利用 ebpf 监控并记录系统上所有新发起的 IPv4 TCP 连接，其工作流程如下：\n挂载点：程序使用 fentry 机制把自己附加到内核函数 tcp_connect 的入口。每当系统中有任何一个进程尝试发起一个 TCP 连接时，这个内核函数就会被调用，从而触发我们的 eBPF 程序。 过滤：程序首先检查连接的地址族是否为 AF_INET，即 IPv4。如果不是（例如是 IPv6），程序会直接退出，不做任何处理。 数据提取：对于 IPv4 连接，程序会从传递给 tcp_connect 函数的 struct sock 参数中提取以下关键信息： 源 IP 地址 (saddr) 目标 IP 地址 (daddr) 目标端口 (dport) 源端口 (sport) 获取进程信息：使用 bpf_get_current_comm() 辅助函数获取当前发起连接的进程名（例如 curl, ssh 等）。 数据发送：程序将收集到的所有信息（IP 地址、端口、进程名）打包成一个 struct event 结构体，并通过一个高效的 ringbuf 映射发送到用户空间。 什么是 fentry fentry 是 eBPF 中的一种程序附加类型，全称为 \u0026ldquo;function entry\u0026rdquo;（函数入口）。它是现代 Linux 内核中用于跟踪和性能分析的高效机制。\nfentry 允许将 eBPF 程序附加到内核函数的入口点，当该函数被调用时，eBPF 程序会在函数的主体执行前运行。这种机制让我们可以：\n拦截函数调用 检查函数参数 收集统计数据 修改执行路径 为什么使用 fentry 使用 fentry（函数入口钩子）来跟踪 TCP 连接，相比于 kprobe 或其他方式，主要有以下几个优点：\n性能更高：fentry 是 BPF 的原生钩子，直接插入到内核函数入口，开销比 kprobe 更低，延迟更小，适合高频事件的跟踪。 更安全稳定：fentry 直接集成在内核 BPF 框架中，API 更加稳定，不容易因为内核升级或符号变化而失效。而 kprobe 依赖于符号解析，容易受到内核实现细节变化影响。 类型安全：fentry 支持 BTF（BPF Type Format），可以直接访问函数参数，类型安全且易于开发。而 kprobe 只能通过寄存器或栈手动解析参数，容易出错。 更好的可维护性：fentry 程序更容易与内核源码保持同步，代码更简洁，维护成本更低。 而 kprobe 适合没有 fentry 支持的老内核或特殊场景。\n内核态 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 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 //go:build ignore #include \u0026#34;../vmlinux/vmlinux.h\u0026#34; #include \u0026lt;bpf/bpf_endian.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; #include \u0026lt;bpf/bpf_tracing.h\u0026gt; #define AF_INET 2 #define TASK_COMM_LEN 16 char __license[] SEC(\u0026#34;license\u0026#34;) = \u0026#34;Dual MIT/GPL\u0026#34;; // struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 \u0026lt;\u0026lt; 24); __type(value, struct event); } events SEC(\u0026#34;.maps\u0026#34;); // 用于捕获和传递 TCP 连接事件的关键信息，通常在 eBPF 程序监控网络活动时使用。 // 当程序检测到新的 TCP连接时，会将连接的详细信息打包到这个结构体中，然后发送给用户空间程序进行处理。 struct event { u8 comm[16]; // 进程名称（最多15个字符 + null终止符） __u16 sport; // 源端口（主机字节序） __be16 dport; // 目标端口（网络字节序，big-endian） __be32 saddr; // 源IP地址（网络字节序，big-endian） __be32 daddr; // 目标IP地址（网络字节序，big-endian） }; SEC(\u0026#34;fentry/tcp_connect\u0026#34;) int BPF_PROG(tcp_connect, struct sock* sk) { if (sk-\u0026gt;__sk_common.skc_family != AF_INET) { return 0; } struct event* tcp_info; tcp_info = bpf_ringbuf_reserve(\u0026amp;events, sizeof(struct event), 0); if (!tcp_info) { return 0; } tcp_info-\u0026gt;saddr = sk-\u0026gt;__sk_common.skc_rcv_saddr; tcp_info-\u0026gt;daddr = sk-\u0026gt;__sk_common.skc_daddr; tcp_info-\u0026gt;dport = sk-\u0026gt;__sk_common.skc_dport; tcp_info-\u0026gt;sport = bpf_htons(sk-\u0026gt;__sk_common.skc_num); bpf_get_current_comm(\u0026amp;tcp_info-\u0026gt;comm, TASK_COMM_LEN); bpf_ringbuf_submit(tcp_info, 0); return 0; } 使用到的 bpf helper function bpf_ringbuf_reserve() 作用：从环形缓冲区（ring buffer）中预留一块内存空间\n参数 1：\u0026amp;events - 环形缓冲区的引用 参数 2：sizeof(struct event) - 需要预留的字节数 参数 3：0 - 标志位（通常为 0） 返回值：指向预留内存的指针，失败时返回 NULL 关键点：这是一个 \u0026quot; 预留 - 提交 \u0026quot; 模式的第一步，先申请空间但数据还未对用户空间可见。\nbpf_htons() 作用：将主机字节序转换为网络字节序（Host TO Network Short）。网络协议使用大端序，而主机可能使用小端序，需要转换保证数据一致性。\n参数：16 位的端口号 返回值：网络字节序的端口号 bpf_get_current_comm() 作用：获取当前进程的命令名称（进程名）\n参数 1：\u0026amp;tcp_info-\u0026gt;comm - 存储进程名的缓冲区指针 参数 2：TASK_COMM_LEN - 缓冲区大小（通常是 16 字节） bpf_ringbuf_submit() 作用：将之前预留的数据提交到环形缓冲区，使其对用户空间可见\n参数 1：tcp_info - 之前通过 bpf_ringbuf_reserve() 获得的指针 参数 2：0 - 标志位 用户态 Go 程序 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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;encoding/binary\u0026#34; \u0026#34;errors\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;github.com/cilium/ebpf/link\u0026#34; \u0026#34;github.com/cilium/ebpf/ringbuf\u0026#34; \u0026#34;github.com/cilium/ebpf/rlimit\u0026#34; ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags \u0026#34;-O2 -g -Wall -Werror -D__TARGET_ARCH_x86\u0026#34; bpf fentry_tcp_connect.bpf.c func main() { stopper := make(chan os.Signal, 1) signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) // 移除内存限制 if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } objs := bpfObjects{} if err := loadBpfObjects(\u0026amp;objs, nil); err != nil { log.Fatalf(\u0026#34;loading bpf objects: %v\u0026#34;, err) } defer objs.Close() link, err := link.AttachTracing(link.TracingOptions{ Program: objs.TcpConnect, }) if err != nil { log.Fatalf(\u0026#34;linking Fentry: %v\u0026#34;, err) } defer link.Close() rd, err := ringbuf.NewReader(objs.Events) if err != nil { log.Fatalf(\u0026#34;opening ringbuf reader: %s\u0026#34;, err) } defer rd.Close() go func() { \u0026lt;-stopper if err := rd.Close(); err != nil { log.Fatalf(\u0026#34;closing ringbuf reader: %s\u0026#34;, err) } }() log.Printf(\u0026#34;%-16s %-15s %-6s -\u0026gt; %-15s %-6s\u0026#34;, \u0026#34;Comm\u0026#34;, \u0026#34;Src addr\u0026#34;, \u0026#34;Port\u0026#34;, \u0026#34;Dest addr\u0026#34;, \u0026#34;Port\u0026#34;, ) // 不断从 ringbuf 中读取事件、解析事件并打印相关信息，直到接收到终止信号 var event bpfEvent for { record, err := rd.Read() if err != nil { if errors.Is(err, ringbuf.ErrClosed) { log.Println(\u0026#34;received signal, exiting..\u0026#34;) return } log.Printf(\u0026#34;reading from reader: %s\u0026#34;, err) continue } // Parse the ringbuf event entry into a bpfEvent structure. if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.BigEndian, \u0026amp;event); err != nil { log.Printf(\u0026#34;parsing ringbuf event: %s\u0026#34;, err) continue } log.Printf(\u0026#34;%-16s %-15s %-6d -\u0026gt; %-15s %-6d\u0026#34;, event.Comm, intToIP(event.Saddr), event.Sport, intToIP(event.Daddr), event.Dport, ) } } // intToIP converts IPv4 number to net.IP func intToIP(ipNum uint32) net.IP { ip := make(net.IP, 4) binary.BigEndian.PutUint32(ip, ipNum) return ip } ","permalink":"https://kerolt.github.io/p/ebpf%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8fentry%E8%B7%9F%E8%B8%AAtcp%E8%BF%9E%E6%8E%A5/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文是对 \u003ca href=\"https://github.com/cilium/ebpf/tree/main/examples/fentry\"\u003ecilium/ebpf examples: fentry \u003c/a\u003e的一个学习记录\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在这里，我们将会利用 ebpf 监控并记录系统上所有新发起的 IPv4 TCP 连接，其工作流程如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e挂载点\u003c/strong\u003e：程序使用 \u003ccode\u003efentry\u003c/code\u003e 机制把自己附加到内核函数 \u003ccode\u003etcp_connect\u003c/code\u003e 的入口。每当系统中有任何一个进程尝试发起一个 TCP 连接时，这个内核函数就会被调用，从而触发我们的 eBPF 程序。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e过滤\u003c/strong\u003e：程序首先检查连接的地址族是否为 \u003ccode\u003eAF_INET\u003c/code\u003e，即 IPv4。如果不是（例如是 IPv6），程序会直接退出，不做任何处理。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据提取\u003c/strong\u003e：对于 IPv4 连接，程序会从传递给 \u003ccode\u003etcp_connect\u003c/code\u003e 函数的 \u003ccode\u003estruct sock\u003c/code\u003e 参数中提取以下关键信息：\n\u003cul\u003e\n\u003cli\u003e源 IP 地址 (\u003ccode\u003esaddr\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003e目标 IP 地址 (\u003ccode\u003edaddr\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003e目标端口 (\u003ccode\u003edport\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003e源端口 (\u003ccode\u003esport\u003c/code\u003e)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e获取进程信息\u003c/strong\u003e：使用 \u003ccode\u003ebpf_get_current_comm()\u003c/code\u003e 辅助函数获取当前发起连接的进程名（例如 \u003ccode\u003ecurl\u003c/code\u003e, \u003ccode\u003essh\u003c/code\u003e 等）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据发送\u003c/strong\u003e：程序将收集到的所有信息（IP 地址、端口、进程名）打包成一个 \u003ccode\u003estruct event\u003c/code\u003e 结构体，并通过一个高效的 \u003ccode\u003eringbuf\u003c/code\u003e 映射发送到用户空间。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"什么是-fentry\"\u003e什么是 fentry\u003c/h2\u003e\n\u003cp\u003efentry 是 eBPF 中的一种程序附加类型，全称为 \u0026ldquo;function entry\u0026rdquo;（函数入口）。它是现代 Linux 内核中用于跟踪和性能分析的高效机制。\u003c/p\u003e","title":"【eBPF学习】使用fentry跟踪tcp连接"},{"content":"透明运算符的概念与价值 在 C++ 编程中，当编写泛型代码时，不同类型的比较或操作可能导致意外的类型转换或精度损失。假设有一个 std::vector\u0026lt;uint32_t\u0026gt;，使用自定义仿函数进行排序，一切运行正常。但当你将容器改为 std::vector\u0026lt;uint64_t\u0026gt; 却忘记修改仿函数的实现时，编译器不会报错，但数据可能在比较前被静默截断，导致排序结果完全错误。而 C++14 引入透明运算符可以帮助我们避免这种 bug。\n透明运算符（Transparent Operator）是 C++14 引入的一项强大特性，它通过 std::less\u0026lt;\u0026gt;、std::greater\u0026lt;\u0026gt; 等空模板参数的运算符函子实现，允许编译器在模板实例化时自动推导操作数的实际类型，从而避免不必要的类型转换和潜在错误。与传统的重载运算符不同，透明运算符的核心优势在于其类型透明性——它们不会强迫操作数转换为特定类型，而是根据操作数的实际类型进行推导，保留完整的类型信息。\n传统 C++ 运算符重载需要严格定义操作数类型，这使得编写真正通用的泛型代码变得困难。例如，当我们使用 std::less\u0026lt;int\u0026gt; 进行比较时，它会强制将两个操作数都视为 int 类型，如果操作数实际是 long 或 double，就可能发生精度损失或意外的类型转换。而透明运算符如 std::less\u0026lt;\u0026gt; 则解决了这一问题，它本质上是一个模板化的函子，能够自动适应操作数的类型，保持代码的通用性和安全性。\n实现原理 透明运算符的神奇之处在于其简洁而精妙的实现机制。让我们深入探究其工作原理，揭开这层看似简单的语法糖衣下蕴含的强大能力。\n模板元编程技巧 透明运算符的核心实现依赖于空模板参数列表（operator\u0026lt;\u0026gt;）这一巧妙设计。观察 std::less 的标准库实现，我们会发现它提供了两种形式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 传统形式：指定比较类型 template \u0026lt;class T\u0026gt; struct less { bool operator()(const T\u0026amp; lhs, const T\u0026amp; rhs) const; }; // 透明形式：自动类型推导 template \u0026lt;\u0026gt; struct less\u0026lt;void\u0026gt; { template \u0026lt;class T, class U\u0026gt; auto operator()(T\u0026amp;\u0026amp; t, U\u0026amp;\u0026amp; u) const -\u0026gt; decltype(std::forward\u0026lt;T\u0026gt;(t) \u0026lt; std::forward\u0026lt;U\u0026gt;(u)); }; 当使用 std::less\u0026lt;\u0026gt; 时，我们特化到了 less\u0026lt;void\u0026gt;，它包含一个泛化的函数调用运算符。这个运算符是模板成员函数，接受任意类型的两个参数 T 和 U，并返回它们比较的结果。\n类型推导与完美转发 透明运算符的实现依赖于两个现代 C++ 核心特性：自动类型推导和完美转发。当编译器遇到 std::less\u0026lt;\u0026gt;{}(a, b) 这样的表达式时：\n模板参数推导：编译器根据参数 a 和 b 的实际类型推导出模板参数 T 和 U 完美转发：通过 std::forward 保持参数的值类别（左值/右值），避免不必要的拷贝 返回类型推导：使用 decltype 自动推导比较结果的准确类型，保留常量性、引用性等类型信息 这种机制确保了比较操作以最直接的方式进行，不引入任何中间转换。例如，比较 int 和 double 时，编译器会直接生成 int 与 double 比较的代码，遵循标准的算术类型转换规则，而不是先将两者转换为同一类型。\n1 std::pirntln(\u0026#34;{}\u0026#34;, std::less\u0026lt;\u0026gt;{}(1, 2.1)); // 输出 true 🌰 std::less 实现 1 2 3 4 5 6 7 8 struct less\u0026lt;\u0026gt; { template \u0026lt;typename T, typename U\u0026gt; auto operator()(T\u0026amp;\u0026amp; t, U\u0026amp;\u0026amp; u) const -\u0026gt; decltype(std::forward\u0026lt;T\u0026gt;(t) \u0026lt; std::forward\u0026lt;U\u0026gt;(u)) { return std::forward\u0026lt;T\u0026gt;(t) \u0026lt; std::forward\u0026lt;U\u0026gt;(u); } }; 这个简洁的实现包含了透明运算符的所有精髓：\n通用引用：T\u0026amp;\u0026amp; 和 U\u0026amp;\u0026amp; 可绑定到任何类型的左值或右值 完美转发：保持操作数的原始类型和值类别 后置返回类型：使用 decltype 确保返回类型与原生运算符完全一致 无约束模板：接受任何可比较类型，不限制操作数必须为相同类型 对比 传统实现方式及其局限 在透明运算符出现之前，C++ 开发者通常需要编写冗长的泛型仿函数或模板类来实现类似功能。考虑一个需要泛型比较的场景，传统实现可能如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 自定义泛型比较仿函数 struct GenericLess { template\u0026lt;typename T, typename U\u0026gt; auto operator()(T\u0026amp;\u0026amp; t, U\u0026amp;\u0026amp; u) const -\u0026gt; decltype(std::forward\u0026lt;T\u0026gt;(t) \u0026lt; std::forward\u0026lt;U\u0026gt;(u)) { return std::forward\u0026lt;T\u0026gt;(t) \u0026lt; std::forward\u0026lt;U\u0026gt;(u); } }; // 使用示例 std::vector\u0026lt;int\u0026gt; v = {5, 3, 8, 1, 4}; std::sort(v.begin(), v.end(), GenericLess()); 这种方式虽然可行，但存在几个明显问题：\n代码冗余：每个运算符都需要单独定义仿函数 可读性差：需要命名并实例化仿函数对象 维护成本：自定义实现可能不一致或包含错误 缺乏标准化：不同开发者可能有不同的实现风格 另一种替代方案是使用C++14 多态 lambda 表达式：\n1 2 3 4 std::sort(v.begin(), v.end(), [](auto\u0026amp;\u0026amp; t, auto\u0026amp;\u0026amp; u) -\u0026gt; decltype(auto) { return std::forward\u0026lt;decltype(t)\u0026gt;(t) \u0026lt; std::forward\u0026lt;decltype(u)\u0026gt;(u); }); 虽然更紧凑，但语法复杂，可读性低，特别是对于不熟悉现代 C++ 的开发者。\n透明运算符的简洁实现 对比传统方案，透明运算符提供了一种标准化、简洁且安全的替代方案：\n1 std::sort(v.begin(), v.end(), std::less\u0026lt;\u0026gt;()); 这行代码包含了透明运算符的所有优势：\n零冗余：直接使用标准库组件，无需自定义实现 类型安全：自动推导操作数类型，避免截断或错误转换 完美转发：保持操作数值类别，优化性能 标准化：所有开发者使用统一、可靠的实现 [!NOTE]\n但是需要注意的是 v 中的元素需要重载 operator\u0026lt;，否则会编译错误。\n类型安全对比 考虑一个具体示例，突显透明运算符如何防止类型截断错误：\n1 2 3 4 5 6 7 8 9 std::vector\u0026lt;uint64_t\u0026gt; big_nums = {UINT64_MAX, 1, UINT64_MAX-1}; // 危险的传统方式：使用固定类型的比较器 std::sort(big_nums.begin(), big_nums.end(), std::less\u0026lt;uint32_t\u0026gt;()); // 发生uint64_t到uint32_t的静默截断，排序结果错误！ // 安全的透明运算符方式： std::sort(big_nums.begin(), big_nums.end(), std::less\u0026lt;\u0026gt;()); // 保持uint64_t比较，结果正确 传统方式中，std::less\u0026lt;uint32_t\u0026gt; 强制将 uint64_t 转换为 uint32_t，可能导致高位截断。而 std::less\u0026lt;\u0026gt; 保留原始类型，进行正确的比较。\n应用场景与实践 透明运算符在现代 C++ 开发中有多种关键应用场景：\n泛型算法与容器：在 std::sort、std::set、std::map 等需要比较操作的泛型算法和容器中使用透明运算符，可避免类型限制，提高代码的通用性。 1 2 3 4 5 6 // 使用透明比较器的set可接受多种兼容类型查找 std::set\u0026lt;std::string, std::less\u0026lt;\u0026gt;\u0026gt; transparent_set; transparent_set.insert(\u0026#34;hello\u0026#34;); // 直接使用const char*查找，无需构造临时std::string auto it = transparent_set.find(\u0026#34;world\u0026#34;); 异构查找：透明运算符支持异构查找，允许在关联容器中使用与键类型不同的对象进行查找，避免不必要的临时对象创建。 1 2 3 4 5 6 std::map\u0026lt;std::string, int, std::less\u0026lt;\u0026gt;\u0026gt; transparent_map; // 插入时需要构造string（正常行为） transparent_map.emplace(\u0026#34;key\u0026#34;, 42); // 查找时可直接使用char*，无需构造临时string auto pos = transparent_map.find(\u0026#34;key\u0026#34;); 自定义类型处理：当创建自定义数值类型包装器时，透明运算符提供与内置类型一致的操作体验。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class TransparentInt { int value; public: // 转换运算符支持透明比较 operator int() const { return value; } // 透明运算符友好的设计 template \u0026lt;typename T\u0026gt; friend bool operator==(const TransparentInt\u0026amp; lhs, T\u0026amp;\u0026amp; rhs) { return lhs.value == std::forward\u0026lt;T\u0026gt;(rhs); } }; TransparentInt ti{42}; if (ti == 42.0) { // 与double直接比较 // ... } 性能敏感场景：在需要避免不必要的临时对象创建和类型转换的高性能代码中，透明运算符可减少开销。 总结 C++ 透明运算符代表了类型安全和泛型编程的重要演进。通过提供类型自适应的操作，它们解决了长期存在的类型截断和意外转换问题，使泛型代码更安全、更简洁。\n透明运算符的核心优势可总结为：\n类型安全增强：消除因类型不匹配导致的静默错误，如整数截断、有符号/无符号不匹配等问题。 代码简化：减少自定义仿函数和模板特化的需求，使代码更简洁可读。 性能优化：避免不必要的临时对象创建和类型转换，提升运行时效率。 异构支持：启用关联容器的异构查找能力，提高 API 灵活性。 标准化实践：提供一致、可靠的实现方式，减少重复造轮子和错误。 随着现代 C++ 的发展，透明运算符已成为专业 C++ 开发的基础工具。它们与 C++20 概念、范围等特性协同工作，构建更安全、更表达力的泛型代码。掌握透明运算符不仅提升现有代码质量，也为理解更高级的现代 C++ 特性奠定基础。\n\u0026quot; 透明运算符解决了泛型编程中的一个基本矛盾：我们既希望代码通用，又希望操作具体。它通过将类型决策推迟到最后一刻——实例化时刻——实现了这一平衡。\u0026quot; —— C++ 标准委员会专家观点\n","permalink":"https://kerolt.github.io/p/c-%E9%80%8F%E6%98%8E%E8%BF%90%E7%AE%97%E7%AC%A6/","summary":"\u003ch2 id=\"透明运算符的概念与价值\"\u003e透明运算符的概念与价值\u003c/h2\u003e\n\u003cp\u003e在 C++ 编程中，当编写泛型代码时，不同类型的比较或操作可能导致\u003cstrong\u003e意外的类型转换\u003c/strong\u003e或\u003cstrong\u003e精度损失\u003c/strong\u003e。假设有一个 \u003ccode\u003estd::vector\u0026lt;uint32_t\u0026gt;\u003c/code\u003e，使用自定义仿函数进行排序，一切运行正常。但当你将容器改为 \u003ccode\u003estd::vector\u0026lt;uint64_t\u0026gt;\u003c/code\u003e 却忘记修改仿函数的实现时，编译器不会报错，但数据可能在比较前被\u003cstrong\u003e静默截断\u003c/strong\u003e，导致排序结果完全错误。而 C++14 引入透明运算符可以帮助我们避免这种 bug。\u003c/p\u003e","title":"C++透明运算符"},{"content":" 本文是对于 Eunomia Tutorials 2 的一个学习记录\n什么是 kprobe Kprobe​​（Kernel Probe）是 Linux 内核提供的一项强大功能，它允许开发者和系统管理员在不​​修改内核源代码​​或重启系统的前提下，在任意内核函数处动态插入“探针”：\n​​工作原理​​：通过​​临时替换​​目标函数的前几条指令为一个断点指令（如 int3） ​​执行流程​​：当程序执行到断点时，CPU 控制权会交给 kprobe 系统 ​​事件回调​​：系统执行注册的回调函数，完成数据采集后恢复原函数执行 ​​两种类型​​： ​​Kprobe​​：在函数入口处执行 ​​Kretprobe​​：在函数返回时执行 这种机制为我们提供了​​零侵入式​​的内核行为洞察能力，特别适用于​​实时监控​​、​​性能分析​​和​​故障排查​​等场景。\ndo_unlinkat 的作用 do_unlinkat 是 Linux 内核中的一个内部函数，它的作用是执行文件或目录的删除操作。其在内核源码中的定义如下：\n1 2 3 static int do_unlinkat(int dfd, struct filename *name) { ... } do_unlinkat 是内核中实际执行文件删除逻辑的最终汇聚点 用户空间调用 unlink()、unlinkat() 或 rmdir() 等系统调用时，最终都会通过系统调用表路由到这个函数 采用文件描述符 (AT_FDCWD) 和路径名的组合方式，提供了灵活的路径解析能力 vmlinux.h 不同内核版本之间，内核数据结构如结构体字段位置、字段名称等都可能发生变化。传统的 eBPF 程序直接使用内核头文件会导致：\n兼容性问题：程序在​​不同内核版本​​中崩溃 字段偏移错误：读取到​​无效内存数据​​ 维护困难：需要针对​​每个内核版本​​进行适配 vmlinux.h 利用内核的​​BTF（BPF Type Format）​​ 信息生成与当前运行内核完全匹配的类型定义：\n1 bpftool btf dump file /sys/kernel/btf/vmlinux format c \u0026gt; vmlinux.h 内核态程序 kprobe_unlink.bpf.c 为 do_unlinkat 函数同时设置入口探针和返回探针：\n​​函数入口​​： 捕获调用进程的 PID 获取要删除的文件名 ​​函数返回​​： 捕获调用进程的 PID 获取函数返回值（删除操作的结果） 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 //go:build ignore #include \u0026#34;vmlinux.h\u0026#34; // 下面几个头文件需要安装了libbpf库 #include \u0026lt;bpf/bpf_core_read.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; #include \u0026lt;bpf/bpf_tracing.h\u0026gt; char LICENSE[] SEC(\u0026#34;license\u0026#34;) = \u0026#34;Dual BSD/GPL\u0026#34;; SEC(\u0026#34;kprobe/do_unlinkat\u0026#34;) int BPF_KPROBE(do_unlinkat, int dfd, struct filename* name) { pid_t pid; const char* filename; pid = bpf_get_current_pid_tgid() \u0026gt;\u0026gt; 32; filename = BPF_CORE_READ(name, name); bpf_printk(\u0026#34;KPROBE ENTRY pid = %d, filename = %s\\n\u0026#34;, pid, filename); return 0; } SEC(\u0026#34;kretprobe/do_unlinkat\u0026#34;) int BPF_KRETPROBE(do_unlinkat_exit, long ret) { pid_t pid; pid = bpf_get_current_pid_tgid() \u0026gt;\u0026gt; 32; bpf_printk(\u0026#34;KPROBE EXIT: pid = %d, ret = %ld\\n\u0026#34;, pid, ret); return 0; } 用户态 Go 程序 用户态程序负责三个主要任务：\n​​环境初始化​​： 移除 eBPF 的内存限制 加载编译好的 eBPF 程序 ​​探针附加​​： 将 kprobe 绑定到 do_unlinkat 将 kretprobe 绑定到 do_unlinkat 的返回点 ​​日志处理​​： 读取内核的跟踪日志 筛选并显示与文件删除相关的事件 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 package main import ( \u0026#34;bufio\u0026#34; \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/cilium/ebpf/link\u0026#34; \u0026#34;github.com/cilium/ebpf/rlimit\u0026#34; ) func main() { // 移除内存限制 if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } objs := bpfObjects{} if err := loadBpfObjects(\u0026amp;objs, nil); err != nil { log.Fatalf(\u0026#34;loading bpf objects: %v\u0026#34;, err) } defer objs.Close() // SEC(\u0026#34;kprobe/do_unlinkat\u0026#34;) kp, err := link.Kprobe(\u0026#34;do_unlinkat\u0026#34;, objs.DoUnlinkat, nil) if err != nil { log.Fatalf(\u0026#34;linking kprobe: %v\u0026#34;, err) } defer kp.Close() // SEC(\u0026#34;kretprobe/do_unlinkat\u0026#34;) krep, err := link.Kretprobe(\u0026#34;do_unlinkat\u0026#34;, objs.DoUnlinkatExit, nil) if err != nil { log.Fatalf(\u0026#34;linking kretprobe: %v\u0026#34;, err) } defer krep.Close() ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() fmt.Println(\u0026#34;eBPF 程序已成功加载，开始监控文件删除操作...\u0026#34;) fmt.Println(\u0026#34;请在另一个终端执行文件删除操作，如: rm test.txt\u0026#34;) fmt.Println(\u0026#34;按 Ctrl+C 退出\u0026#34;) // 启动协程读取内核日志 go readTraceLog() // 等待中断信号 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() \u0026lt;-ctx.Done() fmt.Println(\u0026#34;\\n正在退出...\u0026#34;) } // 读取内核跟踪日志 func readTraceLog() { file, err := os.Open(\u0026#34;/sys/kernel/debug/tracing/trace_pipe\u0026#34;) if err != nil { log.Printf(\u0026#34;无法打开 trace_pipe: %v\u0026#34;, err) log.Printf(\u0026#34;提示: 请确保以 root 权限运行程序\u0026#34;) return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() // 只显示我们程序产生的日志 if strings.Contains(line, \u0026#34;KPROBE ENTRY\u0026#34;) || strings.Contains(line, \u0026#34;KPROBE EXIT\u0026#34;) { fmt.Printf(\u0026#34;[%s] %s\\n\u0026#34;, time.Now().Format(\u0026#34;15:04:05\u0026#34;), line) } } if err := scanner.Err(); err != nil { log.Printf(\u0026#34;读取 trace_pipe 出错: %v\u0026#34;, err) } } 编译与运行 Makefile：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 克隆项目并进入目录 git clone https://github.com/kerolt/learn-ebpf # 2. 生成vmlinux.h ./vmlinux/update.sh # 3. 生成.bpf.c对应的go代码和字节码 cd kprobe_unlink go generate # 4. 运行程序 sudo go run . 运行后会得到类似的结果：\n","permalink":"https://kerolt.github.io/p/ebpf%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8kprobe%E7%9B%91%E6%B5%8B%E6%8D%95%E8%8E%B7unlink%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文是对于 \u003ca href=\"https://eunomia.dev/zh/tutorials/2-kprobe-unlink/\"\u003eEunomia Tutorials 2\u003c/a\u003e 的一个学习记录\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"什么是-kprobe\"\u003e什么是 kprobe\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eKprobe​\u003c/strong\u003e​（Kernel Probe）是 Linux 内核提供的一项强大功能，它允许开发者和系统管理员在不​\u003cstrong\u003e​修改内核源代码​\u003c/strong\u003e​或重启系统的前提下，在任意内核函数处动态插入“探针”：\u003c/p\u003e","title":"【eBPF学习】使用kprobe监测捕获unlink系统调用"},{"content":"如何修改上次的 commit 比如有的时候 commit 漏掉了文件、commit 信息写错了，可以这么做：\n你要指定一个有效的编辑器作为 Git 的默认编辑器，可以使 Vim 或者 VSCode：\n1 git config --global core.editor \u0026#34;vim\u0026#34; # 使用 VSCode 的话就用 code 然后将上次忘记提交的修改先 git add （如果有的话），接着：\n1 git commit --amend --amend 会用新的内容替换上一次提交，而不是创建一个新的提交。如果上一次提交已经被推送到远程仓库，使用 --amend 后需要强制推送：\n1 git push --force 这个方法我有时候用来同步一下两台电脑上的代码，因为有时候一台电脑上写了一部分代码但是还没有到一次提交的地步，这时如果要外出或者要用另一台电脑了，就可以用这个方法来同步一下。\n不过，使用 git push --force 强制推送后，远程仓库的历史记录会被改写。这种操作可能会导致本地和远程分支的提交历史不一致。我自己通常这么解决：\n1 2 git fetch origin git reset --hard origin/\u0026lt;branch\u0026gt; 这一般是不需要保留本地未推送的更改，可以直接将本地分支强制更新为远程分支的状态。\ngit 如何移除 submodule 1 2 3 4 5 git submodule deinit -f path/to/submodule git rm -f path/to/submodule rm -rf path/to/submodule # 如果要删除物理目录 git commit -m \u0026#34;Removed submodule path/to/submodule\u0026#34; rm -rf .git/modules/path/to/submodule # 可选清理 推送分支 第一次推送（远程分支可能还不存在） 1 git push -u origin 分支名 ## 推送到远程的另一个分支名 如果想把本地分支 dev 推送到远程的 test 分支：\n1 git push origin dev:test ","permalink":"https://kerolt.github.io/p/git%E4%BD%BF%E7%94%A8/","summary":"\u003ch2 id=\"如何修改上次的-commit\"\u003e如何修改上次的 commit\u003c/h2\u003e\n\u003cp\u003e比如有的时候 commit 漏掉了文件、commit 信息写错了，可以这么做：\u003c/p\u003e\n\u003cp\u003e你要指定一个有效的编辑器作为 Git 的默认编辑器，可以使 Vim 或者 VSCode：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit config --global core.editor \u003cspan class=\"s2\"\u003e\u0026#34;vim\u0026#34;\u003c/span\u003e \u003cspan class=\"c1\"\u003e# 使用 VSCode 的话就用 code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e然后将上次忘记提交的修改先 \u003ccode\u003egit add\u003c/code\u003e （如果有的话），接着：\u003c/p\u003e","title":"Git使用"},{"content":"这个 project 主要是希望我们理解 MVCC 的一些相关知识。MVCC 的设计目标是通过维护多个版本的元组（tuple），使得不同的事务能够访问到与其时间戳一致的数据版本，而无需通过锁机制完全阻塞其他事务的操作。具体来说：\n每个事务在启动时会被分配一个唯一的 事务 ID 或 读取时间戳 。 数据库系统会根据事务的读取时间戳，决定该事务能看到哪些数据版本： 可见性规则 ：事务只能看到在其读取时间戳之前提交的数据版本。 不可见性规则 ：事务不能看到在其读取时间戳之后提交或未提交的数据版本。 15445 的 lab 写了太久太久了，自己懒是一方面，实力不够也是一方面，task4 后面的 lab 就不打算写了（无奈.jpg）。在这整个过程中，学到的东西还挺多的，对于代码能力也有提升，以后有时间还是可以多看看配套的课程，然后再好好读读代码，写写注释和博客去深入理解理解，毕竟 bustub 的不论是代码架构还是规范我都觉得非常棒。后面就要像 15445 网页中写到的，去多关注生活中其他感兴趣的地方了，希望能够不断提升自己~\n0. Bustub 中的 undolog bustub 中在 TransactionManager 中通过一个哈希表来保存所有 tuple 的 undolog 起点：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class TransactionManager { public: ... struct PageVersionInfo { /** protects the map */ std::shared_mutex mutex_; /** Stores previous version info for all slots. Note: DO NOT use `[x]` to access it because * it will create new elements even if it does not exist. Use `find` instead. */ std::unordered_map\u0026lt;slot_offset_t, VersionUndoLink\u0026gt; prev_version_; }; /** protects version info */ std::shared_mutex version_info_mutex_; /** Stores the previous version of each tuple in the table heap. Do not directly access this field. Use the helper * functions in `transaction_manager_impl.cpp`. */ std::unordered_map\u0026lt;page_id_t, std::shared_ptr\u0026lt;PageVersionInfo\u0026gt;\u0026gt; version_info_; ... } 其中的 VersionUndoLink 也只是对 UndoLink 的一个包装：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct UndoLink { /* Previous version can be found in which txn */ txn_id_t prev_txn_{INVALID_TXN_ID}; /* The log index of the previous version in `prev_txn_` */ int prev_log_idx_{0}; ... }; struct VersionUndoLink { /** The next version in the version chain. */ UndoLink prev_; /** Whether a transaction is modifying the version link. Fall 2023: you do not need to read / write this field until * task 4.2. */ bool in_progress_{false}; ... }; 一个 tuple 最新的数据会保存在 table heap 中，而 txn manager 通过这个 tuple 的 rid 可以获取到这个 tuple 的 undolog：\nexec_ctx_-\u0026gt;GetTransactionManager()-\u0026gt;GetUndoLink(rid);\n1. Task 1 - Timestamps 每个事务有两个时间戳：\nread_ts：事务开始时分配，表示该事务能看到的最新提交的数据版本。 commit_ts：事务提交时分配，是一个单调递增的逻辑时间戳，决定事务的串行化顺序。 规则如下：\n事务开始时（Begin），read_ts = last_commit_ts 事务提交时（Commit），commit_ts = ++last_commit_ts 对于 Watermark 机制，其定义是所有活跃事务中的最小 read_ts，作用为确定哪些数据版本已经不再被任何事务读取，从而可以安全地清理旧版本数据。\n这里很简单，就是需要在维护好系统中的 watermark，遍历事务映射中的所有事务，找出所有进行中事务中最小的 read_ts 为 watermark。在事务的 Begin、Commit、Abort 中需要使用 Watermark::AddTxn 和 Watermark::RemoveTxn 来更新 watermark。在更新时，可以使用红黑树或者哈希表 + 优先队列来快速找到最小的 read_ts。\n2. Task2 - Storage Format and Sequential Scan 2.1 Tuple Reconstruction 数据库需要一种机制来“回溯”数据的历史版本，而 ReconstructTuple 正是这种机制的核心实现。\n在 MVCC 系统中，表堆（table heap）通常存储的是最新的数据版本，而旧版本的元组信息则通过 undo logs 记录下来。当事务需要访问某个元组的历史版本时，ReconstructTuple 会从表堆中获取最新的元组，并根据 undo logs 逐步恢复出符合事务读取时间戳的版本。\n在 MVCC 系统中，数据的删除操作并不会立即从表堆中移除元组，而是通过设置 is_deleted 标志来标记逻辑删除。ReconstructTuple 在处理 undo logs 时会检查 is_deleted 标志，从而判断某个元组是否已被删除。ReconstructTuple 将始终应用提供给函数的所有修改，而无需查看元数据或撤销日志中的时间戳。除了函数参数列表中提供的数据外，它不需要访问其他数据。\nundo log 记录的是上次操作完的结果（换句话说，这次没操作前是什么样子），只有 modified_fields_ 是为了告诉这次操作，在上次修改完成之前 tuple 是什么样子。\nmodified_fields_：长度与 tuple 的 schema 相同，每个位代表在这次的操作中对于某个字段时候有更新 tuple_：其长度 \u0026lt;= 原始的 tuple，只记录了 modified_fields_ 中为 true 的字段，并且保存的值是本次操作之前的值 ReconstructTuple 的工作流程：\n从表堆中读取最新版本的元组（base_tuple）及其元数据（base_meta）。 初始化 tuple_values，存储元组的字段值。 获取最新元组 从表堆中读取最新版本的元组（base_tuple）及其元数据（base_meta）。 初始化 tuple_values，存储元组的字段值。 遍历 undo logs Undo logs 按时间戳降序排列，记录了元组的历史修改。 对于每个 undo log： 如果 is_deleted_ 为 true，标记元组为已删除。 否则，根据 modified_fields_ 更新 tuple_values 中对应字段的值。（所以这里类似于去覆盖更新） 生成结果元组 如果元组被标记为删除，返回空值。 否则，根据更新后的 tuple_values 构造并返回新的元组。 2.2 Sequential Scan / Tuple Retrieval 这里的意图就是需要在 bustub 执行 seq executor 算子时找到当前事务可以使用的 tuple。\n判断当前 tuple 是否对当前事务可见:\ncase1：tuple 的 ts_ 比 txn_read_ts 小，则 tuple 对当前事务可见 case2：tuple 的 ts_ 等于 txn_temp_ts，则 tuple 对当前事务可见 case3：不满足 case1 也不满足 case2，则 tuple 对当前事务不可见 如果是 case1 和 case2，说明当前事务可以直接使用 table heap 中的这条 tuple；而如果是 case3，则需要遍历这条 tuple 的版本链，找出可以用的 undolog，如果有就使用 ReconstructTuple 来重建一个可见的历史版本的 tuple。\n3. Task3 - MVCC Executors 在插入一条记录时，要把这条记录的主键值记下来，这样之后回滚时只需要把这个主键值对应的记录删掉就好了； 在删除一条记录时，要把这条记录中的内容都记下来，这样之后回滚时再把由这些内容组成的记录插入到表中就好了； 在更新一条记录时，要把被更新的列的旧值记下来，这样之后回滚时再把这些列更新为旧值就好了。 from 小林coding - 为什么需要-undo-log\n简单理解：改了什么，你就保存什么。\nInsert 在这一步只需要将插入的 tuple 的 rid 记录到事务 write set 中，然后在 txn manager 中设置新 tuple 的 undolog 链为空（相当于给这个 undolog 链初始化）。\n为什么 Insert 操作不需要 undo log\n在 MVCC（多版本并发控制）机制下，每个事务看到的数据版本取决于其时间戳。对于 INSERT 操作，由于插入的是全新的记录，不存在“历史版本”的问题。因此，不需要为 INSERT 操作维护 Undo Log 来支持 MVCC 中的版本链。\n当执行一个 INSERT 操作时，如果事务需要回滚，只需删除刚刚插入的记录即可，而无需像 UPDATE 或 DELETE 那样恢复旧数据状态。因此，INSERT 操作只需要记录被插入记录的主键信息，以便在回滚时定位并删除该记录。这种情况下，Undo Log 的作用非常有限，甚至可以省略。\n相比之下，UPDATE 和 DELETE 操作会创建新的版本或标记旧版本为无效，这些都需要通过 Undo Log 来记录和管理。\nUpdate \u0026amp; Delete 在修改操作（update 和 delete）中，对于 tuple 中的字段有两种情况：\n情况 1（当前事务在未提交前进行了修改）：由于每个事务都对当前操作的 tuple 的 undolog 只能保存一份，因此在生成本次的 undolog 后还需要需要合并上一次的 undolog。 情况 2（这是当前事务第一次修改）：创建一个新的 undolog，并把它添加到版本链中。 在完成修改操作后，需要更新 table heap 中的值：修改 tuple 的数据为更新后的值，同时更新 tuple 的 meta，其时间戳为当前事务的 temp ts（这可用于标识这个 tuple 正在被哪个事务修改）；然后还要将当前修改 tuple 的 id（也就是其 rid）添加到当前事务的 write set，这在事务提交时会根据 rid 来正确设置 table heap 中 tuple 的 ts。\nCommit 刚刚说到在修改时需要修改 tuple 的 meta 数据中的 ts 为当前事务的 temp ts，因为此时事务还没提交，使用 temp ts 就能分辨出现在这条 tuple 是否正在被修改：temp ts 是一个很大的数（TXN_START_ID + txn_id，其中 TXN_START_ID = 1LL \u0026lt;\u0026lt; 62），事务的 read ts 和 commit ts 都会小于这个数；在判断一个 tuple 是否可见时，都是通过事务的 read ts（txn.read_ts）与 tuple.meta.ts 进行比较，如果 tuple.meta.ts \u0026lt;= txn.read_ts，那么这个 tuple 对于 txn 是可见的，而如果 tuple.meta.ts == txn.temp_ts，则这个 tuple 正在被 txn 进行修改。\n这样的话，即便是在修改后 tuple.meta.ts 也只是事务的 temp ts，所以在 commit 时，需要将其设置为系统的最新 commit ts。这里需要做几点：\n获取当前系统的 commit ts，计算方式为 last_commit_ts_ + 1 遍历当前要提交的事务的 write set，更新修改了的 tuple 的 meta 数据，将 meta.ts 设置为 commit ts 设置事务的 commit ts，并且更新系统的 last_commit_ts GarbageCollection 之前一旦我们将事务添加到 txn manager 中，我们就永远不会删除它，因为 read_ts 较小的事务可能需要读取存储在先前已提交或已中止事务中的撤销日志。这里需要实现 TransactionManager::GarbageCollection() 函数，删除未使用的事务。\n一个事务在运行时只需要看到它自己的快照，一旦事务结束，它所依赖的数据版本就可以被考虑回收。只有当某个数据版本不再被任何活跃事务需要时，才能进行垃圾回收（GC）。\n举个🌰，假设我们有如下记录：\nRID ts_ 数据 A 1 Alice A 2 Bob A 3 Charlie A 4 David 这表示记录 A 被不同事务更新了四次。现在系统中有三个事务：\n事务 ID 状态 可见性范围 T2 RUNNING ts ≤ 2 T4 RUNNING ts ≤ 4 T5 RUNNING ts ≤ 5 watermark = 2（最小活跃事务 ID）\nQ1: T5 能看到 ts_=1 的数据吗？\n没问题，因为 T5 的 ts=5 \u0026gt; 1，且事务 1 已完成。\nQ2: ts_=1 的数据可以被清理吗？\n不可以，因为 T2（ts=2）还在运行，它的可见范围是 ts ≤ 2，所以它可能会访问 ts=1 的数据。\nQ3: 如果 T2 提交或中止，watermark 会变成多少？此时 ts=1 的数据能被清理吗？\nwatermark 更新为 min(4,5) = 4 此时所有活跃事务的 ts ≥ 4 所以 ts ≤ 3 的数据都对活跃事务不可见 → 可以安全清理 这里有一个关键点：可见 ≠ 不可清理\n可见性：表示某个事务是否能看到某条记录（比如 T5 能看到 ts_=3 的记录） 清理条件：表示这条记录是否仍然被任何一个活跃事务所需要，如果没有任何活跃事务再访问它，就可以清理 所以 GC 的步骤可以简单总结如下：\n遍历数据库中所有 tuple 判断 tuple.ts_ ≤ watermark → 不再被活跃事务访问 沿着 undo link 遍历 undo log 链 统计每个事务有多少 undo log 不再被访问 如果某个事务所有 undo log 都不可见，且事务已完成 → 删除其 undo log 和事务元信息 References https://blog.csdn.net/weixin_48885685/article/details/143225977 https://github.com/ZepengLi111/cmu15445-2023-fall-bustub Qwen ChatGPT ","permalink":"https://kerolt.github.io/p/cmu15-445-fall2023project4-concurrency-control-%E5%B0%8F%E7%BB%93/","summary":"\u003cp\u003e这个 project 主要是希望我们理解 MVCC 的一些相关知识。MVCC 的设计目标是通过维护多个版本的元组（tuple），使得不同的事务能够访问到与其时间戳一致的数据版本，而无需通过锁机制完全阻塞其他事务的操作。具体来说：\u003c/p\u003e","title":"【CMU15-445 Fall2023】Project4 Concurrency Control 小结"},{"content":"C++17 为模板元编程带来了一个非常有用的特性：折叠表达式（fold expressions）。它的出现让变参模板函数的编写变得更加简洁、清晰和直观。\n1. 如何处理变长参数包 在 C++11/14 中，如果我们要处理变长参数包（parameter pack），通常需要递归展开，例如：\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;typename T\u0026gt; void print(const T\u0026amp; t) { std::cout \u0026lt;\u0026lt; t \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } template\u0026lt;typename T, typename... Args\u0026gt; void print(const T\u0026amp; t, const Args\u0026amp;... args) { std::cout \u0026lt;\u0026lt; t \u0026lt;\u0026lt; \u0026#34;, \u0026#34;; print(args...); } 虽然这段代码能完成任务，但却略显冗长，并且生成的汇编代码也会很多。C++17 中引入的折叠表达式，就是为了解决这个问题。\n可以在 godbolt 上看看两种方法生成的汇编代码：https://godbolt.org/z/55a7q85oE\n2. 什么是折叠表达式 折叠表达式是一种用运算符对参数包进行折叠的方式。折叠表达式的实例化按以下方式展开成表达式 e：\n（图片来源：https://zh.cppreference.com/w/cpp/language/fold）\n基本语法形式：\n类型 语法形式 示例 一元左折叠 (... op pack) (... + args) 一元右折叠 (pack op ...) (args + ...) 二元左折叠 (init op ... op pack) (0 + ... + args) 二元右折叠 (pack op ... op init) (args + ... + 0) 折叠表达式支持多种运算符，包括但不限于：\n算术运算符：+, -, *, / 逻辑运算符：\u0026amp;\u0026amp;, || 比较运算符：==, !=, \u0026lt;, \u0026gt; 位运算符：\u0026amp;, |, ^ 逗号运算符：, 需要注意的是：\n若参数包为空，使用二元折叠可设置初始值防止编译错误。 初始值放在哪边，就由哪一边“开始结合” 3. 折叠表达式示例 3.1 求和函数 1 2 3 4 template\u0026lt;typename... Args\u0026gt; auto sum(Args... args) { return (... + args); // 左折叠 } 使用：\n1 std::cout \u0026lt;\u0026lt; sum(1, 2, 3, 4); // 输出 10 3.2 打印变参 1 2 3 4 template\u0026lt;typename... Args\u0026gt; void print(Args\u0026amp;\u0026amp;... args) { (std::cout \u0026lt;\u0026lt; ... \u0026lt;\u0026lt; args) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // 左折叠结合流输出 } 使用：\n1 print(\u0026#34;Hello, \u0026#34;, \u0026#34;fold \u0026#34;, \u0026#34;expression!\u0026#34;); // 输出 Hello, fold expression! 3.3 所有参数是否都满足某条件 1 2 3 4 template\u0026lt;typename... Args\u0026gt; bool all_true(Args... args) { return (... \u0026amp;\u0026amp; args); } 使用：\n1 bool result = all_true(true, true, false); // false 4. 与 std::cout 结合 std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; b \u0026lt;\u0026lt; c 是一个左结合的表达式，其等价于：\n1 ((std::cout \u0026lt;\u0026lt; a) \u0026lt;\u0026lt; b) \u0026lt;\u0026lt; c 因此，在使用折叠表达式来打印参数时，必须使用左折叠！！！。\n正确示例为：\n1 2 3 4 template\u0026lt;typename... Args\u0026gt; void print(Args\u0026amp;\u0026amp;... args) { (std::cout \u0026lt;\u0026lt; ... \u0026lt;\u0026lt; args); // 左折叠 } 这个展开方式等价于：\n1 (((std::cout \u0026lt;\u0026lt; arg1) \u0026lt;\u0026lt; arg2) \u0026lt;\u0026lt; arg3) ... 这样每次都把输出结果“继续”传给 std::cout \u0026lt;\u0026lt; ...，保持流操作链的有效性。\n但如果按下面的写法来：\n1 (std::cout \u0026lt;\u0026lt; args \u0026lt;\u0026lt; ...); 会导致编译错误或者语义错误，因为 std::cout 必须放在最左边，才能正确启动流操作链。\n5. 与 std::cout 和逗号运算符结合 对于逗号运算符：在 (a, b) 中，逗号运算符会先执行 a，再执行 b，返回 b 的值。这允许我们：\n在一次折叠中 执行多条语句 且确保顺序执行、无中间变量展开 例如下面的例子：\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;iostream\u0026gt; template\u0026lt;typename... Args\u0026gt; void print(Args... args) { ((std::cout \u0026lt;\u0026lt; args \u0026lt;\u0026lt; \u0026#34;, \u0026#34;), ...); } int main() { print(1, 3.0, \u0026#39;*\u0026#39;); } 其在编译器处理之后会是这个样子（使用这个工具 https://cppinsights.io/）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;iostream\u0026gt; template\u0026lt;typename ... Args\u0026gt; void print(Args... args) { (((std::cout \u0026lt;\u0026lt; args) \u0026lt;\u0026lt; \u0026#34;, \u0026#34;), ...); } template\u0026lt;\u0026gt; void print\u0026lt;int, double, char\u0026gt;(int __args0, double __args1, char __args2) { (std::operator\u0026lt;\u0026lt;(std::cout.operator\u0026lt;\u0026lt;(__args0), \u0026#34;, \u0026#34;)) , ( (std::operator\u0026lt;\u0026lt;(std::cout.operator\u0026lt;\u0026lt;(__args1), \u0026#34;, \u0026#34;)) , (std::operator\u0026lt;\u0026lt;(std::operator\u0026lt;\u0026lt;(std::cout, __args2), \u0026#34;, \u0026#34;)) ); } int main() { print(1, 3.0, \u0026#39;*\u0026#39;); return 0; } 值得注意的是：看起来 ... 在 args 的右边，但这依然是左折叠。\n","permalink":"https://kerolt.github.io/p/c-%E6%8A%98%E5%8F%A0%E8%A1%A8%E8%BE%BE%E5%BC%8F/","summary":"\u003cp\u003eC++17 为模板元编程带来了一个非常有用的特性：\u003cstrong\u003e折叠表达式（fold expressions）\u003c/strong\u003e。它的出现让变参模板函数的编写变得更加简洁、清晰和直观。\u003c/p\u003e\n\u003ch2 id=\"1-如何处理变长参数包\"\u003e1. 如何处理变长参数包\u003c/h2\u003e\n\u003cp\u003e在 C++11/14 中，如果我们要处理变长参数包（parameter pack），通常需要递归展开，例如：\u003c/p\u003e","title":"C++折叠表达式"},{"content":"在 C++20 及更高版本中，可以使用 Concepts 来替代传统的基于虚函数的接口设计，这种方式提供了更好的编译时检查、更高效的代码生成和更灵活的接口约束。\n在之前，可能是这样的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class IDrawable { public: virtual ~IDrawable() = default; virtual void draw() const = 0; }; class Circle : public IDrawable { public: void draw() override { puts(\u0026#34;Circle::draw()\u0026#34;); } }; class Squre : public IDrawable { public: void draw() override { puts(\u0026#34;Squre::draw()\u0026#34;); } }; void render(IDrawable\u0026amp; drawable) { drawable.draw(); } 但是有了 Concept 后：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template \u0026lt;typename T\u0026gt; concept Drawable = requires(T t) { { t.draw() } -\u0026gt; std::same_as\u0026lt;void\u0026gt;; }; class Circle { public: void draw() const { puts(\u0026#34;Circle::draw()\u0026#34;); } }; class Squre { public: void draw() { puts(\u0026#34;Squre::draw()\u0026#34;); } }; template \u0026lt;Drawable T\u0026gt; void render(T\u0026amp; drawable) { drawable.draw(); } 优势：\n​​编译时多态​​：不需要运行时虚表查找，性能更高 ​​值语义​​：可以直接传递对象而不需要指针或引用 ​​更灵活的约束​​：可以约束多个不相关的类型 ​​更好的错误信息​​：编译错误更清晰明确 ​​无对象切片问题​​：因为不使用继承 ","permalink":"https://kerolt.github.io/p/%E4%BD%BF%E7%94%A8-concept-%E6%9B%BF%E4%BB%A3%E5%9F%BA%E4%BA%8E%E8%99%9A%E5%87%BD%E6%95%B0%E7%9A%84%E6%8E%A5%E5%8F%A3/","summary":"\u003cp\u003e在 C++20 及更高版本中，可以使用 Concepts 来替代传统的基于虚函数的接口设计，这种方式提供了更好的编译时检查、更高效的代码生成和更灵活的接口约束。\u003c/p\u003e\n\u003cp\u003e在之前，可能是这样的：\u003c/p\u003e","title":"使用 Concept 替代基于虚函数的接口"},{"content":"最近 Fedora42 更新了，因此我打算升级一下玩玩。在执行完下面的命令后：\n1 2 3 sudo dnf upgrade --refresh sudo dnf system-upgrade download --releasever=42 sudo dnf system-upgrade reboot 重启开机成了这样:(\n在网上搜索一圈后，执行以下命令系统就 ok 了：\n1 sudo dracut --regenerate-all --force 错误的原因可能是更新内核后没有正确生成新的 initramfs 文件，系统可能会使用旧的、不兼容的 initramfs 文件，从而导致启动失败。\n而 dracut 是一个用于生成 Linux 系统 initramfs（初始内存文件系统）的工具。运行 这条命令后，dracut 会重新生成与当前系统中所有已安装内核对应的 initramfs 文件。\n参考：https://www.reddit.com/r/Fedora/comments/1hfkqnq/boot_error_after_updating_to_6124200\n","permalink":"https://kerolt.github.io/p/%E8%A7%A3%E5%86%B3fedora42%E5%8D%87%E7%BA%A7%E5%90%AF%E5%8A%A8%E6%97%B6%E6%98%BE%E7%A4%BAkernel-panic/","summary":"\u003cp\u003e最近 Fedora42 更新了，因此我打算升级一下玩玩。在执行完下面的命令后：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo dnf upgrade --refresh\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo dnf system-upgrade download --releasever\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo dnf system-upgrade reboot\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e重启开机成了这样:(\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\"\u003e\u003c/p\u003e\n\u003cp\u003e在网上搜索一圈后，执行以下命令系统就 ok 了：\u003c/p\u003e","title":"【解决】Fedora42升级启动时显示kernel panic"},{"content":"在C++中，编译器会根据类的定义情况自动决定是否生成默认的特殊成员函数（如构造函数、拷贝/移动操作、析构函数）。\n1. 用户显式声明相关成员函数 显式声明或删除某个函数：\n如果用户显式声明（即使使用 =default 或 =delete）某个特殊成员函数，编译器将不再生成默认版本。例如：\n1 2 3 4 5 6 class Example { public: Example() = default; // 允许生成默认构造函数 Example(const Example\u0026amp;) {} // 用户定义的拷贝构造函数 // 编译器不再生成默认的移动构造函数和移动赋值运算符 }; 2. 用户定义析构函数、拷贝/移动操作的影响 定义析构函数：\n如果用户定义了析构函数（即使为空），编译器会删除默认的移动操作（移动构造函数和移动赋值运算符），但拷贝操作仍可能生成（除非其他条件阻止）。\n1 2 3 4 5 class Example { public: ~Example() {} // 用户定义的析构函数 // 移动操作被隐式删除，拷贝操作可能生成（若无其他限制） }; 定义拷贝操作：\n如果用户定义了拷贝构造函数或拷贝赋值运算符，编译器会删除默认的移动操作。\n1 2 3 4 5 class Example { public: Example(const Example\u0026amp;) {} // 用户定义的拷贝构造函数 // 移动操作被隐式删除 }; 定义移动操作：\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 #include \u0026lt;utility\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;iostream\u0026gt; struct Example { std::string str; Example() = default; Example(const std::string\u0026amp; s): str(s) { std::cout \u0026lt;\u0026lt; \u0026#34;Example()\\n\u0026#34;; } Example(Example\u0026amp;\u0026amp; other) { str = std::move(other.str); std::cout \u0026lt;\u0026lt; \u0026#34;Example(Example\u0026amp;\u0026amp;)\\n\u0026#34;; } Example\u0026amp; operator=(Example\u0026amp;\u0026amp; other) { str = std::move(other.str); std::cout \u0026lt;\u0026lt; \u0026#34;operator=(Example\u0026amp;\u0026amp;)\\n\u0026#34;; return *this; } }; int main() { Example x1(\u0026#34;Hello\u0026#34;); Example x2 = std::move(x1); Example x3; x3 = std::move(x2); } 执行结果为：\n1 2 3 Example() Example(Example\u0026amp;\u0026amp;) operator=(Example\u0026amp;\u0026amp;) 3. 类成员或基类的限制 不可默认构造/拷贝/移动的成员：\n如果类中包含无法默认构造、拷贝或移动的成员（如 std::unique_ptr、带有删除拷贝操作的类），则对应的默认特殊成员函数会被隐式删除。\n1 2 3 4 class Example { std::unique_ptr\u0026lt;int\u0026gt; ptr; // 不可拷贝 }; // 默认的拷贝构造函数和拷贝赋值运算符被删除 基类或成员的特殊成员函数被删除：\n如果基类或成员的某个特殊成员函数被删除或不可访问，派生类对应的函数也会被隐式删除。\n1 2 3 4 5 6 7 8 class NonCopyable { public: NonCopyable(const NonCopyable\u0026amp;) = delete; }; class Derived : public NonCopyable { // 拷贝构造函数被隐式删除，因为基类的拷贝构造函数被删除 }; ","permalink":"https://kerolt.github.io/p/c-%E4%BD%95%E6%97%B6%E4%BC%9A%E9%98%BB%E6%AD%A2%E9%BB%98%E8%AE%A4%E7%9A%84%E7%89%B9%E6%AE%8A%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%E7%9A%84%E7%94%9F%E6%88%90/","summary":"\u003cp\u003e在C++中，编译器会根据类的定义情况自动决定是否生成默认的特殊成员函数（如构造函数、拷贝/移动操作、析构函数）。\u003c/p\u003e\n\u003ch2 id=\"1-用户显式声明相关成员函数\"\u003e1. 用户显式声明相关成员函数\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e显式声明或删除某个函数\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e如果用户显式声明（即使使用 \u003ccode\u003e=default\u003c/code\u003e 或 \u003ccode\u003e=delete\u003c/code\u003e）某个特殊成员函数，编译器将\u003cstrong\u003e不再生成默认版本\u003c/strong\u003e。例如：\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"C++何时会阻止默认的特殊成员函数的生成"},{"content":" 该系列博客只是为了记录自己在写 Lab 时的思路，按照课程要求不会在 Github 和博客中公开源代码。欢迎与我一起讨论交流！\n这个 project 和之前就不一样了，开始深入数据库内核的实现了。需要理清楚一条 sql 语句是如何被执行的，方才能写出代码。\n前置奶酪 一条 SQL 语句的执行 这里需要去看看一条 sql 语句传入 bustub 内部之后的代码：src/common/bustub_instance.cpp:ExecuteSqlTxn：\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 auto BustubInstance::ExecuteSqlTxn(const std::string \u0026amp;sql, ResultWriter \u0026amp;writer, Transaction *txn, std::shared_ptr\u0026lt;CheckOptions\u0026gt; check_options) -\u0026gt; bool { if (!sql.empty() \u0026amp;\u0026amp; sql[0] == \u0026#39;\\\\\u0026#39;) { // 处理元命令 ... } // binder，但是在其中会使用libpg_query来解析sql语句 bustub::Binder binder(*catalog_); binder.ParseAndSave(sql); // 经过上一步后，binder中的statement_nodes_存储着所有的语句解析节点 for (auto *stmt : binder.statement_nodes_) { // 将stmt转换成BoundStatement对象，方便后面处理数据 auto statement = binder.BindStatement(stmt); // 只有不需要构建plan树、不需要进行优化的sql语句才会在switch之后继续执行 switch (statement-\u0026gt;type_) { ... } // 生成初步的执行计划 bustub::Planner planner(*catalog_); planner.PlanQuery(*statement); // 优化刚刚的执行计划 bustub::Optimizer optimizer(*catalog_, IsForceStarterRule()); auto optimized_plan = optimizer.Optimize(planner.plan_); ... // 执行优化后的plan，这里会使用火山模型去根据下面节点的Next函数来执行相应的算子 execution_engine_-\u0026gt;Execute(optimized_plan, \u0026amp;result_set, txn, exec_ctx.get()); // 将执行结果输出至指定位置 ... } return 是否执行成功;\t} 在 binder 之后，我们就有了一条 sql 的语句解析节点，例如执行 select * from (select * from test_2 where colA \u0026gt; 10) where colB \u0026gt; 2;，其 statement node 如下：\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 BoundSelect { table=BoundSubqueryRef { alias=__subquery#0, subquery=BoundSelect { table=BoundBaseTableRef { table=test_2, oid=23 }, columns=[\u0026#34;test_2.colA\u0026#34;, \u0026#34;test_2.colB\u0026#34;, \u0026#34;test_2.colC\u0026#34;], groupBy=[], having=, where=(test_2.colA\u0026gt;10), limit=, offset=, order_by=[], is_distinct=false, ctes=, }, columns=[\u0026#34;test_2.colA\u0026#34;, \u0026#34;test_2.colB\u0026#34;, \u0026#34;test_2.colC\u0026#34;], }, columns=[\u0026#34;__subquery#0.test_2.colA\u0026#34;, \u0026#34;__subquery#0.test_2.colB\u0026#34;, \u0026#34;__subquery#0.test_2.colC\u0026#34;], groupBy=[], having=, where=(__subquery#0.test_2.colB\u0026gt;2), limit=, offset=, order_by=[], is_distinct=false, ctes=, } 其中的子查询和 where 的条件还有需要哪些列都能非常清楚的看到。\nIterator Model 通常一个 SQL 会被组织成树状的查询计划，数据从叶子节点流到根节点，查询结果在根节点中得出。\nbustub 中采用的数据库查询执行模型叫做迭代器模型，也叫火山模型。\n查询计划（query plan）中的每步 operator 对应的 executor 都实现一个 next 函数，每次调用时，operator 返回一个 tuple 或者 null，后者表示数据已经遍历完毕。operator 本身实现一个循环，每次调用其 child operators 的 next 函数，从它们那边获取下一条数据供自己操作，这样整个 query plan 就被从上至下地串联起来。\n但是像 Joins, Aggregates, Subqueries, Order By 这样的操作需要等所有 children 返回它们的 tuple。虽然一次调用请求一条数据，占用内存较小，但函数调用开销大。\nCatalog, Table and Index 下图出处：https://www.cnblogs.com/joey-wang/p/17351258.html\n索引 index 在 Bustub 中，索引用于加速数据访问。索引通过维护表中数据的有序结构，使得查询可以更快地定位到所需的记录。\n结构 索引的结构图和上面表的结构图类似。在 catalog 中，可以获取到一个表对应的所有IndexInfo，每个 IndexInfo 中包含着这个索引的信息，这里讲两个个我认为比较重要的成员变量：\nkey_schema_：索引对应的列的结构，例如使用其 ToString() 函数时，其会返回 (添加了索引的列的名称:该列的数据类型) index_：这是一个指针，指向一个 Index 类的对象，也就是真正的索引 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 // catalog.h class Catalog { public: template \u0026lt;class KeyType, class ValueType, class KeyComparator\u0026gt; auto CreateIndex(Transaction *txn, const std::string \u0026amp;index_name, const std::string \u0026amp;table_name, const Schema \u0026amp;schema, const Schema \u0026amp;key_schema, const std::vector\u0026lt;uint32_t\u0026gt; \u0026amp;key_attrs, std::size_t keysize, HashFunction\u0026lt;KeyType\u0026gt; hash_function, bool is_primary_key = false, IndexType index_type = IndexType::HashTableIndex) -\u0026gt; IndexInfo *; auto GetIndex(const std::string \u0026amp;index_name, const std::string \u0026amp;table_name) -\u0026gt; IndexInfo *; auto GetIndex(const std::string \u0026amp;index_name, const table_oid_t table_oid) -\u0026gt; IndexInfo *; auto GetIndex(index_oid_t index_oid) -\u0026gt; IndexInfo *; auto GetTableIndexes(const std::string \u0026amp;table_name) const -\u0026gt; std::vector\u0026lt;IndexInfo *\u0026gt;; ... private: ... /** * Map index identifier -\u0026gt; index metadata. * * NOTE: that `indexes_` owns all index metadata. */ std::unordered_map\u0026lt;index_oid_t, std::unique_ptr\u0026lt;IndexInfo\u0026gt;\u0026gt; indexes_; /** Map table name -\u0026gt; index names -\u0026gt; index identifiers. */ std::unordered_map\u0026lt;std::string, std::unordered_map\u0026lt;std::string, index_oid_t\u0026gt;\u0026gt; index_names_; /** The next index identifier to be used. */ std::atomic\u0026lt;index_oid_t\u0026gt; next_index_oid_{0}; }; struct IndexInfo { ... /** The schema for the index key */ Schema key_schema_; /** The name of the index */ std::string name_; /** An owning pointer to the index */ std::unique_ptr\u0026lt;Index\u0026gt; index_; /** The unique OID for the index */ index_oid_t index_oid_; /** The name of the table on which the index is created */ std::string table_name_; /** The size of the index key, in bytes */ const size_t key_size_; /** Is primary key index? */ bool is_primary_key_; /** The index type */ [[maybe_unused]] IndexType index_type_{IndexType::BPlusTreeIndex}; }; Index 中有着三个虚函数供其派生类去实现，其唯一的成员变量的类型为 IndexMeta，用来存储一些元信息，例如这个索引的名称，它所属的表的名称，最重要的还有一个 key_attrs_，稍后就说谈论它。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // index.h class IndexMeta { ... private: /** The name of the index */ std::string name_; /** The name of the table on which the index is created */ std::string table_name_; /** The mapping relation between key schema and tuple schema */ const std::vector\u0026lt;uint32_t\u0026gt; key_attrs_; /** The schema of the indexed key */ std::shared_ptr\u0026lt;Schema\u0026gt; key_schema_; /** Is primary key? */ bool is_primary_key_; }; class Index { ... private: /** The Index structure owns its metadata */ std::unique_ptr\u0026lt;IndexMetadata\u0026gt; metadata_; }; fall2023 我们使用的是哈希索引，底层使用的就是在 project2 中实现的可拓展哈希。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // extendible_hash_table_index.h #define HASH_TABLE_INDEX_TYPE ExtendibleHashTableIndex\u0026lt;KeyType, ValueType, KeyComparator\u0026gt; template \u0026lt;typename KeyType, typename ValueType, typename KeyComparator\u0026gt; class ExtendibleHashTableIndex : public Index { public: ExtendibleHashTableIndex(std::unique_ptr\u0026lt;IndexMetadata\u0026gt; \u0026amp;\u0026amp;metadata, BufferPoolManager *buffer_pool_manager, const HashFunction\u0026lt;KeyType\u0026gt; \u0026amp;hash_fn); ~ExtendibleHashTableIndex() override = default; auto InsertEntry(const Tuple \u0026amp;key, RID rid, Transaction *transaction) -\u0026gt; bool override; void DeleteEntry(const Tuple \u0026amp;key, RID rid, Transaction *transaction) override; void ScanKey(const Tuple \u0026amp;key, std::vector\u0026lt;RID\u0026gt; *result, Transaction *transaction) override; protected: // comparator for key KeyComparator comparator_; // container DiskExtendibleHashTable\u0026lt;KeyType, ValueType, KeyComparator\u0026gt; container_; }; 更新索引 当插入新记录时，不仅需要将记录插入到表中，还需要将相应的索引条目插入到索引中。这样，后续的查询操作可以利用索引快速定位到目标记录。如果不更新索引，后续的查询操作可能会错过新插入的记录，导致查询结果不准确。\nUPDATE 和 DELETE 如何影响索引？\nUPDATE\n✅ 需要更新索引的情况：修改了索引列 的值（例如：将 id=100 的 name 字段从 \u0026ldquo;Alice\u0026rdquo; 改为 \u0026ldquo;Bob\u0026rdquo;，而 name 被建了索引）\n此时旧索引项（\u0026ldquo;Alice\u0026rdquo;）需要被删除 新索引项（\u0026ldquo;Bob\u0026rdquo;）需要被插入 ❌ 不需要更新索引的情况：修改的是非索引列\n例如：age 字段没有被索引，修改它不会影响索引 DELETE\n无论删除哪一行，只要该行在某个索引中存在，就需要从索引中删除对应的条目 。\nbustub 中有哈希索引和 B+Tree 索引，fall2023 版本使用的是可拓展哈希作为作为索引。不过这两个具体的实现都有一个基类 Index，其中有以下虚函数需要子类去实现：\nInsertEntry(const Tuple \u0026amp;key, RID rid, Transaction *transaction): 插入一个索引条目。 DeleteEntry(const Tuple \u0026amp;key, RID rid, Transaction *transaction): 删除一个索引条目。 ScanKey(const Tuple \u0026amp;key, std::vector\u0026lt;RID\u0026gt; *result, Transaction *transaction): 根据索引键搜索记录，并将结果 RID 存储在指定的向量中。 所以不管用的是哈希还是 B+Tree，在操作索引时用的接口都相同。\n如何理解索引 就如网上很多介绍索引的博客所描述的那样，数据库索引是用来加速检索速度的，就如同新华字典中的音节索引一样：\n如同 table_info，catalog 中也有许多的 index_info，每个 index_info 就如同上图音节表中的一个字母。我们对一个字段（列）构建一个索引，就如同在上图中音节表中多加一个字母（例如 X）。\n需要插入一条记录时，就往对应的索引下插入 (记录， 对应记录的地址) 这样的键值对，例如上图的 (xian, 519)，这里的地址为 RID。 需要删除一条记录时，在对应的索引下删掉匹配的键值对。 需要更新一条记录时，由于 bustub 没有提供更新索引的 API，所以可以用先删除再插入的方式模拟更新。 执行器如何使用索引获取数据 当执行器需要从表中获取数据时，如果查询计划中包含索引扫描操作，执行器会通过索引来快速定位数据。以下是具体的步骤：\n解析查询计划： 执行器根据查询计划确定需要使用的索引。 获取索引的元数据，包括索引键的模式和表列的映射关系。 构建索引键： 根据查询条件和索引的元数据，构建索引键。这通常涉及到从查询条件中提取列值，并根据索引键的模式进行转换。 使用索引进行搜索： 调用索引的 ScanKey 方法，传入构建好的索引键和一个结果 RID 向量。 索引会根据键值查找对应的记录，并将找到的 RID 存储在结果向量中。 读取数据页： 使用结果向量中的 RID，从缓冲池中查找对应的页。如果页不在缓冲池中，则从磁盘加载到缓冲池。 从页中读取数据并创建 Tuple 对象。 处理和返回结果： 使用 Tuple 对象的方法（如 GetValue、IsNull 等）访问和处理元组中的数据。 将处理后的数据作为结果返回给用户或进一步处理。 谓词下推 谓词下推（Predicate Pushdown）是数据库查询优化中的一种技术，其核心思想是将查询中的过滤条件（即谓词）尽可能早地应用到查询执行计划的底部，也就是数据生成的地方。这样做的目的是为了减少数据的传输量和处理量，从而提高查询效率。\n具体来说，谓词下推包括以下几个方面：\n过滤条件前移：在查询执行的过程中，尽早地对数据进行过滤，这样不需要将所有数据都传递到上层操作中，只传递满足条件的数据。 减少数据传输：通过在数据生成的阶段就进行过滤，可以减少从数据库存储引擎到查询处理引擎之间的数据传输量。 减少 CPU 处理：不需要对所有数据进行后续的处理，只需要处理已经过滤的数据，这样可以减少 CPU 的工作量。 利用索引：如果过滤条件可以利用现有的索引，谓词下推可以使得查询直接利用索引来快速定位数据，而不是扫描整个表。 优化查询计划：数据库优化器会根据谓词下推的原则重新规划查询的执行步骤，生成更高效的查询计划。 例如：\n1 SELECT * FROM employees WHERE department_id = 5 AND salary \u0026gt; 50000; 在这个查询中，WHERE 子句包含了两个过滤条件。如果不进行谓词下推，数据库可能会先扫描整个 employees 表，然后将所有行传递给上层操作，之后再应用过滤条件。而通过谓词下推，数据库可以在扫描表的时候直接应用这些过滤条件，只返回部门 ID 为 5 且薪资大于 50000 的员工记录。\n谓词下推是数据库查询优化中非常重要的一环，它有助于提高查询性能，特别是在处理大规模数据集时。数据库优化器会尝试自动应用谓词下推，但有时开发者也可以通过编写更优化的查询条件来帮助优化器更好地进行谓词下推。\nTask1 - Access Method Executors SeqScan 顺序扫描指定的表，表的遍历可以使用 TableIterator。\n每次找到一条没有被标记为“删除”或者不是 where 之类的过滤子句匹配（这里会在 delete 操作中说明）的 tuple（记录）就并返回，如果已经扫描到了表的结束位置则返回 false。\nInsert 为什么 Insert 等 Executor 有 child 而 SeqScan 没有？ InsertExecutor 的主要职责是将一条或多条记录插入到指定的表中。它可能需要依赖于其他 Executor 来获取要插入的数据。例如，如果 INSERT 操作是从一个 SELECT 查询的结果集中插入数据，那么 InsertExecutor 可能会有一个子 Executor（如 SeqScanExecutor 或其他类型的 Executor），该子 Executor 负责执行 SELECT 操作并提供数据给 InsertExecutor。\n因此，InsertExecutor 有 child 是因为它可能需要从另一个查询的结果中获取数据。\nSeqScanExecutor 的主要职责是对表进行全表扫描，即按顺序读取表中的所有记录。这是一个基本的操作，通常不需要其他 Executor 的支持来完成其工作。\n它直接作用于存储层，遍历表中的每一行数据，因此没有子 Executor。它的任务相对简单，就是遍历和返回表中的所有记录。\n简而言之，InsertExecutor 需要 child 是因为它的操作可能涉及从其他查询结果中获取数据，而 SeqScanExecutor 不需要 child 是因为它的任务是独立完成的，只需遍历表中的所有记录即可。这反映了数据库执行计划中不同操作之间的依赖关系和交互方式。其他 Executor 同理。\n举个批量插入的🌰：\n假设我们有一个 orders 表，包含以下列：\norder_id (主键) customer_id product_id quantity order_date 我们希望通过一个子查询（select）来获取一批订单记录，并将这些记录插入到 orders 表中：\n1 2 3 4 INSERT INTO orders (customer_id, product_id, quantity, order_date) SELECT customer_id, product_id, quantity, order_date FROM pending_orders WHERE status = \u0026#39;approved\u0026#39;; 很明显，我们在插入之前要从 select 子句中获取数据，因此这个子查询操作就是 insert 操作的 child_executor。\n没有子操作时，需要插入的数据从哪里获取？ 比如执行如下 SQL 时：\n1 insert into test_1 values (202, 1, 2, 3); 从肉眼看可以知道需要插入的数据为 (202, 1, 2, 3)，但是在代码中又是从哪里获取的呢？\n让我们使用一下 explain 工具来看看这条 SQL 语句在 bustub 内部做了什么：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bustub\u0026gt; explain insert into test_1 values (202, 1, 2, 3); === BINDER === BoundInsert { table=BoundBaseTableRef { table=test_1, oid=22 }, select= BoundSelect { table=BoundExpressionListRef { identifier=__values#0, values=[[\u0026#34;202\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;]] }, columns=[\u0026#34;__values#0.0\u0026#34;, \u0026#34;__values#0.1\u0026#34;, \u0026#34;__values#0.2\u0026#34;, \u0026#34;__values#0.3\u0026#34;], groupBy=[], having=, where=, limit=, offset=, order_by=[], is_distinct=false, ctes=, } } === PLANNER === Insert { table_oid=22 } | (__bustub_internal.insert_rows:INTEGER) Projection { exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:INTEGER, __values#0.3:INTEGER) Values { rows=1 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:INTEGER, __values#0.3:INTEGER) === OPTIMIZER === Insert { table_oid=22 } | (__bustub_internal.insert_rows:INTEGER) Values { rows=1 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:INTEGER, __values#0.3:INTEGER) 将目标聚焦在生成的查询计划上，从上到下，在一个 Insert 的查询计划中，使用 Projection 从其输入源（如表扫描、索引扫描、连接等）中提取所需的列，最下层使用 Value 获取到要操作的数据！\n所以对应的，InsertExecutor 的 child_executor 为 ProjectionExecutor，而 ProjectionExecutor 的 child_executor 为 ValuesExecutor，使用迭代器模型就能很方便的获取到数据了（ValuesExecutor 就是最后的 Executor，其 Next 函数不会再往下调用，其所做的只是根据在解析 SQL 及其之后的一些步骤中得到的需要操作的数据封装成一个 tuple 进行返回）。\nDelete 需要写的代码和 insert 操作的基本相同。但有个地方需要注意一下，在执行一条 delete 语句时，让我们看看做了些什么：\n1 2 3 4 5 6 7 8 9 10 bustub\u0026gt; explain delete from test_1 where colA = 999; === BINDER === Delete { table=BoundBaseTableRef { table=test_1, oid=22 }, expr=(test_1.colA=999) } === PLANNER === Delete { table_oid=22 } | (__bustub_internal.delete_rows:INTEGER) Filter { predicate=(#0.0=999) } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER) SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER) === OPTIMIZER === Delete { table_oid=22 } | (__bustub_internal.delete_rows:INTEGER) SeqScan { table=test_1, filter=(#0.0=999) } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER) 可以看到，在 optimizer 阶段，where 子句的 filter 下放至 SeqScan 处与其合并了，也就是说，我们需要在实现 SeqScanExecutor 时注意处理一下 filter。这里提示一下：\n1 2 while (cur_tuple.first.is_deleted_ || (plan_-\u0026gt;filter_predicate_ \u0026amp;\u0026amp; !(plan_-\u0026gt;filter_predicate_-\u0026gt;Evaluate(tuple, GetOutputSchema()).GetAs\u0026lt;bool\u0026gt;()))) 如果其返回 true，说明 filter 匹配到了数据（就如例子中匹配到了 colA 列为 999 的 tuple），如果此时这个 tuple 没有被标记为删除，那么就说明找到了我们需要删除的 tuple。\nUpdate 我们如何知道 update 需要更新的数据从哪里取呢？\n1 2 3 4 5 6 7 8 bustub\u0026gt; explain(p, o) update test_1 set colB = 15445; === PLANNER === Update { table_oid=22, target_exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;15445\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } Filter { predicate=true } SeqScan { table=test_1 } === OPTIMIZER === Update { table_oid=22, target_exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;15445\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } SeqScan { table=test_1, filter=true } 看看 Update 中，有一个 target_exprs 数组，这个数组可不就是我们的一行数据吗，并且是需要更新的那行数据：update 语句可以不加 where 子句，这样就是选中表中的所有行，也就是这里将表中 colB 列的数据都 update 为 15445！\n对于 target_exprs 这个数组，我们可以通过 plan_-\u0026gt;target_expressions_ 获取，然后用其构建一个新的 tuple。\n需要注意的是，这里并没有提供直接更新 tuple 的操作，所以我们的 update 操作可以用先删除后插入的方式来模拟。\nIndexScan 我们首先需要完成 OptimizeSeqScanAsIndexScan 这个优化步骤。\n假设现在我们有一个表叫“test_1”，其列如下：\n1 2 3 +-------------+-------------+-------------+-------------+ | test_1.colA | test_1.colB | test_1.colC | test_1.colD | +-------------+-------------+-------------+-------------+ 现在我们希望这条 SQL 能执行的更快：\n1 select * from test_1 where colB = 11; 那比较不错的方法就是给 colB 列加上索引：\n1 create index v1 on test_1(colB); 这样在执行时可以更快速的查找数据。\n那么为了实现这一目标，我们需要通过 OptimizeSeqScanAsIndexScan 将 plan 树中的 SeqScanPlanNode 转换成 IndexScanPlanNode，这样我们才能使用 IndexScanPlanNode 对应的算子 \u0026mdash;IndexScanExecutor 去使用索引。但是由于 bustub 的一些设计，需要遵循以下规则才可以转化：\n当前的节点的类型必须是 PlanType::SeqScan 当前节点必须有 filter 谓词，如果只是 select * from test_1; 这样的是不需要使用索引的 当前表中必须有索引，没有索引还玩啥呢 fileter 谓词中的逻辑表达式只能有一个，并且其类型必须是 ComparisonType::Equal（我想这里必须是“等于”是不是因为 fall2023 使用的索引是哈希索引） 在当前表的索引信息中找到与 filter 谓词相对应的索引后才能返回一个 IndexScanPlanNode 在 select * from test_1 where colB = 11; 中，加了索引后，最需要关注的就是 where colB = 11 这一个过滤条件。bustub 中要求 IndexScan 过滤运算符必须是 =，且只能有一个条件。如果 colA 和 colB 都是索引，然后执行 select * from test_1 where colB = 11 and colA = 1;，这样是不会走索引优化的。\n我们将查询计划中的过滤谓词转化成 ComparisonExpression 类型，据我的理解，其可通过 GetChildAt 函数获取比较谓词左边的列名表达式（即这里的“colB”，有了这个列名的表达式，我们就可以获取到这个列的 col_id 值）和右边的值（即“11”）。之后需要去这张表中的所有索引中去找是否有 colB 的索引，怎么确定是否有呢，那就要看这个表中的每个 Index 的 key_attrs_：\n在 Index 中，key_attrs_ 决定了索引的关键字由哪些列组成，对应了每个列的下标。例如：\n如果表的 schema 定义有 5 列，分别为 A, B, C, D, E。 某个索引的 key_attrs_ 是 [0, 2]，则表示该索引使用了第 0 列（A）和第 2 列（C）作为其关键字。 如果之前我们获取的列的 col_id 值和某个 Index 的 key_attrs_ 中的值相同，那么就存在相应的索引！这时构造一个 IndexScanPlanNode 返回即可（参考 merge_filter_scan.cpp 中是如何做的）。\n当优化器成功更换节点后，在执行时就会走索引，其底层算子就会使用到 IndexScanExecutor。到这里，这个算子需要做的事情就很简单了，就是调用哈希索引的 ScanKey 进行查找，不过这里有 3 点需要注意：\n在 project2 中，我们实现的索引引擎只支持一个键对应一个值（不只是这个版本的可拓展哈希，其他版本中的 B+Tree 也只要求这样实现），也就是我们的这个算子在底层索引引擎不扩展的情况下最多查到一条记录，这样的话就可以在算子的 Init 函数中调用 ScanKey 查找记录就行。 可拓展哈希中是将一个 Tuple 的 data 转化一下当作 key，所以在索引中，其是将索引列的值作为 key，其对应的 oid 作为值。在使用 ScanKey 时，第一个参数需要的 tuple 将用查询计划节点 IndexScanPlanNode 中的 pred_key_ 构造。 表中的 tuple 的元数据中，其 is_deleted_ 可能为 true，这说明这个 tuple 在逻辑上已经删除了，所以如果我们通过 2 中获取的 oid 对应的 tuple 是这种情况，就不用向上返回数据。 Task2 - Aggregation \u0026amp; Join Executors Aggregation 分析一个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bustub\u0026gt; EXPLAIN SELECT MAX(colC), MIN(colB) FROM test_1 GROUP BY colA HAVING MAX(colB) \u0026gt; 10; === BINDER === BoundSelect { table=BoundBaseTableRef { table=test_1, oid=22 }, columns=[\u0026#34;max([\\\u0026#34;test_1.colC\\\u0026#34;])\u0026#34;, \u0026#34;min([\\\u0026#34;test_1.colB\\\u0026#34;])\u0026#34;], groupBy=[\u0026#34;test_1.colA\u0026#34;], having=(max([\u0026#34;test_1.colB\u0026#34;])\u0026gt;10), where=, limit=, offset=, order_by=[], is_distinct=false, ctes=, } === PLANNER === Projection { exprs=[\u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (\u0026lt;unnamed\u0026gt;:INTEGER, \u0026lt;unnamed\u0026gt;:INTEGER) Filter { predicate=(#0.1\u0026gt;10) } | (test_1.colA:INTEGER, agg#0:INTEGER, agg#1:INTEGER, agg#2:INTEGER) Agg { types=[\u0026#34;max\u0026#34;, \u0026#34;max\u0026#34;, \u0026#34;min\u0026#34;], aggregates=[\u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.1\u0026#34;], group_by=[\u0026#34;#0.0\u0026#34;] } | (test_1.colA:INTEGER, agg#0:INTEGER, agg#1:INTEGER, agg#2:INTEGER) SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER) === OPTIMIZER === Projection { exprs=[\u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (\u0026lt;unnamed\u0026gt;:INTEGER, \u0026lt;unnamed\u0026gt;:INTEGER) Filter { predicate=(#0.1\u0026gt;10) } | (test_1.colA:INTEGER, agg#0:INTEGER, agg#1:INTEGER, agg#2:INTEGER) Agg { types=[\u0026#34;max\u0026#34;, \u0026#34;max\u0026#34;, \u0026#34;min\u0026#34;], aggregates=[\u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.1\u0026#34;], group_by=[\u0026#34;#0.0\u0026#34;] } | (test_1.colA:INTEGER, agg#0:INTEGER, agg#1:INTEGER, agg#2:INTEGER) SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER) 对于 AggregationExecutor，我们可以获取到 SQL 语句中：\n聚合操作的 types（进行聚合操作的类型）和 aggregates（需要聚合操作的列），二者一一对应 需要 group by 进行分组的列 再看看 lecture 中的这个例子，其对列 cid 使用 group by 进行分组，其中涉及的聚合操作为 AVG，可转换成 COUNT 和 SUM 操作。这里相当于：\ntypes = [\u0026#39;count\u0026#39;, \u0026#39;sum\u0026#39;] aggregates = [\u0026#39;s.gpa\u0026#39;, \u0026#39;s.gpa\u0026#39;] 我的理解是根据 group by 的字段的值进行 hash 函数处理作为哈希表的键，例如图中的“15-445”，“15-826”等；然后哈希表的值为一个集合，这个集合的大小和 types 和 aggregates 的大小相同，并且对应的位置就为 aggregates 的值：例如图中键“15-445”的值中，第一个元素就为 COUNT 操作下 s.gpa 为 15-445 的个数。\n在 lab 中需要实现 count、sum、max、min 操作，其实就是在 SimpleAggregationHashTable：：CombineAggregateValues 中实现对应的操作即可，本质上是对哈希表的几个很简单的操作。\naggregation 通常需要对一组数据进行计算，这些计算具有以下特点：\n需要完整输入：Aggregation 通常需要从下层拉取所有相关数据才能计算结果，例如计算 SUM 需要遍历所有行。 阻塞性：在传统实现中，Aggregation 算子通常被称为“阻塞算子”，因为它必须等待所有输入数据都拉取完成才能产出结果。这意味着 next() 调用会被延迟，直到聚合计算完成。 在我们的火山模型中 aggregation 是阻塞算子：\n当上层算子调用 next() 时，aggregation 会向下层算子连续调用 next()，直到拉取完全部数据并完成聚合。 在数据尚未完全拉取并聚合完成之前，上层的 next() 调用无法直接返回结果。 需要注意的是：\nSQL 中进行 group by 后使用 count，统计的是每组数据中的记录数，而非分组后新表的行数。 distinct 其实就是对某个字段进行 group by 操作 count(*) 统计 null，而 count(字段) 不统计 null NestedLoopJoin Inner Join：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bustub\u0026gt; EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE; === BINDER === BoundSelect { table=BoundCrossProductRef { left=BoundBaseTableRef { table=__mock_table_1, oid=0 }, right=BoundBaseTableRef { table=__mock_table_3, oid=2 } }, columns=[\u0026#34;__mock_table_1.colA\u0026#34;, \u0026#34;__mock_table_1.colB\u0026#34;, \u0026#34;__mock_table_3.colE\u0026#34;, \u0026#34;__mock_table_3.colF\u0026#34;], groupBy=[], having=, where=(__mock_table_1.colA=__mock_table_3.colE), limit=, offset=, order_by=[], is_distinct=false, ctes=, } === PLANNER === Projection { exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) Filter { predicate=(#0.0=#0.2) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) NestedLoopJoin { type=Inner, predicate=true } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) === OPTIMIZER === NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bustub\u0026gt; EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE; === BINDER === BoundSelect { table=BoundJoin { type=Inner, left=BoundBaseTableRef { table=__mock_table_1, oid=0 }, right=BoundBaseTableRef { table=__mock_table_3, oid=2 }, condition=(__mock_table_1.colA=__mock_table_3.colE) }, columns=[\u0026#34;__mock_table_1.colA\u0026#34;, \u0026#34;__mock_table_1.colB\u0026#34;, \u0026#34;__mock_table_3.colE\u0026#34;, \u0026#34;__mock_table_3.colF\u0026#34;], groupBy=[], having=, where=, limit=, offset=, order_by=[], is_distinct=false, ctes=, } === PLANNER === Projection { exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) === OPTIMIZER === NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) 可以看到，即便我们不加 inner join，默认情况下也是使用的 Inner Join：NestedLoopJoin 中的 type 均为 Inner。\nLeft Join：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bustub\u0026gt; EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE; === BINDER === BoundSelect { table=BoundJoin { type=Left, left=BoundBaseTableRef { table=__mock_table_1, oid=0 }, right=BoundBaseTableRef { table=__mock_table_3, oid=2 }, condition=(__mock_table_1.colA=__mock_table_3.colE) }, columns=[\u0026#34;__mock_table_1.colA\u0026#34;, \u0026#34;__mock_table_1.colB\u0026#34;, \u0026#34;__mock_table_3.colE\u0026#34;, \u0026#34;__mock_table_3.colF\u0026#34;], groupBy=[], having=, where=, limit=, offset=, order_by=[], is_distinct=false, ctes=, } === PLANNER === Projection { exprs=[\u0026#34;#0.0\u0026#34;, \u0026#34;#0.1\u0026#34;, \u0026#34;#0.2\u0026#34;, \u0026#34;#0.3\u0026#34;] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) NestedLoopJoin { type=Left, predicate=(#0.0=#1.0) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) === OPTIMIZER === NestedLoopJoin { type=Left, predicate=(#0.0=#1.0) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER, __mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER) MockScan { table=__mock_table_3 } | (__mock_table_3.colE:INTEGER, __mock_table_3.colF:VARCHAR) 由于火山模型中的 Next 每次返回一条 tuple，所以我们需要在 Init 中得到 join 后的所有 tuple。Nested Loop Join 其实就是用两个 for 循环去遍历两张表，保存满足筛选条件的 tuple。\ninner join 只会返回两个表中满足连接条件的 tuple；而 left join 会返回左表中的所有记录，以及右表中满足条件的 tuple。所以在嵌套 for 循环中，如果右表中有未能满足条件的 tuple，那么就保存左表中的每一列的值，并且加上右表中每一列的 null 值。\nTask3 - HashJoin Executor and Optimization 为什么需要 hash join 呢？如果两张要进行 join 的表非常大，这时使用 NLJ 的时间复杂度就为 O(n*m)，但是如果我们使用 hash join，先对一张表建立哈希映射，然后再对另一张表进行哈希检测来判断是否满足连接条件，这样就只要各扫描一次两张表，时间复杂度就降为了 O(n+m)。\n将 NL Join 优化为 Hash Join 实验指导中说，优化器需要将 nl join 优化为 hash join 的情况为：连接条件中有多个等值 AND 操作，即 x = y AND a = b AND ...。那我们只要在碰到 NestedLoopPlanNode 时获取其谓词，判断一下是否满足该情况就好了。\n需要注意的是，当谓词仅为 a = b 时，这个谓词是一个 ComparationExpression；而当它为 x = y AND a = b AND ... 时，它是一个 LogicExpression。这里在判断的是否需要区分，而在之前的实验中，有学习到可以使用标准库中的 std::dynamic_pointer_cast 函数来将谓词从 Expression基类 转换为指定的 Expression派生类，当转换失败时，会返回空指针，这样我们可以通过返回的指针来判断是否是想要的 Expression，如果为空，就代表是另一个 Expression。\n由于谓词中很有可能是 LogicExpression 嵌套着 LogicExpression 和 ComparationExpression，数量也不确定，且子 LogicExpression 很可能还有嵌套，这样的话使用递归来处理就非常方便。\n实现 Hash Join hash join 的核心就是：先对一张表建立哈希映射，然后再对另一张表进行哈希检测来判断是否满足连接条件。哈希表中的 key 为连接条件中对应的值的拼接，value 为整个 tuple。而一张表中的所有 tuple，很有可能会有几个列是完全相同的，如果这些列刚好作为连接条件，那进行哈希时就会造成 key 相同的情况，这就造成了哈希碰撞。\n解决方法也很简单：\n使用 std::unordered_map，将拼接的列值作为 key，而值的类型为 std::vector\u0026lt;Tuple\u0026gt;; 使用 std::unordered_multimap 来处理。 当我们构建好哈希表后，每次获取了另一张表中的 tuple 后进行一次哈希检测，如果 key 存在，需要将所有 value 都进行连接拼接。同 nested loop join 一样，需要特殊处理连接类型为 left 的情况。\nTask4 - Sort + Limit Executors + Window Functions + Top-N Optimization Sort 这个算子的实现思路很简单，但是需要注意两点：\norder by 后面可以跟多个关键字，也就是需要在 std::sort 中对多个关键字进行排序（利用 for 循环）。 这些关键字中可能进行算数运算（算数对象类型为 ArithmeticExpression，搞清楚进行运算的函数就可）。 Limit 这个就更简单了，根据 limit 的返回对应数量的 tuple。\nTopN topn 算子的优化只有 sort 和 limit 同时出现的是否才会触发，这里的优化逻辑比较简单。\n而具体的算子的实现其实就是一个优先队列。举个例子，现在 tuple 按照某个关键字升序排列，并且 limit 为 10，那么就可以构造一个小根堆，并维护其大小最多为 10，最后留在堆中的就是结果。如果力扣刷了一点题的话很容易就能理解这里。\nWindow Function 说来惭愧，学 SQL 的时候并不知道窗口函数这个东西\u0026hellip;简单来理解，就是在使用聚合函数的后面加上 over ([可选操作]) 即可对区间进行聚合操作。\n对于下面的 SQL，最后输出的 schema 应该和 WindowFunc.columns 相同，并且行数也和子 executor 返回的行数相同（下文将子 executor 返回的 tuple 称为 child_tuples），只是多了一些额外的计算列。\n1 2 3 4 5 6 7 8 9 10 11 12 bustub\u0026gt; explain(o) select v1, min(v1) over () as min_v1, max(v1) over () as max_v1, count(v1) over () as count_v1, sum(v1) over () as sum_v1 from t1; === OPTIMIZER === WindowFunc { columns=#0.0, placeholder, placeholder, placeholder, placeholder, , window_functions={ 1=\u0026gt;{ function_arg=#0.0, type=min, partition_by=[], order_by=[] }, 2=\u0026gt;{ function_arg=#0.0, type=max, partition_by=[], order_by=[] }, 3=\u0026gt;{ function_arg=#0.0, type=count, partition_by=[], order_by=[] }, 4=\u0026gt;{ function_arg=#0.0, type=sum, partition_by=[], order_by=[] } } } SeqScan { table=t1 } 在这条 SQL 中，返回的 tuple 的格式应该为：\n这条 tuple 的 v1 列的值 所有 tuple 中最小的 v1 的值 所有 tuple 中最大的 v1 的值 所有 tuple 中 v1 的个数 所有 tuple 中 v1 的值的和 这其实和 WindowFunc.columns 有很大的关系，placeholder 说明这只是个占位符，其应该为 window_functions[下标] 对应的窗口函数。例如第二个 placeholder 在 columns 的下表为 1，其对应的窗口函数就是 { function_arg=#0.0, type=min, partition_by=[], order_by=[] }。那么就可以通过 column 来找到每个窗口对应的哈希表。\n️需要注意的是：\n如果按照 ORDER BY 进行排序后，每一行的窗口范围从第一行开始扩展到当前行 否则每一行的窗口范围是整个 child_tuples 既然如此，我们需要先从 child_executor 获取到所有的 child_tuples，然后对 child_tuples 遍历：如果有分组行为，那就需要按照某一列（或多列）进行分组，然后对于每一个 tuple，都让其执行一次 WindowFunc.window_functions 中的窗口函数。\n稍有不同的是，之前的 aggregation 操作分组后可能需要完成多个聚合函数，但是这里我们分组之后，只会完成一个聚合函数，因为每个窗口中只有一个聚合函数。\n现在看来，这个窗口函数也无非就是对某个范围内的 tuple 进行分组和聚合操作，实验指导中也提示我们可以去利用 task2 中写的代码。在我的实现方案中，并不像 task2 中只使用一个哈希表，因为在一条 SQL 函数中，可能有多个窗口函数，每一个窗口函数又可能又不同的分组和不同的聚合操作，因此我对于每一个窗口函数都设置了一个哈希表。\n该 task 中 bustub 很仁慈地简化了难度：如果窗口函数任意一个中有 order by，那么所有窗口函数的 order by 都相同。不过在有排序和没有排序的情况下，窗口的范围有所不同，处理起来的方法也不同。\n无排序的情况 这个情况下，每个窗口函数的范围就是整个 child_tuples。之后再次遍历每一条 tuple，按照输出的 schema 来构建返回的 tuple。那么实现操作应该如下：\n先描所有 tuple，生成这条 tuple 对应的 key 和 value，再把 key 和 value 加入对应的哈希表。\n然后对于每一条 tuple，遍历 WindowFunc.columns：\n如果 column 不是 placeholder，那么说明这个位置是这个 tuple 中的一列，获取这一列对应的 Value 就好。 如果 column 是 placeholder，那么说明这个位置的值应该是对应的窗口函数的执行结果，那么从 column 对应的哈希表中找出这个 tuple 对应的值就 ok。 有排序的情况 在这个情况下，排序号后每条 tuple 的范围是自身及之前的所有 tuple，而不是像之前一样的所有 tuple。这就有点像一句话：“走一步看一步”。\n还是遍历每一条 tuple，对每一条 tuple 又遍历 WindowFunc.columns：\n如果 column 不是 placeholder，那么说明这个位置是这个 tuple 中的一列，获取这一列对应的 Value 就好。这里和无排序的情况一致。 如果 column 是 placeholder， 生成这条 tuple 对应的 key 生成这条 tuple 对应的 value，并将 {key, value} 插入 column 对应的哈希表（在 task2 中可知，这里的“插入”其实是对 key 处的旧值 old_value 与新值 value 进行聚合操作） 从哈希表中取出 key 对应的 value 这里的 2.2 和 2.3 就是之前所说的“走一步看一步”：当前 tuple 的结果是在之前的 tuple 上聚合而来的。\n总结 这个 Project 需要我们去深入理解 bustub 的源码，知道一条 SQL 会被解析成一棵什么样的 plan 树，在经过基于规则的优化器优化后才会是最终的物理 plan 树，这时又要去理解这棵树上每个节点对应的算子应该是怎么实现的。其实知道了 plan 树是什么样子后，节点对应的算子就按照要求去设计就好了。\n在众多 AI 例如 ChatGPT、DeepSeek、Kimi 等帮助下，还是慢慢理解并完成了这个 Project！但是有的地方我可能还没有做的比较好，以后有时间再优化一下。\n","permalink":"https://kerolt.github.io/p/cmu15-445-fall2023project3-query-execution-%E5%B0%8F%E7%BB%93/","summary":"\u003cblockquote\u003e\n\u003cp\u003e该系列博客只是为了记录自己在写 Lab 时的思路，按照课程要求不会在 Github 和博客中公开源代码。欢迎与我一起讨论交流！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个 project 和之前就不一样了，开始深入数据库内核的实现了。需要理清楚一条 sql 语句是如何被执行的，方才能写出代码。\u003c/p\u003e","title":"【CMU15-445 Fall2023】Project3 Query Execution 小结"},{"content":"最近把博客的构建工具从 Hexo 换成了 Hugo，感觉 Hugo 配置和使用起来更简洁方便。\n由于我的博客总体来说有两个仓库，一个私有仓库是放置建站工具的目录，其中包含博客 Markdown 内容、一些配置还有主题；另一个就是通过 GitHub Pages 来访问公共仓库。为了方便，之前在使用 Hexo 的使用使用了 Github Actions 来一键部署博客，换成 Hugo 后这个 actions 需要修改一下。\n.github/workflows/hugo.yml：\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 name: GitHub Pages on: push: branches: - master # 监听 master 分支的推送事件 pull_request: jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: true # 拉取 Hugo 主题子模块 fetch-depth: 0 # 获取完整提交历史 - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: \u0026#34;0.126.2\u0026#34; extended: true - name: Build run: hugo --minify # 启用压缩优化 - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.ACCESS_TOKEN }} # 仓库访问令牌 external_repository: kerolt/kerolt.github.io # 发布的仓库地址 PUBLISH_BRANCH: master # 部署分支 PUBLISH_DIR: ./public # Hugo 输出目录 关键点说明：\n子模块处理\nsubmodules: true，确保能正确拉取 Hugo 主题（因为我的博客主题是通过 git submodule 添加的） Hugo 扩展版\nextended: true ，因为大多数 Hugo 主题需要 Sass/SCSS 支持 安全凭证\n通过 secrets.ACCESS_TOKEN 实现安全部署，需要在 Repo Settings → Secrets 中添加一个具有 repo 权限的 Personal Access Token 双仓库模式\n使用 external_repository 将构建结果发布到独立的 GitHub Pages 仓库，实现源码与部署分离 ","permalink":"https://kerolt.github.io/p/hugo%E9%85%8D%E7%BD%AEgithub-actions/","summary":"\u003cp\u003e最近把博客的构建工具从 Hexo 换成了 Hugo，感觉 Hugo 配置和使用起来更简洁方便。\u003c/p\u003e\n\u003cp\u003e由于我的博客总体来说有两个仓库，一个私有仓库是放置建站工具的目录，其中包含博客 Markdown 内容、一些配置还有主题；另一个就是通过 GitHub Pages 来访问公共仓库。为了方便，之前在使用 Hexo 的使用使用了 Github Actions 来一键部署博客，换成 Hugo 后这个 actions 需要修改一下。\u003c/p\u003e","title":"Hugo配置Github Actions"},{"content":"微信在不久前终于有了 Linux 原生版本，我的电脑是 Fedora41，之前安装的是 flatpak 打包的微信，现在在官网下载 rpm 包后运行发现无法使用 fcitx 的中文输入法，找了一下是环境遍历的问题。\n需要添加的环境变量为：\n1 2 3 export XMODIFIERS=\u0026#34;@im=fcitx\u0026#34; export GTK_IM_MODULE=\u0026#34;fcitx\u0026#34; export QT_IM_MODULE=\u0026#34;fcitx\u0026#34; 但是在 KDE6 Wayland 下如果把它写入 /etc/profile 中好像会有问题？所以我把这个环境变量放到 wechat.desktop 中去，相当于给 /usr/bin/wechat 这个程序进行隔离（重点在 Exec 中）：\n1 2 3 4 5 6 7 8 9 10 11 [Desktop Entry] Name=wechat Name[zh_CN]=微信 Exec=env XMODIFIERS=\u0026#34;@im=fcitx\u0026#34; GTK_IM_MODULE=\u0026#34;fcitx\u0026#34; QT_IM_MODULE=\u0026#34;fcitx\u0026#34; /usr/bin/wechat %U StartupNotify=true Terminal=false Icon=/opt/wechat/icons/wechat.png Type=Application Categories=Utility; Comment=Wechat Desktop Comment[zh_CN]=微信桌面版 ","permalink":"https://kerolt.github.io/p/linux%E4%B8%8B%E5%BE%AE%E4%BF%A1%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8%E4%B8%AD%E6%96%87%E8%BE%93%E5%85%A5%E6%B3%95%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3/","summary":"\u003cp\u003e微信在不久前终于有了 Linux 原生版本，我的电脑是 Fedora41，之前安装的是 flatpak 打包的微信，现在在官网下载 rpm 包后运行发现无法使用 fcitx 的中文输入法，找了一下是环境遍历的问题。\u003c/p\u003e","title":"Linux下微信无法使用中文输入法问题解决"},{"content":"什么是mmap？ mmap 是一种用于将文件或设备与进程的地址空间关联起来的内存映射技术。通过 mmap，可以将文件的内容直接映射到进程的虚拟内存地址空间，使得文件的内容可以像操作普通内存一样进行读取和写入。\n在Linux中，虚拟内存的布局如下：\n图片来源：小林coding\n当我们在Linux上使用mmap系统调用时，得到的文件映射就会放在图中的“文件映射与匿名映射区”。每当我们需要读取或修改文件时，只需要去操作这一块虚拟内存即可，而省去了将文件的内容从磁盘读取到内核缓冲区，然后再拷贝到用户空间的缓冲区，这大大减小了资源开销。\n系统调用参数说明 该lab希望我们实现xv6上的mmap和munmap系统调用，其函数声明为：\n1 2 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t len); 这与Linux上的使用是相同的，对其中的参数解释如下：\nvoid *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);：\naddr (void *): 这是建议的映射起始地址。通常设置为 NULL，由内核自动选择合适的地址。如果指定了非空地址，则内核尽量在这个地址处创建映射（但不保证）。（xv6中不要求实现，addr只要考虑为0/NULL的情况） 如果使用了 MAP_FIXED 标志，则必须将映射建立在 addr 所指向的地址，否则映射会失败。（xv6中不要求实现） len (size_t): 要映射的内存长度（以字节为单位）。如果不是页大小的倍数，通常会向上舍入到最近的页边界。 prot (int): 映射区域的保护权限。可以是以下权限的组合： PROT_READ: 映射区域可读。 PROT_WRITE: 映射区域可写。 PROT_EXEC: 映射区域可执行。 PROT_NONE: 映射区域不可访问。 flags (int): 控制映射对象的类型、映射页是否可共享、映射是否同步到磁盘等。常见的标志有： MAP_SHARED: 共享映射，对映射区域的修改会同步到底层文件，其他映射到同一文件的进程也会看到修改。 MAP_PRIVATE: 私有映射，对映射区域的修改不会影响底层文件，修改是写时复制的（Copy-On-Write）。 MAP_ANONYMOUS: 创建一个匿名映射，与文件无关。fd 参数被忽略，通常与 MAP_PRIVATE 结合使用。（xv6不要求实现） fd (int): 打开的文件描述符，表示要映射的文件。如果使用 MAP_ANONYMOUS 标志，则此参数被忽略，通常设为 -1。 offset (off_t): 文件映射的起始偏移量。必须是页大小的整数倍。（xv6中不要求实现，即只要输入0） int munmap(void *addr, size_t len)：\naddr (void *): 要解除映射的起始地址。这个地址必须是由之前的 mmap 调用返回的地址，或者是由 mmap 创建的某个映射区域的地址。 len (size_t): 要解除映射的内存长度，必须与 mmap 调用中的 len 相匹配。如果长度小于 mmap 时指定的长度，可能会导致部分映射区域仍然保留。 如何实现？ 在xv6的虚拟内存布局中，可以看到堆区和trapframe之间有一片没有使用的区域，我们可以拿它作为文件映射区域。（xv6和Linux的虚拟内存布局有点区别，xv6的堆区在栈区上面）\n当使用mmap系统调用时，也可以使用懒分配的策略（类似于Copy On Write）：我们在mmap系统调用中 标识（不是分配） 文件映射区中有一个区域与文件相关联，但这时还不会分配物理块，自然还不急着将文件读入这片内存区域；当我们需要访问这片区域的内存时，可以通过触发page fault来分配物理块，然后读入文件内容到内存块中，并将虚拟内存映射到这块物理内存上。\n使用munmap系统调用时，会解除文件在映射区[addr, addr + len]范围内的映射，将这块区域的内存写回文件，并释放掉这块内存。实验中保证释放的区域大小一定是页的整数倍。\n我们也仿照Linux上的，让文件映射区从高地址处开始向低地址增长。下图是文件映射的样子，左边为映射区域大小不固定，右边为映射大小为页框的整数倍：\n在实验的提示中，有说到mmaptest中没有使用的功能可以不实现，其中每次使用mmap都是映射的PGSIZE的整数倍，那也就说明我们可以之用考虑右边的情况，这让实验降低了一点复杂度。\n标识映射区域 根据实验提示，我们需要为每个进程设置用于标识映射区域的结构体：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // proc.h #define NVMA 16 struct vma { uint len; // 映射区域大小 uint prot; // 映射区域的保护权限 struct file *file; // 需要映射的文件 int used; // 是否被使用 int flags; // 映射类型 int offset; // 偏移量 uint64 start; // 映射区域开始的地址 uint64 end; // 映射区域结束的地址 }; struct proc { ... struct vma vmas[NVMA]; // Virtual memory area }; 实现sys_mmap 在此之前，我们需要先注册mmap和munmap系统调用，这里我们就不赘述了\n获得映射区中的可用区域 什么意思呢？我们的映射区设计的是从高地址向低地址增长，那么我们每次需要增长时，最简单的就是在已有的映射区中找到地址最低的，并将新的映射区放在其之后，即地址最低的映射区的start就是新的映射区的end：\n可是这样的算法有很大的问题：如果我们取消了文件2的映射后，有一个只需要一个页框的映射区，按照这个算法它会被安排到文件3的映射区下面，这样就浪费了之前释放的映射区。\n不过嘛，在这个实验中这么做没什么问题😜，如果想知道更好的方法，可以参考这篇博客。\n我的实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // sysfile.c // 获取一个可使用的vma的end地址 static uint64 vma_end() { struct proc *p = myproc(); struct vma *v = 0; uint64 min_vma_end = TRAPFRAME; for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used \u0026amp;\u0026amp; p-\u0026gt;vmas[i].end \u0026lt;= min_vma_end) { min_vma_end = p-\u0026gt;vmas[i].end; v = \u0026amp;p-\u0026gt;vmas[i]; } } // 如果进程中还没有文件映射，就从trapframe后开始设置映射区 if (!v) { return min_vma_end; } // 这里可以直接返回v-\u0026gt;start，这样做可以处理映射区域大小不固定的情况（应该吧） return PGROUNDDOWN(v-\u0026gt;start); } sys_mmap 虽然刚刚我们有了可以获取映射区地址的函数，但是这个系统调用并不用真正分配内存，它只需要进行标记vma即可。\n找到一个可以使用的vma区域的end地址 初始化vma 返回vma的start地址 这里我觉得最重要的就是设置start和end地址，一个映射区的范围为[start, end)，其长度就为len，通过vma_end函数我们可以获取新映射区的end地址，再通过end - len即可得到start地址。\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 // sysfile.c uint64 sys_mmap(void) { // void *mmap(void *addr, int len, int prot, int flags, int fd, int offset); uint64 addr; int len, prot, flags, fd, offset; struct proc *p = myproc(); struct file *f; argaddr(0, \u0026amp;addr); argint(1, \u0026amp;len); argint(2, \u0026amp;prot); argint(3, \u0026amp;flags); argfd(4, \u0026amp;fd, \u0026amp;f); argint(5, \u0026amp;offset); if (addr \u0026lt; 0 || len \u0026lt; 0 || prot \u0026lt; 0 || flags \u0026lt; 0 || fd \u0026lt; 0 || offset \u0026lt; 0) { return -1; } if (!f-\u0026gt;readable \u0026amp;\u0026amp; (prot \u0026amp; PROT_READ) \u0026amp;\u0026amp; (flags \u0026amp; MAP_SHARED)) { return -1; } if (!f-\u0026gt;writable \u0026amp;\u0026amp; (prot \u0026amp; PROT_WRITE) \u0026amp;\u0026amp; (flags \u0026amp; MAP_SHARED)) { return -1; } // 找到一个可用的vma struct vma *v = 0; for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used == 0) { v = \u0026amp;p-\u0026gt;vmas[i]; break; } } if (!v) { return -1; } // 初始化vma uint64 end = vma_end(); v-\u0026gt;len = len; v-\u0026gt;prot = prot; v-\u0026gt;file = f; v-\u0026gt;used = 1; v-\u0026gt;flags = flags; v-\u0026gt;offset = offset; v-\u0026gt;end = end; v-\u0026gt;start = end - len; // 有文件映射时，对应的文件的引用计数也+1 filedup(f); return v-\u0026gt;start; } 懒分配策略 找到触发fault的地址，并据此找到对应的vma 校验 分配物理内存块 设置权限 读取文件内容到内存块中，注意偏移量 设置物理内存与虚拟内存的映射 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 // trap.c // 处理mmap的懒分配策略 static int handle_mmap_fault(uint64 addr) { struct proc *p = myproc(); struct vma *v = 0; // 根据触发fault的地址，并据此找到对应的vma for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used \u0026amp;\u0026amp; addr \u0026gt;= p-\u0026gt;vmas[i].start \u0026amp;\u0026amp; addr \u0026lt; p-\u0026gt;vmas[i].end) { v = \u0026amp;p-\u0026gt;vmas[i]; break; } } if (!v) { printf(\u0026#34;no no no\\n\u0026#34;); return -1; } // 校验 if (!v-\u0026gt;file-\u0026gt;readable \u0026amp;\u0026amp; r_scause() == 13 \u0026amp;\u0026amp; (v-\u0026gt;flags \u0026amp; MAP_SHARED)) { return -1; } if (!v-\u0026gt;file-\u0026gt;writable \u0026amp;\u0026amp; r_scause() == 15 \u0026amp;\u0026amp; (v-\u0026gt;flags \u0026amp; MAP_SHARED)) { return -1; } // 设置内存块权限 uint perm = PTE_V | PTE_U; if (v-\u0026gt;prot \u0026amp; PROT_READ) { perm |= PTE_R; } if (v-\u0026gt;prot \u0026amp; PROT_WRITE) { perm |= PTE_W; } if (v-\u0026gt;prot \u0026amp; PROT_EXEC) { perm |= PTE_X; } // 分配物理块 char *pa = kalloc(); if (!pa) { return -1; } memset(pa, 0, PGSIZE); // 读取文件内容到内存块 uint offset = addr - v-\u0026gt;start; ilock(v-\u0026gt;file-\u0026gt;ip); if (readi(v-\u0026gt;file-\u0026gt;ip, 0, (uint64)pa, offset, PGSIZE) == 0) { iunlock(v-\u0026gt;file-\u0026gt;ip); return -1; } iunlock(v-\u0026gt;file-\u0026gt;ip); // 设置虚拟内存与物理内存的映射 mappages(p-\u0026gt;pagetable, PGROUNDDOWN(addr), PGSIZE, (uint64)pa, perm); return 0; } 然后在usertrap中处理读写造成的page fault：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // trap.c void usertrap(void) { ... if(r_scause() == 8){ ... } else if((which_dev = devintr()) != 0){ // ok } else if (r_scause() == 13 || r_scause() == 15) { if (handle_mmap_fault(r_stval()) != 0) { printf(\u0026#34;usertrap(): unexpected scause %p pid=%d\\n\u0026#34;, r_scause(), p-\u0026gt;pid); printf(\u0026#34; sepc=%p stval=%p\\n\u0026#34;, r_sepc(), r_stval()); setkilled(p); } } else { ... } .. } 实现sys_munmap sys_munmap sys_munmap需要将内存块中的内容写回文件，并释放这个内存块。这里我们将这个操作额外封装一层，即不将具体实现放在sys_munmap中，这是因为在进程销毁也需要使用这个操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // sysfile.c uint64 sys_munmap(void) { // int munmap(void *addr, int len); uint64 addr; int len; argaddr(0, \u0026amp;addr); argint(1, \u0026amp;len); if (addr \u0026lt; 0 || len \u0026lt; 0) { return -1; } return munmap(addr, len); } 解除映射 遍历所有的vma，找到addr所在的vma，要求addr不能是vma区域的中间位置，可以是开头和结束位置。 使用mmap_writeback将这addr的内容写回对应的文件 更新vma的范围 如果vma的len小于等于0，说明该文件的映射已经结束，可以关闭文件了，同时这个vma也应该释放了 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 // vm.c // 解除区域 [addr, addr + len) 的文件映射 uint64 munmap(uint64 addr, int len) { struct proc *p = myproc(); struct vma *v = 0; for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used \u0026amp;\u0026amp; addr \u0026gt;= p-\u0026gt;vmas[i].start \u0026amp;\u0026amp; addr \u0026lt; p-\u0026gt;vmas[i].end) { v = \u0026amp;p-\u0026gt;vmas[i]; break; } } if (!v) { return -1; } // 不在合法的位置 if (addr \u0026gt; v-\u0026gt;start \u0026amp;\u0026amp; addr + len \u0026lt; v-\u0026gt;end) { return -1; } // 将映射区域写回文件 mmap_writeback(p-\u0026gt;pagetable, addr, len, v); // 修改映射区域大小 if (addr == v-\u0026gt;start) { v-\u0026gt;start += len; } else if (addr == v-\u0026gt;end - len) { v-\u0026gt;end = addr; } v-\u0026gt;len -= len; // 映射区域大小为0 if (v-\u0026gt;len \u0026lt;= 0) { fileclose(v-\u0026gt;file); v-\u0026gt;used = 0; } return 0; } 将映射区内容写回文件 遍历这个vma中的所有页框，对于其中的每一个页帧，获取对应的pte，需要考虑到由于懒分配带来的影响。 如果这个页帧被修改过，并且这块vma的策略是可写，那么就将这个页写回文件，注意偏移量 释放这块页帧对应的物理内存 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 // 将映射区域写回文件，并释放映射区域的内存 static int mmap_writeback(pagetable_t pgtbl, uint64 src_va, int len, struct vma *vma) { pte_t *pte; uint64 addr; // 遍历区域的页框 for (addr = PGROUNDDOWN(src_va); addr \u0026lt; PGROUNDDOWN(src_va + len); addr += PGSIZE) { // 获取页帧对应的pte if ((pte = walk(pgtbl, addr, 0)) == 0) { panic(\u0026#34;mmap_writeback\u0026#34;); } // 这是为了处理这样一种情况：使用了mmap系统调用却没有有访问映射的文件，由于懒分配的策略， // 在写回文件时vma虽然有效，但是对应的pte并没有设置PTE_V，映射区域也还没有真正的映射文件 if (!(*pte \u0026amp; PTE_V)) { continue; } // 映射区域被修改了，可以写回文件 if ((*pte \u0026amp; PTE_D) \u0026amp;\u0026amp; (vma-\u0026gt;flags \u0026amp; MAP_SHARED)) { begin_op(); ilock(vma-\u0026gt;file-\u0026gt;ip); uint offset = addr - src_va; writei(vma-\u0026gt;file-\u0026gt;ip, 1, addr, offset, PGSIZE); iunlock(vma-\u0026gt;file-\u0026gt;ip); end_op(); } kfree((void *)PTE2PA(*pte)); *pte = 0; } return 0; } 我们使用到了pte中的一个标志位PTE_D，它是用来标识一个页框是否被修改了（即脏位），我们需要在riscv.h中定义它：\n1 2 3 // riscv.h #define PTE_D (1L \u0026lt;\u0026lt; 7) 在exit时需要清空映射区 当进程退出时，其映射区中的内容也需要释放，这也是为什么要将munmap独立出来的原因。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // proc.c void exit(int status) { struct proc *p = myproc(); if(p == initproc) panic(\u0026#34;init exiting\u0026#34;); for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used) { if (munmap(p-\u0026gt;vmas[i].start, p-\u0026gt;vmas[i].len) != 0) { panic(\u0026#34;exit: munmap\u0026#34;); } } } ... } 在fork时需要“复制”映射区 我们这里所说的复制并不是将映射区的内存块在fork时都复制给子进程，可别忘了COW哦，我们只需要复制父进程中的vma数组，知道映射的哪些位置有什么样的文件映射，在真正访问时再按需加载即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // proc.c int fork(void) { ... for (int i = 0; i \u0026lt; NVMA; i++) { if (p-\u0026gt;vmas[i].used) { np-\u0026gt;vmas[i] = p-\u0026gt;vmas[i]; // 子进程也映射了和父进程相同的文件，那么这个文件的引用计数也要增加 filedup(p-\u0026gt;vmas[i].file); } } ... } Code Details 代码实现详情请见：Github\nReference https://xiaolincoding.com/os/3_memory/linux_mem.html#_3-%E8%BF%9B%E7%A8%8B%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4 https://ttzytt.com/2022/08/xv6_lab11_record/index.html Summary 这个lab的代码还是比较多的，不过它还给我们放了些水，只让我们实现一些基础的功能。在lab中更重要的是要搞清楚mmap的实现原理，一定要去理解其中的细节。\n","permalink":"https://kerolt.github.io/p/mit6.s081lab10-mmap/","summary":"\u003ch2 id=\"什么是mmap\"\u003e什么是mmap？\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003emmap\u003c/code\u003e 是一种用于将文件或设备与进程的地址空间关联起来的内存映射技术。通过 \u003ccode\u003emmap\u003c/code\u003e，可以将文件的内容直接映射到进程的虚拟内存地址空间，使得文件的内容可以像操作普通内存一样进行读取和写入。\u003c/p\u003e","title":"【MIT6.S081】Lab10 mmap"},{"content":"Intro 在这个实验中，我们需要让xv6支持更大的文件和软链接。实验总体不是特别难，不过需要我们理解好文件系统是如何工作的。Lab8 lock中的Buffer Cache也是文件系统的一部分，不过它位于文件系统的下层，这里我们需要处理的更多在上层，偏应用层。\nLarge files 如何扩大单个文件的大小上限？ 要求我们扩大单个文件的大小，实现机制就是通过修改inode的块索引，将原来的12个直接索引和一个一级间接索引变为11个直接索引、一个一级间接索引和一个二级间接索引。\n在xv6中，一个数据块的大小为1KB，由于间接块中存放的是下一个块的地址（其实个人认为更准确的说是块号），使用的类型是uint，那么一个间接块能存放这样的地址共1KB / sizeof(uint) = 1KB / 4B = 256个。\n这样的话，单个文件大小最大为12 * 1KB + 256 * 1KB = 268KB变为了11 * 1KB + 256 * 1KB + 256 * 256 * 1KB = 65803KB。\n修改inode结构体 其数据块地址中：有11个直接块地址，1个一级间接块地址，1个二级间接块地址\n在原本的代码中，表示直接块数量的宏为NDIRECT，其定义在fs.h中，我们需要修改其值为11，并且由于我们扩大了文件大小上限，所以也要修改对应的宏MAXFILE，扩大后的应该为：NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT；同时还要修改inode结构体：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // fs.h #define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) // On-disk inode structure struct dinode { short type; // File type short major; // Major device number (T_DEVICE only) short minor; // Minor device number (T_DEVICE only) short nlink; // Number of links to inode in file system uint size; // Size of file (bytes) uint addrs[NDIRECT+2]; // Data block addresses }; 内存中保存的inode结构体也需要修改：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // file.h // in-memory copy of an inode struct inode { uint dev; // Device number uint inum; // Inode number int ref; // Reference count struct sleeplock lock; // protects everything below here int valid; // inode has been read from disk? short type; // copy of disk inode short major; short minor; short nlink; uint size; uint addrs[NDIRECT+2]; }; 修改bmap函数 bmap函数会根据传入参数的数据块号来返回其对应的地址\n我们在bmap中看到如果数据块还没有分配的时候，会使用balloc来分配，并返回分配的块的块号。\n这个函数我一开始看的时候对于如何分配的比较奇怪，后来在AI的帮助下慢慢理解了。在xv6中的硬盘布局是这样的：\n1 [ boot block | super block | log | inode blocks | free bit map | data blocks] 在学习操作系统时，我们有学到过一个叫位图的东西，将多个块合并作为一个位图，其中的每一位（bit）用来唯一表示一个数据块是否被使用了（例如0表示为使用，1表示使用）。在xv6中，我们在需要分配数据块时，会去位图中寻找可用的数据块的编号，在去获取对应的数据块：\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 static uint balloc(uint dev) { int b, bi, m; struct buf *bp; bp = 0; // 文件系统总共有 200000 个块（sb.size = 200000），且每个位图块可以管理 8192 个块（BPB = 8192） // 在第一次迭代中，b = 0，读取管理第 0 到第 8191 个块的位图块。 // 内层循环遍历这 8192 个块，寻找空闲块。 // 在第二次迭代中，b = 8192，读取管理第 8192 到第 16383 个块的位图块。 for(b = 0; b \u0026lt; sb.size; b += BPB){ bp = bread(dev, BBLOCK(b, sb)); // 获取在第n次迭代中管理一个BPB大小的位图块 // 遍历这个位图块中的每一位，找到未使用的块号并返回 for(bi = 0; bi \u0026lt; BPB \u0026amp;\u0026amp; b + bi \u0026lt; sb.size; bi++){ m = 1 \u0026lt;\u0026lt; (bi % 8); if((bp-\u0026gt;data[bi/8] \u0026amp; m) == 0){ // Is block free? bp-\u0026gt;data[bi/8] |= m; // Mark block in use. log_write(bp); brelse(bp); bzero(dev, b + bi); return b + bi; } } brelse(bp); } printf(\u0026#34;balloc: out of blocks\\n\u0026#34;); return 0; } 要实现大文件的bmap，我们可以仿照原本的bmap是如何处理一级间接块的：\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 static uint bmap(struct inode *ip, uint bn) { ... // 块号（bn）减去11个，这样是为了方便之后可以按偏移值为0开始计算 bn -= NDIRECT; // 如果bn超出了11个，也就说明需要通过一级间接块去获取 if(bn \u0026lt; NINDIRECT){ // Load indirect block, allocating if necessary. // ip-\u0026gt;addrs[11]代表了一级间接块的地址 if((addr = ip-\u0026gt;addrs[NDIRECT]) == 0){ // 此时还未分配一级间接块，分配 addr = balloc(ip-\u0026gt;dev); if(addr == 0) return 0; ip-\u0026gt;addrs[NDIRECT] = addr; } // 从硬盘中读出这个块 bp = bread(ip-\u0026gt;dev, addr); // 获取这个块中的数据（不过这里只是通过指针来更好的操作） a = (uint*)bp-\u0026gt;data; if((addr = a[bn]) == 0){ // 如果还没有分配bn所表示的数据块，分配 addr = balloc(ip-\u0026gt;dev); if(addr){ a[bn] = addr; log_write(bp); } } brelse(bp); return addr; } ... } 同样的，二级间接块也是类似的操作手法：\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 static uint bmap(struct inode *ip, uint bn) { ... // 如果bn超出了11个，也就说明需要通过一级间接块去获取 if(bn \u0026lt; NINDIRECT){ ... } // 如果运行到了这里，说明要从二级间接块中寻找，这里减去NINDIRECT（256）也是像之前一样方便计算 bn -= NINDIRECT; // 从二级间接块中找 if (bn \u0026lt; NINDIRECT * NINDIRECT) { // inode的二级间接块还未分配 if ((addr = ip-\u0026gt;addrs[NDIRECT + 1]) == 0) { addr = balloc(ip-\u0026gt;dev); if(addr == 0) return 0; ip-\u0026gt;addrs[NDIRECT + 1] = addr; } int level1 = bn / NINDIRECT; int level2 = bn % NINDIRECT; // 读取二级间接块 bp = bread(ip-\u0026gt;dev, addr); a = (uint *)bp-\u0026gt;data; if ((addr = a[level1]) == 0){ a[level1] = addr = balloc(ip-\u0026gt;dev); log_write(bp); // 修改了就要记录日志 } brelse(bp); bp = bread(ip-\u0026gt;dev, addr); a = (uint *)bp-\u0026gt;data; if ((addr = a[level2]) == 0) { a[level2] = addr = balloc(ip-\u0026gt;dev); log_write(bp); // 修改了就要记录日志 } brelse(bp); return addr; } } 多的步骤就是二级间接索引需要多一次读取数据块和查找：\n假设传入bmap的块号为524，那么其不在直接块中，也不在一级间接块中，而是在二级间接块中。通过bmap中的前两个if之后对于bn的减法操作，此时的bn = 524 - 11 - 256 = 257。那么在二级间接块的第一层中（level1），索引为257 / 256 = 1，在第二层中（level2）索引为257 % 256 = 1 。如下图所示：\nSymbolic links 首先要搞清楚软硬链接的区别：\n软链接（符号链接）： 软链接是一种特殊的文件，它包含指向另一个文件或目录的路径。它类似于Windows中的快捷方式。 存储的是目标文件或目录的路径信息。 硬链接： 硬链接是文件系统中的一个普通文件名，只是它与其他文件名指向同一个物理文件数据块。 存储的是目标文件的数据本身，不包含路径信息。 那么要我们实现软链接，我们需要做的事情有两个：\n实现软链接的系统调用 处理使用open打开一个软链接文件 系统调用的实现 实验中让我们实现的系统调用接受连个参数(char *target, char *path)，它在path处创建了一个软链接，该链接指向文件名为target的文件。\n如何注册系统调用啥的这里就不赘述了。\n首先我们需要准备一些宏：\n1 2 3 4 5 // stat.h #define T_SYMLINK 4 // symbolic link // fcntl.h #define O_NOFOLLOW 0x800 实现sys_symlink系统调用：\n我们创建一个软链接时需要使用create新建一个inode，因为软链接也是一个文件；然后调用writei将目标路径的字符串target写入软链接文件的数据块。\n需要注意的是在完成这两步之后需要使用iunlockput(ip)来释放在create中加上的锁。\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 uint64 sys_symlink(void) { char target[MAXPATH], path[MAXPATH]; if (argstr(0, target, MAXPATH) \u0026lt; 0 || argstr(1, path, MAXPATH) \u0026lt; 0) { return -1; } begin_op(); // 软链接是一个特殊的文件，其存储的数据为目标文件的路径信息，因此我们在创建软链接时需要创建一个新的inode struct inode *ip = create(path, T_SYMLINK, 0, 0); if (ip == 0) { end_op(); return -1; } // 将target字符串写入inode的第一个直接块中 if (writei(ip, 0, (uint64)target, 0, strlen(target)) \u0026lt; 0) { end_op(); return -1; } iunlockput(ip); end_op(); return 0; } 处理open一个软链接的情况 这里可能是这个lab的一个难点了。为什么我们需要处理这样的情况呢？\n还记得我们刚刚准备的一个宏O_NOFOLLOW吗，在Linux中，它有这样些特点：\n符号链接检查：当O_NOFOLLOW标志被设置时，如果指定的文件名是一个软链接，打开操作将不会跟随这个链接，而是会失败，并返回一个错误。 错误返回：如果尝试打开一个软链接，并且O_NOFOLLOW标志被设置，系统会返回ELOOP错误，表示遇到了太多的符号链接。 用途：这个标志通常用于安全目的，防止应用程序无意中打开一个软链接，这可能被恶意用户用来绕过安全限制。 例如：\n1 int fd = open(\u0026#34;/path/to/symlink\u0026#34;, O_RDONLY | O_NOFOLLOW); 如果/path/to/symlink是一个软链接，这个调用将失败。\n我们需要在sys_open中处理：当需要打开的文件类型是软链接类型并且没有使用O_NOFOLLOW标志时，递归找到最终的目标文件。为什么说是“递归”呢，因为可能有这样一种情况：一个软链接指向了另一个软链接，甚至如此以往。为了避免“子子孙孙无穷无尽”的情况，我们需要设置一个递归层数的限制，当然，我们这里使用循环来代替递归。\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 uint64 sys_open(void) { ... if(ip-\u0026gt;type == T_DEVICE \u0026amp;\u0026amp; (ip-\u0026gt;major \u0026lt; 0 || ip-\u0026gt;major \u0026gt;= NDEV)){ ... } // 文件的类型是软链接，且没有使用O_NOFOLLOW标志，就递归地查找，直到找到最终的目标文件的inode // 否则，就当作是正常的文件进行打开 if (ip-\u0026gt;type == T_SYMLINK \u0026amp;\u0026amp; !(omode \u0026amp; O_NOFOLLOW)) { // 如果循环10次后找到的文件类型还是软链接，那么说明有错误了 for (int i = 0; i \u0026lt; 10; i++) { // 从软链接文件的inode中读取目标文件的路径字符串 // 注意，在这一步之前已经对ip上锁，因此这里操作完后需要解锁 if (readi(ip, 0, (uint64)path, 0, strlen(path)) \u0026lt; 0) { iunlockput(ip); end_op(); return -1; } iunlockput(ip); ip = namei(path); if (ip == 0) { end_op(); return -1; } ilock(ip); if (ip-\u0026gt;type != T_SYMLINK) { break; } } if (ip-\u0026gt;type == T_SYMLINK) { iunlockput(ip); end_op(); return -1; } } if((f = filealloc()) == 0 || (fd = fdalloc(f)) \u0026lt; 0){ ... } ... } Code Detail 代码实现详情请见Github：\nLarge files Symbolic links Reference https://pdos.csail.mit.edu/6.S081/2023/labs/fs.html https://xv6.dgs.zone/labs/answers/lab9.html Summary 文件系统是一个有意思的东西，同时也很复杂，xv6中设计了一个简化的文件系统，并采用了分层的设计。虽然这里将实验做完了，但是我觉得自己还有很多细节没有搞懂。例如其中的日志层，其是如何实现crash之后能恢复过来的代码是如何设计编写的，还有其中的锁的获取与释放等等。希望之后能结合实际再分析分析。\n","permalink":"https://kerolt.github.io/p/mit6.s081lab9-file-system/","summary":"\u003ch2 id=\"intro\"\u003eIntro\u003c/h2\u003e\n\u003cp\u003e在这个实验中，我们需要让xv6支持更大的文件和软链接。实验总体不是特别难，不过需要我们理解好文件系统是如何工作的。\u003ccode\u003eLab8 lock\u003c/code\u003e中的Buffer Cache也是文件系统的一部分，不过它位于文件系统的下层，这里我们需要处理的更多在上层，偏应用层。\u003c/p\u003e","title":"【MIT6.S081】Lab9 file system"},{"content":"Intro 这个实验个人感觉挺难的，需要我们重新设计数据结构，还要考虑在并发（并行）情况下对于锁的操作，以减少多核情况下对于锁的竞争。其中主要涉及内存分配和IO缓冲块分配，在这个lab之前，xv6对于这两个分配都是使用的全局对象，并只有一把全局锁进行操作，这样的话在并行情况下锁的竞争是很激烈的，我们的任务就是重新设计这两个分配器，它们的重构思路并不完全一致，需要具体问题具体分析。\nMemory Allocator 在xv6中，内存通过kalloc()来分配，在其内部，会使用一个kmem的结构体变量：\n1 2 3 4 5 6 7 8 9 10 // kernel/kalloc.c struct run { struct run *next; }; struct { struct spinlock lock; struct run *freelist; } kmem; kmem.freelist保存着未使用的内存块（每块大小为4KB），在内核初始化时，会通过kinit()将地址KERNBASE ~ PHYSTOP的物理内存（一共128MB）放入freelist中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void kinit() { initlock(\u0026amp;kmem.lock, \u0026#34;kmem\u0026#34;); freerange(end, (void*)PHYSTOP); // end为kernel.ld中定义的，值为0x80000000，即KERNBASE } void freerange(void *pa_start, void *pa_end) { char *p; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE \u0026lt;= (char*)pa_end; p += PGSIZE) kfree(p); // 使用kfree将内存块放入freelist中 } 通过上面的源码分析我们可以看到，整个xv6内核都是通过一个全局的kmem来分配和回收内存块，那么如果当多个CPU（多个core）需要操作内存块时，就必须得用锁才能保证整体的稳定和正确。\n但是这样做又会有一个大问题，那就是一个cpu在操作kmem时，另一个CPU即便想获取内存块或释放内存块，因为锁的缘故也只能等待。故我们的解决方案是为每个CPU都设置一个kmem，这样，哪个CPU需要操作内存块时就可以只锁上它自己的kmem，其他CPU受到的干扰会大大减少。\n为什么说是大大减少而不是完全减少呢？这是因为当某个CPU的kmem.freelist中没有可分配的内存块时，需要去其他CPU的kmem.freelist中去拿一个过来，这时就需要处理好这两个CPU的锁的处理了。\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 struct { struct spinlock lock; struct run *freelist; char lock_name[8]; // 每个kmem的锁的名称 } kmem[NCPU]; // 为每个CPU都设置一个kmem void kinit() { // 初始化时要对每个kmem都初始锁 for (int i = 0; i \u0026lt; NCPU; i++) { initlock(\u0026amp;kmem[i].lock, \u0026#34;kmem\u0026#34;); snprintf(kmem[i].lock_name, sizeof(kmem[i].lock_name), \u0026#34;kmem%d\u0026#34;, i); } // 这里会先将所有内存块都分配给kmem[0]，因为内核启动时是cpus[0]在做初始化操作 // 之后其他CPU需要内存块时，从cpus[0]这里拿 freerange(end, (void*)PHYSTOP); } void kfree(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa \u0026lt; end || (uint64)pa \u0026gt;= PHYSTOP) panic(\u0026#34;kfree\u0026#34;); // Fill with junk to catch dangling refs. memset(pa, 1, PGSIZE); r = (struct run*)pa; // ---------------------------------------- // kfree时只需要将内存块放到自己所属cpu的kmem.freelist中 push_off(); int cid = cpuid(); acquire(\u0026amp;kmem[cid].lock); r-\u0026gt;next = kmem[cid].freelist; kmem[cid].freelist = r; release(\u0026amp;kmem[cid].lock); pop_off(); // ---------------------------------------- } void * kalloc(void) { struct run *r; // ---------------------------------------- push_off(); int cid = cpuid(); acquire(\u0026amp;kmem[cid].lock); r = kmem[cid].freelist; if (r) { // 如果当前的freelist中还有内存块，则直接用 kmem[cid].freelist = r-\u0026gt;next; } else { // 没有？那就拿！ // 遍历一下其他CPU的kmem，如果找到的freelist中还有内存块，就拿它的 for (int next_cid = 0; next_cid \u0026lt; NCPU; next_cid++) { // 不找自己 if (next_cid == cid) continue; acquire(\u0026amp;kmem[next_cid].lock); r = kmem[next_cid].freelist; if (r) { kmem[next_cid].freelist = r-\u0026gt;next; release(\u0026amp;kmem[next_cid].lock); break; } release(\u0026amp;kmem[next_cid].lock); } } release(\u0026amp;kmem[cid].lock); pop_off(); // ---------------------------------------- if(r) memset((char*)r, 5, PGSIZE); // fill with junk return (void*)r; } Buffer Cache 先介绍下实验初始的bcache：\n1 2 3 4 5 6 7 8 9 struct { struct spinlock lock; struct buf buf[NBUF]; // Linked list of all buffers, through prev/next. // Sorted by how recently the buffer was used. // head.next is most recent, head.prev is least. struct buf head; } bcache; struct spinlock lock：bcache的全局锁 struct buf buf[NBUF]：缓冲块池，即包含了所有的buffer cache struct buf head：一个LRU链表，用于操作缓冲块，使用head.next获取的是最近刚使用过的buffer，head.prev获取的是最近未使用时间最久的buffer（或者说是未被使用的buffer，即引用计数为0） 原来的bcache中的buf数组在binit时就给LRU链表初始化用了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void binit(void) { struct buf *b; initlock(\u0026amp;bcache.lock, \u0026#34;bcache\u0026#34;); // Create linked list of buffers bcache.head.prev = \u0026amp;bcache.head; bcache.head.next = \u0026amp;bcache.head; for(b = bcache.buf; b \u0026lt; bcache.buf+NBUF; b++){ b-\u0026gt;next = bcache.head.next; b-\u0026gt;prev = \u0026amp;bcache.head; initsleeplock(\u0026amp;b-\u0026gt;lock, \u0026#34;buffer\u0026#34;); bcache.head.next-\u0026gt;prev = b; bcache.head.next = b; } } 而这样设计的buffer cache有一个问题，由于buffer cache只有一个全局锁，当在高并发的情况下，进程需要并发访问bcache时，无法达到多进程带来的优势，一个进程必须要先等前一个进程释放锁后才可以操作。\n考虑前一个实验中的kalloc，我们能否也使用那样的策略？\nReducing contention in the block cache is more tricky than for kalloc, because bcache buffers are truly shared among processes (and thus CPUs). For kalloc, one could eliminate most contention by giving each CPU its own allocator; that won\u0026rsquo;t work for the block cache.\n为什么这么说呢？是因为block cache并不像内存页那样具有通用性。block cache对应着真实的物理外存块，每一个CPU都可能使用同一个block cache，所以无法像kalloc中那样为每个CPU分配其对应的cache。\n既然我们无法分别为每个CPU分配，那么我们可以换一个角度，对所有的block块进行分组：我们可以创建一个哈希桶hash buckets（这里使用的容量为13），那么对每个block块的块号进行取余操作（mod 13）即可将它们映射到其中一个哈希桶中。这样一来，我们需要使用标号为blockno的块时，到其映射的哈希桶中去寻找即可，并且，在并发情况下，我们一般上锁的单位从整个bcache变为了其中的一个桶，锁的粒度大大减小了。\n那么我们修改一下bcache的数据结构：\n1 2 3 4 5 6 7 8 9 10 11 #define NBUCKET 13 #define BLOCK_HASH(blockno) (blockno % NBUCKET) struct { struct spinlock g_lock; // 全局锁，这里相较之前只是改了个名字 struct buf buf[NBUF]; struct spinlock bk_lock[NBUCKET]; // 每个hash bucket都对应有一把锁 struct buf bucket[NBUCKET]; // hash bucket int size; // 缓冲块池（buf[NBUF]）中已使用的块数 } bcache; 我们在重构的方案中，不再使用LRU链表，但还是使用了LRU算法的思想，只是用时间戳来代替LRU链表：\n1 2 3 4 5 6 7 8 9 10 11 12 struct buf { int valid; // has data been read from disk? int disk; // does disk \u0026#34;own\u0026#34; buf? uint dev; uint blockno; struct sleeplock lock; uint refcnt; // struct buf *prev; // LRU cache list struct buf *next; uchar data[BSIZE]; uint timestamp; }; 这个时间戳通过一个全局变量ticks（trap.c）来获取：\n1 extern uint ticks; 那么在初始化bcache时，我们只需要初始化其中的锁和bcache.size即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void binit(void) { struct buf *b; bcache.size = 0; initlock(\u0026amp;bcache.g_lock, \u0026#34;bcache\u0026#34;); for (int i = 0; i \u0026lt; NBUCKET; i++) { initlock(\u0026amp;bcache.bk_lock[i], \u0026#34;bk_lock\u0026#34;); } for(b = bcache.buf; b \u0026lt; bcache.buf+NBUF; b++){ initsleeplock(\u0026amp;b-\u0026gt;lock, \u0026#34;buffer\u0026#34;); } } 由于我们对于锁的操作单位变为了bucket，所以下面两个函数也需要修改：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void bpin(struct buf *b) { int idx = BLOCK_HASH(b-\u0026gt;blockno); acquire(\u0026amp;bcache.bk_lock[idx]); b-\u0026gt;refcnt++; release(\u0026amp;bcache.bk_lock[idx]); } void bunpin(struct buf *b) { int idx = BLOCK_HASH(b-\u0026gt;blockno); acquire(\u0026amp;bcache.bk_lock[idx]); b-\u0026gt;refcnt--; release(\u0026amp;bcache.bk_lock[idx]); } 在原先的释放buffer块的brelse(struct buf *b)函数中，会先对b的引用计数-1，当其值为0时，将其转移至LRU链表的head-\u0026gt;prev位置，说明这个buffer没有被任何进程使用了。而在重构方案中，我们使用时间戳代替了LRU链表，每个bucket中我们并没有维护一个LRU链表，而是认为引用计数为0且时间戳最小的的buffer是没有被任何进程使用的，故在引用计数为0时，只需要更新buffer的时间戳即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void brelse(struct buf *b) { if(!holdingsleep(\u0026amp;b-\u0026gt;lock)) panic(\u0026#34;brelse\u0026#34;); releasesleep(\u0026amp;b-\u0026gt;lock); int idx = BLOCK_HASH(b-\u0026gt;blockno); acquire(\u0026amp;bcache.bk_lock[idx]); b-\u0026gt;refcnt--; if (b-\u0026gt;refcnt == 0) { // no one is waiting for it. b-\u0026gt;timestamp = ticks; // 未使用的buffer的时间戳一定是最小的 } release(\u0026amp;bcache.bk_lock[idx]); } 对于bcache最重要的bget()函数，我们重构的思路为：\n检查标号为blockno的块的缓存是否在cache中，如果在，则其引用计数+1，返回该缓存块 在缓存中没有找到，先在缓冲块池中寻找还未分配给bucket的缓存块 如果缓冲块池中都分配出去了，就到每个bucket中去找。如果当前遍历的bucket中有buffer的引用计数为0，拿到其中时间戳最小的buffer（这也就是说这个buffer是距离现在最久没有使用的合法块） 如果这个buffer在原先的bucket中，返回这个buffer 否则需要将这个buffer从当前bucket中转移到目标bucket中 这是一个大致的思路，具体细节参考如下代码：\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 static struct buf* bget(uint dev, uint blockno) { struct buf *b; int bucket_idx = BLOCK_HASH(blockno); // 先对blockno对应的bucket上锁即可 acquire(\u0026amp;bcache.bk_lock[bucket_idx]); // Is the block already cached? for(b = \u0026amp;bcache.bucket[bucket_idx]; b; b = b-\u0026gt;next){ if(b-\u0026gt;dev == dev \u0026amp;\u0026amp; b-\u0026gt;blockno == blockno){ b-\u0026gt;refcnt++; release(\u0026amp;bcache.bk_lock[bucket_idx]); acquiresleep(\u0026amp;b-\u0026gt;lock); return b; } } // 在缓存中没有找到，先在缓冲块池中寻找还未分配给bucket的缓存块 // 需要使用全局锁来保证bcache.size++的原子性 // 这里也是为什么buffer时间戳不需要初始化的原因：每一次需要使用buffer时（会调用bget）， // 如果buffer池中还有空闲buffer，则会直接使用这个buffer，完成操作后会调用relese释放buffer，此时就会更新buffer的时间戳 acquire(\u0026amp;bcache.g_lock); if (bcache.size \u0026lt; NBUF) { struct buf *b = \u0026amp;bcache.buf[bcache.size++]; b-\u0026gt;next = bcache.bucket[bucket_idx].next; bcache.bucket[bucket_idx].next = b; b-\u0026gt;dev = dev; b-\u0026gt;blockno = blockno; b-\u0026gt;valid = 0; b-\u0026gt;refcnt = 1; release(\u0026amp;bcache.g_lock); acquiresleep(\u0026amp;b-\u0026gt;lock); return b; } release(\u0026amp;bcache.g_lock); // 在这时才能释放该bucket的锁，假设在检查缓存是否存在后释放： // 如果有两个进程1和2，此时缓冲块池还有多个未分配的块，进程1检查bucket，发现没有缓存，释放锁，准备去缓冲块池中拿 // 此时切换到进程2，进程2检查bucket,发现没有缓存，也去缓冲块池中拿 // 这样就会导致bucket会添加两个blockno的缓冲块 release(\u0026amp;bcache.bk_lock[bucket_idx]); // 在每个bucket中去找可用的buffer cache for (int i = 0; i \u0026lt; NBUCKET; i++) { struct buf *cur_buf, *pre_buf, *min_buf, *min_pre_buf; uint min_timestamp = -1; acquire(\u0026amp;bcache.bk_lock[bucket_idx]); pre_buf = \u0026amp;bcache.bucket[bucket_idx]; cur_buf = pre_buf-\u0026gt;next; // 遍历bcache.bucket[bucket_idx] while (cur_buf) { // 为什么这里需要重新检查？考虑这样一种情况： // 假设缓冲块池中还有一个未分配的，此时有两个进程，进程1和2都需要访问同一个标号blockno的块 // 进程1先拿到这个未分配的，并将其放入了对应的bucket中 // 之后进程2发现池中没有未分配的了，开始遍历所有bucket， // 如果这时不重新检查一下blockno对应的bucket，则会导致一个bucket中有两个blockno的块 if (bucket_idx == BLOCK_HASH(blockno) \u0026amp;\u0026amp; cur_buf-\u0026gt;blockno == blockno \u0026amp;\u0026amp; cur_buf-\u0026gt;dev == dev) { cur_buf-\u0026gt;refcnt++; release(\u0026amp;bcache.bk_lock[bucket_idx]); acquiresleep(\u0026amp;cur_buf-\u0026gt;lock); return cur_buf; } // 只有引用计数为0,并且时间戳最小的缓冲块才可被重新分配 if (cur_buf-\u0026gt;refcnt == 0 \u0026amp;\u0026amp; cur_buf-\u0026gt;timestamp \u0026lt; min_timestamp) { min_pre_buf = pre_buf; min_buf = cur_buf; min_timestamp = cur_buf-\u0026gt;timestamp; } pre_buf = cur_buf; cur_buf = cur_buf-\u0026gt;next; } // 在本轮中找到了可重新分配的缓冲块 if (min_buf) { min_buf-\u0026gt;dev = dev; min_buf-\u0026gt;blockno = blockno; min_buf-\u0026gt;valid = 0; min_buf-\u0026gt;refcnt = 1; // 是自身bucket中的，不用做转移操作 if (bucket_idx == BLOCK_HASH(blockno)) { // release(\u0026amp;bcache.hash_lock); release(\u0026amp;bcache.bk_lock[bucket_idx]); acquiresleep(\u0026amp;min_buf-\u0026gt;lock); return min_buf; } // 是其他bucket中的，需要转移 // 先将目标bucket中的buffer移除，然后释放锁 min_pre_buf-\u0026gt;next = min_buf-\u0026gt;next; release(\u0026amp;bcache.bk_lock[bucket_idx]); // 接着获取blockno对应的锁，并将从目标bucket中移除的buffer放至blockno对应的bucket，返回该buffer bucket_idx = BLOCK_HASH(blockno); acquire(\u0026amp;bcache.bk_lock[bucket_idx]); min_buf-\u0026gt;next = bcache.bucket[bucket_idx].next; bcache.bucket[bucket_idx].next = min_buf; release(\u0026amp;bcache.bk_lock[bucket_idx]); acquiresleep(\u0026amp;min_buf-\u0026gt;lock); return min_buf; } release(\u0026amp;bcache.bk_lock[bucket_idx]); // 如果到底了，从头来，保证遍历每一个bucket if (++bucket_idx == NBUCKET) { bucket_idx = 0; } } panic(\u0026#34;bget: no buffers\u0026#34;); } bget是这个lab中最复杂的地方，其处理逻辑虽然较好理解，但是多个锁的操作顺序很让人头疼，需要考虑到多种并发情况。\nCode Details 代码实现详情请见Github：https://github.com/kerolt/xv6-labs-2023/commit/ccac48e8ae6b6dde3e7c747a77f4149232420901\nReference https://github.com/whileskies/xv6-labs-2020/blob/main/doc/Lab8-locks.md https://blog.csdn.net/LostUnravel/article/details/121430900 ","permalink":"https://kerolt.github.io/p/mit6.s081lab8-lock/","summary":"\u003ch2 id=\"intro\"\u003eIntro\u003c/h2\u003e\n\u003cp\u003e这个实验个人感觉挺难的，需要我们重新设计数据结构，还要考虑在并发（并行）情况下对于锁的操作，以减少多核情况下对于锁的竞争。其中主要涉及内存分配和IO缓冲块分配，在这个lab之前，xv6对于这两个分配都是使用的全局对象，并只有一把全局锁进行操作，这样的话在并行情况下锁的竞争是很激烈的，我们的任务就是重新设计这两个分配器，它们的重构思路并不完全一致，需要具体问题具体分析。\u003c/p\u003e","title":"【MIT6.S081】Lab8 lock"},{"content":"在做thread lab的时候，阅读xv6的源码后对于进程调度的实现有了大致的了解，但是其中锁的获取与释放顺序让我困惑了好久：在yield函数中，不是先获取了进程p的锁吗，那么之后在调度器中又获取p的锁，那不是会死锁吗？在调度器内使用swtch发生进程切换后，又会跳转到哪里？\n而在我观摩大佬的一些博客和视频后，发现我之前的想法有很大的问题，归根结底是没有弄明白xv6何时发生了切换，切换后应该从哪里开始运行。这篇笔记就是对于分析xv6进程调度的总结。\n在使用allocproc创建进程时，会为创建的proc设置ra的值，初始会设置为forkret函数的地址。\n1 2 3 4 5 6 7 8 9 10 static struct proc* allocproc(void) { ... memset(\u0026amp;p-\u0026gt;context, 0, sizeof(p-\u0026gt;context)); p-\u0026gt;context.ra = (uint64)forkret; p-\u0026gt;context.sp = p-\u0026gt;kstack + PGSIZE; return p; } 在调度器scheduler开始运行后，遍历进程表，先对遍历到的当前进程p上锁，如果这个进程是可运行的（RUNNABLE），则执行swtch函数切换上下文，保存当前cpu的上下文，同时加载p的上下文到寄存器中。\n执行完swtch后会跳转到ra寄存器所保存的地址，也就是这个进程初始设置的forkret的位置，这个函数中会释放在scheduler中对进程p上的锁。如果这个函数是第一次执行，那么会初始化文件系统。之后执行usertrapret函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // A fork child\u0026#39;s very first scheduling by scheduler() // will swtch to forkret. void forkret(void) { static int first = 1; // Still holding p-\u0026gt;lock from scheduler. release(\u0026amp;myproc()-\u0026gt;lock); if (first) { // File system initialization must be run in the context of a // regular process (e.g., because it calls sleep), and thus cannot // be run from main(). fsinit(ROOTDEV); first = 0; // ensure other cores see first=0. __sync_synchronize(); } usertrapret(); } usertrapret中，将会关闭中断，最后这会让当前进程从内核态返回到用户态（回到用户空间），切换用户页表并恢复用户态寄存器。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // // return to user space // void usertrapret(void) { ... // we\u0026#39;re about to switch the destination of traps from // kerneltrap() to usertrap(), so turn off interrupts until // we\u0026#39;re back in user space, where usertrap() is correct. intr_off(); ... // jump to userret in trampoline.S at the top of memory, which // switches to the user page table, restores user registers, // and switches to user mode with sret. uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline); ((void (*)(uint64))trampoline_userret)(satp); } 在之后如果该进程会触发中断（例如时间片轮转调度中执行时间到了后触发时钟中断）或者使用了系统调用，将会执行usertrap函数（也即处理陷入），在其中会执行yield函数来让该进程让出cpu，并进行进程切换。\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 // // handle an interrupt, exception, or system call from user space. // called from trampoline.S // void usertrap(void) { ... if(r_scause() == 8){ // system call ... } else if((which_dev = devintr()) != 0){ // ok } else { printf(\u0026#34;usertrap(): unexpected scause %p pid=%d\\n\u0026#34;, r_scause(), p-\u0026gt;pid); printf(\u0026#34; sepc=%p stval=%p\\n\u0026#34;, r_sepc(), r_stval()); setkilled(p); } if(killed(p)) exit(-1); // give up the CPU if this is a timer interrupt. if(which_dev == 2) yield(); usertrapret(); } yield函数会先对当前进程p上锁，然后执行shed函数，shed函数会先进行一些检查，然后调用swtch函数进行上下文切换，其保存p的上下文，并加载之前cpu的上下文。这样，ra寄存器中的值就变成了scheduler中的swtch所在地址的后一条指令的地址。在schduler中，重置当前cpu所运行的进程后，释放进程p的锁，之后开启新一轮调度（遍历进程表找到下一个可运行的进程）。\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 void sched(void) { int intena; struct proc *p = myproc(); if(!holding(\u0026amp;p-\u0026gt;lock)) panic(\u0026#34;sched p-\u0026gt;lock\u0026#34;); if(mycpu()-\u0026gt;noff != 1) panic(\u0026#34;sched locks\u0026#34;); if(p-\u0026gt;state == RUNNING) panic(\u0026#34;sched running\u0026#34;); if(intr_get()) panic(\u0026#34;sched interruptible\u0026#34;); intena = mycpu()-\u0026gt;intena; swtch(\u0026amp;p-\u0026gt;context, \u0026amp;mycpu()-\u0026gt;context); mycpu()-\u0026gt;intena = intena; } // Give up the CPU for one scheduling round. void yield(void) { struct proc *p = myproc(); acquire(\u0026amp;p-\u0026gt;lock); p-\u0026gt;state = RUNNABLE; sched(); release(\u0026amp;p-\u0026gt;lock); } 这里yield中使用了acquire和release函数来进行锁的操作，但是这个锁的获取与释放时机并不是如同代码中的这样是一个“顺序”的过程。\n当执行顺序为从调度器选择可调度进程（RUNNABLE）后进行进程切换： 当执行顺序为进程触发中断等陷入时，放弃cpu资源，需要执行进程调度时： 可以看到，在yield函数中，进程p的锁的使用顺序并不是acquire() 获取锁-\u0026gt; 执行shed()函数 -\u0026gt; release()释放锁这样线性的，在其中会发生进程切换因此锁的“获取、释放”逻辑并不发生在一个函数中。\n","permalink":"https://kerolt.github.io/p/mit6.s081xv6%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%88%86%E6%9E%90/","summary":"\u003cp\u003e在做thread lab的时候，阅读xv6的源码后对于进程调度的实现有了大致的了解，但是其中锁的获取与释放顺序让我困惑了好久：在yield函数中，不是先获取了进程p的锁吗，那么之后在调度器中又获取p的锁，那不是会死锁吗？在调度器内使用swtch发生进程切换后，又会跳转到哪里？\u003c/p\u003e\n\u003cp\u003e而在我观摩大佬的一些博客和视频后，发现我之前的想法有很大的问题，归根结底是没有弄明白xv6何时发生了切换，切换后应该从哪里开始运行。这篇笔记就是对于分析xv6进程调度的总结。\u003c/p\u003e","title":"【MIT6.S081】xv6进程调度分析"},{"content":"本次的实验总体都不是很难，第一个练习让我们在用户态模拟了线程的切换，这里重要的就是进程/线程上下文的保存与恢复；第二三个练习则是让我们跳出了xv6，去熟悉pthread库和线程的同步互斥。\n简单分析xv6中的进程切换 init是用户态最先启动的进程，其启动后会创建sh进程，sh进程又会fork并exec其他命令：\n内核会在userinit()中准备init进程，这是最先启动的用户态进程，将其运行状态设置为RUNNABLE 内核启动scheduler进行进程调度（这个函数会一直执行），当前只有init进程，且其状态为RUNNABLE，故运行 init进程会fork一个子进程，并使用exec执行sh进程，其用于处理用户在控制台的输入 用户输入命令后，sh解析命令后fork子进程并用exec来将命令对应的进程替换掉fork的子进程 1 2 3 4 5 init - sh - ls - cat - ... xv6内部采用时间片轮转来进行进程调度，当进程时间片到了后，触发时钟中断，执行yield，在yield中，会将当前进程的状态从运行态RUNNING设置为RUNNABLE，并切换至scheduler调度器，由其来寻找下一个可运行的进程进行切换。\n那么xv6是怎么切换当前进程和scheduler调度器的呢？其实现是根据swtch函数来实现的：\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 // Switch to scheduler. Must hold only p-\u0026gt;lock // and have changed proc-\u0026gt;state. Saves and restores // intena because intena is a property of this // kernel thread, not this CPU. It should // be proc-\u0026gt;intena and proc-\u0026gt;noff, but that would // break in the few places where a lock is held but // there\u0026#39;s no process. void sched(void) { int intena; struct proc *p = myproc(); if(!holding(\u0026amp;p-\u0026gt;lock)) panic(\u0026#34;sched p-\u0026gt;lock\u0026#34;); if(mycpu()-\u0026gt;noff != 1) panic(\u0026#34;sched locks\u0026#34;); if(p-\u0026gt;state == RUNNING) panic(\u0026#34;sched running\u0026#34;); if(intr_get()) panic(\u0026#34;sched interruptible\u0026#34;); intena = mycpu()-\u0026gt;intena; swtch(\u0026amp;p-\u0026gt;context, \u0026amp;mycpu()-\u0026gt;context); mycpu()-\u0026gt;intena = intena; } // Give up the CPU for one scheduling round. void yield(void) { struct proc *p = myproc(); acquire(\u0026amp;p-\u0026gt;lock); p-\u0026gt;state = RUNNABLE; sched(); release(\u0026amp;p-\u0026gt;lock); } swtch(\u0026amp;p-\u0026gt;context, \u0026amp;mycpu()-\u0026gt;context)函数会将当前寄存器中的值保存到p-\u0026gt;context中，并将当前cpu中context的内容恢复到寄存器中，其中最重要的就是ra寄存器，当恢复了这个后，ra的值对应的地址在调度器scheduler()中：\n（这里ra的16进制为0x8000141e）\n这样，xv6就从：执行当前进程 -\u0026gt; yield -\u0026gt; shed -\u0026gt; scheduler；在scheduler调度器中，内核会找到下一个就绪态的进程，并使用swtch进行进程切换：\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 // Per-CPU process scheduler. // Each CPU calls scheduler() after setting itself up. // Scheduler never returns. It loops, doing: // - choose a process to run. // - swtch to start running that process. // - eventually that process transfers control // via swtch back to the scheduler. void scheduler(void) { struct proc *p; struct cpu *c = mycpu(); c-\u0026gt;proc = 0; for(;;){ // The most recent process to run may have had interrupts // turned off; enable them to avoid a deadlock if all // processes are waiting. intr_on(); for(p = proc; p \u0026lt; \u0026amp;proc[NPROC]; p++) { acquire(\u0026amp;p-\u0026gt;lock); if(p-\u0026gt;state == RUNNABLE) { // Switch to chosen process. It is the process\u0026#39;s job // to release its lock and then reacquire it // before jumping back to us. p-\u0026gt;state = RUNNING; c-\u0026gt;proc = p; swtch(\u0026amp;c-\u0026gt;context, \u0026amp;p-\u0026gt;context); // Process is done running for now. // It should have changed its p-\u0026gt;state before coming back. c-\u0026gt;proc = 0; } release(\u0026amp;p-\u0026gt;lock); } } } 这样，xv6又从调度器切换到了下一个进程去执行了。\nUthread: switching between threads 在uthread.c中，我们需要在用户态下模拟了线程的运行与切换。可以简单理解，运行一个线程就是执行了一个函数。\n源代码中已经给出要运行的线程（函数），如何启动它们并对他们进行切换呢？\n前置准备 线程的切换同进程的切换相似，需要保存通用寄存器的值，所以我们可以对内核照猫画虎般在用户线程struct thread中添加一个struct context变量：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct context { uint64 ra; uint64 sp; // callee-saved uint64 s0; uint64 s1; uint64 s2; uint64 s3; uint64 s4; uint64 s5; uint64 s6; uint64 s7; uint64 s8; uint64 s9; uint64 s10; uint64 s11; }; struct thread { char stack[STACK_SIZE]; /* the thread\u0026#39;s stack */ int state; /* FREE, RUNNING, RUNNABLE */ struct context t_context; }; 启动 risc-v中的ra寄存器的作用是保存函数返回地址。具体来说，当一个函数被调用时，调用指令会将返回地址，即调用该函数的下一条指令的地址，保存在ra寄存器中。\n在实验中，我们通过thread_create来创建线程，那么，在执行玩thread_create之后并进行了调度后，是不是就该运行线程函数了？没错，那么我们需要保存创建的线程对应的函数的地址到ra寄存器中，同时，还需要设置线程的栈指针：\n1 2 3 4 5 6 7 8 9 10 11 12 13 void thread_create(void (*func)()) { struct thread *t; for (t = all_thread; t \u0026lt; all_thread + MAX_THREAD; t++) { if (t-\u0026gt;state == FREE) break; } t-\u0026gt;state = RUNNABLE; // YOUR CODE HERE t-\u0026gt;t_context.ra = (uint64)func; t-\u0026gt;t_context.sp = (uint64)t-\u0026gt;stack + STACK_SIZE; } 调度 即切换线程，我们需要保存当前线程的通用寄存器，并将下一个要运行的线程的通用寄存器值恢复到硬件上，如同进程切换那样：\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 # uthread_switch.S thread_switch: /* YOUR CODE HERE */ sd ra, 0(a0) sd sp, 8(a0) sd s0, 16(a0) sd s1, 24(a0) sd s2, 32(a0) sd s3, 40(a0) sd s4, 48(a0) sd s5, 56(a0) sd s6, 64(a0) sd s7, 72(a0) sd s8, 80(a0) sd s9, 88(a0) sd s10, 96(a0) sd s11, 104(a0) ld ra, 0(a1) ld sp, 8(a1) ld s0, 16(a1) ld s1, 24(a1) ld s2, 32(a1) ld s3, 40(a1) ld s4, 48(a1) ld s5, 56(a1) ld s6, 64(a1) ld s7, 72(a1) ld s8, 80(a1) ld s9, 88(a1) ld s10, 96(a1) ld s11, 104(a1) ret /* return to ra */ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // uthread.c void thread_schedule(void) { ... if (current_thread != next_thread) { /* switch threads? */ next_thread-\u0026gt;state = RUNNING; t = current_thread; current_thread = next_thread; /* YOUR CODE HERE * Invoke thread_switch to switch from t to next_thread: * thread_switch(??, ??); */ thread_switch((uint64)\u0026amp;t-\u0026gt;t_context, (uint64)\u0026amp;next_thread-\u0026gt;t_context); } else next_thread = 0; } Using threads 如果只是简单在get和put操作中加上锁，那么这样虽然能够保证对共享资源的互斥访问，当时无法达到双线程所带来的性能提升。\n应该实现的是两个线程，你放你的，我放我的，现在有5个entry table，我们可以不止使用一把锁来锁住5个table，而是使用5把锁，哪一个table需要互斥操作时只锁它一个就行，这样就细化了锁的粒度。\n这里需要把table改为一个数组，实现代码比较简单，这里就不放了。\nBarrier 实验的要求是每来一个线程运行到barrier就阻塞，直到所有线程都运行到了barrier才释放。这里是想让我们熟悉pthread库中关于锁和条件变量的使用，更加了解线程的同步关系。\n我们可以给全局的bstate中添加一个变量count用于记录当前轮次有多少个线程已经到达了barrier，只要有线程到达，执行++bstate.count，需要注意的是，这个全局变量会被多个线程使用，故需要使用锁来保证互斥访问。\n接下来判断count的值是否等于线程的数量，如果不相等，就wait阻塞当前线程，并释放锁；否则，说明一轮结束，将count置0，轮次+1，并使用broadcaset唤醒被阻塞的线程。\n尝试1 按刚刚说的思路，如果这么写代码，将会造成系统程序死锁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 static void barrier() { pthread_mutex_lock(\u0026amp;bstate.barrier_mutex); ++bstate.count; pthread_mutex_unlock(\u0026amp;bstate.barrier_mutex); if (bstate.count == bstate.nthread) { bstate.count = 0; ++bstate.round; pthread_cond_broadcast(\u0026amp;bstate.barrier_cond); return; } pthread_cond_wait(\u0026amp;bstate.barrier_cond, \u0026amp;bstate.barrier_mutex); } 当条件变量 cond 变为真且该线程被唤醒时，pthread_cond_wait 会自动重新获取互斥锁 mutex，然后返回。\n线程1第0轮获得锁，++count后释放锁，然后在wait处阻塞，并释放锁 接着线程2在第0轮获得锁，++count后进入if判断，使用broadcast唤醒线程1 线程1被唤醒，并自动获取锁 线程2进入第1轮，尝试获得锁，但是此时线程1正持有锁，故线程2阻塞 线程1进入第1轮，但之前自己已经获得锁了，此时又请求锁，故阻塞 两个线程都阻塞，程序出现死锁 尝试2 既然pthread_cond_wait会自动重新获取互斥锁，那就在调用之后使用pthread_mutex_unlock释放锁不久好了吗？\n1 2 3 4 5 6 7 8 9 10 11 12 13 static void barrier() { pthread_mutex_lock(\u0026amp;bstate.barrier_mutex); if (++bstate.count == bstate.nthread) { bstate.count = 0; ++bstate.round; pthread_cond_broadcast(\u0026amp;bstate.barrier_cond); return; } pthread_cond_wait(\u0026amp;bstate.barrier_cond, \u0026amp;bstate.barrier_mutex); pthread_mutex_unlock(\u0026amp;bstate.barrier_mutex); } 这样还是不行：原因是pthread_cond_broadcast不会释放锁。\n在第0轮线程1拿到锁后++count后进入wait状态，此时线程1阻塞并释放锁 在第0轮线程2拿到锁，执行++count操作后使用broadcast尝试唤醒线程1，但是线程2并没有释放锁 线程1抢到CPU，但是拿不到锁，继续阻塞 线程2抢到了CPU的执行权进入第1轮，企图获得锁，但是之前自己没有释放锁，也阻塞 两个线程都阻塞，程序出现死锁 正确做法 所以正确的做法是在broadcast后也使用unlock释放锁：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 static void barrier() { pthread_mutex_lock(\u0026amp;bstate.barrier_mutex); if (++bstate.count == bstate.nthread) { bstate.count = 0; ++bstate.round; pthread_cond_broadcast(\u0026amp;bstate.barrier_cond); pthread_mutex_unlock(\u0026amp;bstate.barrier_mutex); return; } pthread_cond_wait(\u0026amp;bstate.barrier_cond, \u0026amp;bstate.barrier_mutex); pthread_mutex_unlock(\u0026amp;bstate.barrier_mutex); } 代码实现详情 请见Github：https://github.com/kerolt/xv6-labs-2023/tree/thread\nReference https://www.cnblogs.com/looking-for-zihuatanejo/p/17682582.html https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec11-thread-switching-robert/11.7-xv6-switch-function https://www.bilibili.com/video/BV1bZ421U75W/?spm_id_from=333.999.0.0\u0026amp;vd_source=e7bb0cfb7224c8d6671fa62c0e80c832 https://pdos.csail.mit.edu/6.S081/2023/labs/thread.html ","permalink":"https://kerolt.github.io/p/mit6.s081lab6-multithreading/","summary":"\u003cp\u003e本次的实验总体都不是很难，第一个练习让我们在用户态模拟了线程的切换，这里重要的就是进程/线程上下文的保存与恢复；第二三个练习则是让我们跳出了xv6，去熟悉pthread库和线程的同步互斥。\u003c/p\u003e","title":"【MIT6.S081】Lab6 multithreading"},{"content":" 该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\n太菜了，从没打过这么艰难的仗QAQ。由于课程的要求不能公开源代码，所以网上的资源会少很多，平台上的测试案例比较全面，有的还比较刁钻，需要考虑到可拓展哈希的实现细节。在自认为写完了后，提交了近40来次总有几个测试集过不了，还好没有崩溃，在看了几篇博客的方法后，加上自己画图理解，最后终于过了😭。不过回头写博客的时候再去看代码，也没有特别的复杂，还是得明白其中的算法逻辑是如何实现的。\nTask1 - Read/Write Page Guards 简单来说，就是为Page实现一个RAII来自动管理资源。因为在BufferPoolManager::Unpin中，每次调用这个函数，都会让对应的page的pin_count_ - 1，当这个值为0时，这个page就可以被回收，或者说被替换了。但如果我们忘记去手动调用，该页面将永远不会被逐出缓冲池。由于缓冲池以更少的帧数运行，磁盘内外的页面交换将更多。不仅性能受到影响，而且很难检测到错误。\n主要需要考虑如何编写移动构造、移动赋值的逻辑。移动了一个对象后，原来的对象的资源应该转移到了新对象上，那么原来的对象无法再访问资源（将原来对象的资源重置nullptr或清空）。\n还有一个Drop()的接口，这是提供给使用者的释放资源的api，在实现虚构函数时可以直接调用它。Drop的实现就是调用Unpin，然后置空资源。需要注意的是，在进行移动赋值时，一开始也要Drop一下，考虑这样一种情况：\n1 2 auto p = std::move(basic_page_guard); p = std::move(basic_page_guard2); 这个时候同一个变量p接管了两个page，那么应该在第二个移动赋值时先drop掉第一个，因为第一个page不再使用了，自然要Unpin。\n还有就是在三个page guard类重载移动赋值时，如果需要移动的对象和自身是同一个，那么直接返回自己就好：\n1 2 3 4 5 6 auto BasicPageGuard::operator=(BasicPageGuard \u0026amp;\u0026amp;that) noexcept -\u0026gt; BasicPageGuard \u0026amp; { if (\u0026amp;that == this) { return *this; } // ... 其他操作 } 在ReadPageGuard和WritePageGuard的Drop()中，还需要考虑释放管理的page的锁。对应的，锁的获取发生在FetchPageWrite()和FetchPageWrite()中。\nTask2 - Extendible Hash Table Pages 为什么我们需要可扩展哈希？\n下图来源：https://www.bilibili.com/video/BV1Qt421w7JT\n在bustub的设计中，Header Page，Directory Page和Bucket Page都是无法直接构造出来的，即不能通过构造函数创建，只能通过各自的PageGuard中的As()或者AsMut()函数来转换。\nHeader header page中有一个max_depth_的成员变量，1 \u0026lt;\u0026lt; max_depth_即为header page中能存放的目录的索引的数量。当我们有值需要放入哈希表时，获取hash(key)的二进制最高max_depth_位作为索引，再从header中对应位置去找到directory。对应的ExtendibleHTableHeaderPage中的功能实现并不难。\nDirectory directory中有两个depth：\nGlobal Depth：若global depth为n，那么这个Directory就有2^n个entry（相当于指向2^n个bucket） Local Depth：若local depth为n，则在这个对应的bucket下，每个元素的key的最后n位都相同 类似header中获取下一级页的索引，directory获取hash(key)的二进制最低global_depth_位作为索引。那local depth的作用是什么呢？\n这就要说到可拓展哈希中的插入和删除操作了。简单来说，在可拓展哈希表中，目录directory的大小是可以变化的（只要不超过最大容量限制）。目录中可能有多个entry映射到同一个bucket。当需要插入时，如果这个bucket还没有满时，可以直接插入；否则，需要将这个bucket分裂成两个bucket（或者说，将这个bucket中的一部分移动到另一个bucket中），并且这个bucket对应的local depth + 1，这样相比之前就多了一位二进制位去识别bucket。具体的用法可以看后面的Task3部分的笔记。\n下面讲几个稍微难懂的函数：\nGetGlobalDepthMask 这个函数的作用是获取global_depth_个二进制1，举个例子，如果global depth是2，那么说明这个目录当前的容量为2^2=4，索引为0~3，用两位二进制就能表示。\n主要用于和hash(key)进行\u0026amp;操作，获取在directory中对应的索引位置。例如，假设key=3，hash(key)=101（二进制），global depth还是2，那么检查hash(key)的最低两位即为01，那么在directory就是第1个entry。\nCanShrink CanShrink() 的核心功能是检测是否可以减少全局深度（global depth），从而缩小哈希表的大小。它基于以下原则：\n在可扩展哈希表中，每个桶（bucket）都有自己的局部深度（local depth），而整个哈希表有一个全局深度（global depth）。如果所有桶的局部深度都小于当前的全局深度，说明哈希表的某些位（超过局部深度的那些位）并没有被实际使用，因而可以安全地减少全局深度。\nGetSplitImageIndex GetSplitImageIndex() 是在可扩展哈希表中用于桶（bucket）拆分时确定拆分后的另一个桶的索引。\n在可扩展哈希表中，当一个桶装满时，目录容量会翻倍，这个桶会拆分成两个桶（但是除了需要拆分的桶，其他目录还是指向原来的桶）。\n每个桶都有一个局部深度（local depth），表示这个桶在哈希表中使用了多少位哈希值来定位数据。桶拆分时，局部深度会增加。 拆分后的桶与当前桶具有相同的局部深度值，只是在第一位上有所不同。例如，如果当前桶的局部深度为 2，那么拆分后的桶与其前后 2 位相同，只有第 1 位不同。 例如，当前桶的索引为 01，局部深度为 3。\n计算翻转位的值：1 \u0026lt;\u0026lt; (local_depth - 1) = 1 \u0026lt;\u0026lt; (3 - 1) = 1 \u0026lt;\u0026lt; 2 = 100 （即二进制的 0100）。 按位异或：bucket_index ⊕ 100 = 001 ⊕ 100 = 101（即二进制的 101，也就是十进制的 5）。 因此，拆分后的桶的索引是 101（5 in decimal）。\nGetLocalDepthMask 这个函数主要是用在bucket的分裂和合并中，用于判断需要操作的bucket中的项在更新后应该属于directory下哪个entry对应的bucket（有点绕）。\n和GetGlobalDepthMask一样，当前directory的entry映射的bucket的local_depth_是多少，其mask的二进制就是多少个1。\nBucket Bucket存储着多个键值对，没有使用标准库的map，而是使用std::pair数组（如果都用标准库了要你实现啥哈希表hh）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 #define MappingType std::pair\u0026lt;KeyType, ValueType\u0026gt; static constexpr uint64_t HTABLE_BUCKET_PAGE_METADATA_SIZE = sizeof(uint32_t) * 2; constexpr auto HTableBucketArraySize(uint64_t mapping_type_size) -\u0026gt; uint64_t { return (BUSTUB_PAGE_SIZE - HTABLE_BUCKET_PAGE_METADATA_SIZE) / mapping_type_size; }; class ExtendibleHTableBucketPage { ... private: uint32_t size_; uint32_t max_size_; MappingType array_[HTableBucketArraySize(sizeof(MappingType))]; }; 任务就是在这个bucket中增删查对应的key and value，由于内部使用的是定长数组，所以最简单的方法就是顺序操作。\n但是需要注意的是，在bucket page的Insert操作中，注释上给的提示是：“当插入成功时返回true，插入失败或者键已经存在时返回false”。但是如果你按着“先判断bucket是否满了，再遍历bucket中的键值对数组，查找有没有key相同的，最后再插入”这样的逻辑去写，那当你提交时怎么测试都有几个测试集过不了，当时我想得脑子都要炸了也想不清怎么回事。\n在看了一篇博客后，我按照他的逻辑~~“先遍历数组，如果有键值相同的，更新它而不是返回false，然后再判断是否已经满了，没满就再插入”~~去写，最后对了。（这里为啥用了删除线，是因为我的代码是按照这个逻辑来写的，但是在我写这篇博客时感觉不对劲，假如这个键已经存在，且此时bucket没有满，那更新之后又插入了一次，数据有重复）\nTask3 - Extendible Hashing Implementation 细节！细节！还是TMD细节！\n前两个task比较容易写，这个task其实本质上还是一个数据结构的设计实现问题，也就是哈希表如何插入数据和删除数据，但是细节需要注意太多了！如何考虑插入后的分裂，还有删除时的合并。从第一次提交到全部通过一共用了7天（哭）。\nInsert Insert操作需要注意的就是插入失败后的分裂问题，小细节在于下面逻辑步骤中的3，8.5和9（加粗表示）:\n检查需要插入的键是否已经存在，如果存在就返回false，否则继续第二步 对key使用hash算法 从header通过hash(key)找到对应的directory page，如果directory不存在，那么创建新的directory page，再创建新的bucket page后执行插入；否则继续第四步。需要注意，如果判断了directory存在后，需要对header page guard进行Drop()操作，因为平台的测试集中有比较刁钻的情况，其buffer manager pool的大小只有3，如果不把header page释放，那么到时候需要进行分裂时无法再获取一个新的page！ 从directory通过hash(key)找到对应的bucket page，如果page不存在，那么创建新的bucket page再插入；否则继续第五步 向bucket page中插入键值对，如果插入成功，返回true；否则继续第六步 （循环开始） 检查当前directory是否已经满了，比如说directory的max_depth_是2，那么说明最多只能有四个bucket page，若此时已经有四个bucket了，那么已经满了，无法继续分裂桶；没满继续第八步 分裂桶 先创建新的bucket page作为当前bucket的镜像桶 如果global depth等于当前bucket的local depth，说明需要扩充directory的大小，同时调整扩充后的directory中的entry与bucket的映射 在directory中设置镜像桶，增加原桶和镜像桶的local depth，此时获取原桶的local depth对应的mask 通过mask将原桶拆成两个桶，比如原桶的local depth为2，对应的mask的二进制为$(11)_2$，通过\u0026amp;操作获取桶中每个键值对的hash(key)的最低两位，如果与bucket_index \u0026amp; mask相同，那么就说明这个键值对应该留在原桶，反之应该移至镜像桶 分裂成两个桶后，在使用mask判断需要插入的键值对应该插入那个桶，记录插入是否成功 考虑这样一种情况，需要插入的bucket满了后要进行分裂，但是有可能分裂后原来的键值对还是在一个bucket中，那这个时候插入就失败了，所以需要继续从第七步执行，直到directory满了或者插入成功 （循环结束） return true Remove 由于课程要求不能公开代码，加上网上关于可拓展哈希的操作多只有Insert，而Remove很少有介绍说明的，所以不得不去思考很多情况，加上自己画图去理解，当然还是去看了几篇大佬的博客我才慢慢写出了解决方案。\n实验指导中说了对于Remove的合并需要进行递归的处理，但其实我们用循环去处理就行，至于为什么要递归去处理，下面的步骤9和步骤13中有加粗解释：\n对key使用hash算法 从header通过hash(key)找到对应的directory page，如果directory不存在，return false；否则继续第3步。类似Insert，这里也需要Drop header page 从directory通过hash(key)找到对应的bucket page，如果page不存在，那return false；否则继续第4步 删除bucket中的key对应的键值对，如果删除失败，返回false；否则继续第5步 如果删除成功后bucket不为空，说明不需要合并，返回true；否则继续第6步 （循环开始） 获取当前bucket的镜像bucket，如果二者的local depth不相同，那么结束循环；否则继续第8步 当前桶和镜像桶可以合并，将当前桶的映射更新到镜像桶，并且local depth都 -1，删除当前桶的bucket page 删除了bucket_index对应的page后，虽然调整了其对应的local depth和page id，但是还应该遍历所有的可成为镜像桶的index，如果它们的local depth相同，也应该合并，记录需要合并的的bucket的page id 在遍历时不需要一个一个去遍历directory的entry，而是可以跳着遍历：只有index的后local_depth - 1位相同的才可能需要合并 如果需要合并的的bucket的page id数量为0，说明已经没有可以合并的bucket了，不应该继续合并，可以结束循环了；否则继续第11步 因为又合并了，所以原桶和其镜像桶对应的 local depth 又要 -1 删除需要合并的的bucket的page 从第7步继续，因为合并后的bucket在其local depth - 1后可能会碰到和其local depth相同的bucket，且其中还有空的bucket，这就需要不断去合并（这就是实验中说的递归合并） （循环结束） 缩减目录大小，直到无法缩小 return true Reference https://www.cnblogs.com/wevolf/p/18302985 https://zhuanlan.zhihu.com/p/622221722 https://zhuanlan.zhihu.com/p/701875021 ","permalink":"https://kerolt.github.io/p/cmu15-445-fall2023project2-extendible-hash-index-%E5%B0%8F%E7%BB%93/","summary":"\u003cblockquote\u003e\n\u003cp\u003e该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e太菜了，从没打过这么艰难的仗QAQ。由于课程的要求不能公开源代码，所以网上的资源会少很多，平台上的测试案例比较全面，有的还比较刁钻，需要考虑到可拓展哈希的实现细节。在自认为写完了后，提交了近40来次总有几个测试集过不了，还好没有崩溃，在看了几篇博客的方法后，加上自己画图理解，最后终于过了😭。不过回头写博客的时候再去看代码，也没有特别的复杂，还是得明白其中的算法逻辑是如何实现的。\u003c/p\u003e","title":"【CMU15-445 Fall2023】Project2 Extendible Hash Index 小结"},{"content":"为什么我们需要copy on write 通过xv6的实验指导书，我们可以知道：\nxv6中的fork()系统调用将父进程的所有用户空间内存复制到子进程中。如果父进程所使用的页数很大，复制可能需要很长时间，而这样的复制操作经常是没有用的：fork()之后通常是子进程中的exec()，这会丢弃复制的内存而不是使用它们。\n那么，我们是不是可以在子进程刚创建时将其页表的映射到父进程的物理页，而在其需要对内存进行写操作时再重新分配内存呢？没错，这就是通过copy on write（写时复制）技术来进行优化。\n该实验的代码实现见：仓库commit\n大致思路 首先说一下大致思路：\n在使用fork系统调用创建子进程时，我们不用去额外拷贝父进程的内存，而是将子进程的虚拟内存映射到和父进程相同的物理内存，并且此时应该将父子进程对这块内存的访问权限设置为只读，并且添加一个用于识别COW（copy on write）的标志：\n当子进程需要对内存进行写操作时，RISC-V会检测到这一块物理地址的权限为只写，触发Page Fault，这时我们可以为子进程重新分配一块内存空间（取消之前的映射，分配内存后重新映射至新内存），并将父子进程的对应物理页的识别标志位进行修改，去除cow标志PTE_COW，添加写权限标志PTE_W：\n具体实现 COW标志位 首先我们需要为PTE添加一个用于识别COW的标志位，上图展示了PTE的后10位也就是其flags的情况，可以看到第8、9位是保留没有被使用的，那么我们可以用第8位来作为COW的标志位：\n1 2 3 // riscv.h #define PTE_COW (1L \u0026lt;\u0026lt; 8) 修改uvmcopy 在xv6的fork系统调用中，子进程拷贝父进程的物理页使用了uvmcopy()函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int fork(void) { int i, pid; struct proc *np; struct proc *p = myproc(); // Allocate process. if((np = allocproc()) == 0){ return -1; } // Copy user memory from parent to child. if(uvmcopy(p-\u0026gt;pagetable, np-\u0026gt;pagetable, p-\u0026gt;sz) \u0026lt; 0){ freeproc(np); release(\u0026amp;np-\u0026gt;lock); return -1; } ... } 那么我们需要修改uvmcopy中的代码，将原先的拷贝内存的操作去掉，并将PTE的只读标志位去除、添加cow标记位：\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 int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { pte_t *pte; uint64 pa, i; uint flags; for(i = 0; i \u0026lt; sz; i += PGSIZE){ if((pte = walk(old, i, 0)) == 0) panic(\u0026#34;uvmcopy: pte should exist\u0026#34;); if((*pte \u0026amp; PTE_V) == 0) panic(\u0026#34;uvmcopy: page not present\u0026#34;); pa = PTE2PA(*pte); flags = PTE_FLAGS(*pte); // 这里移除了拷贝的代码，并设置了父进程相应的标志位 if (flags \u0026amp; PTE_W) { *pte = (*pte \u0026amp; ~PTE_W) | PTE_COW; flags = (flags \u0026amp; ~PTE_W) | PTE_COW; } // 将子进程的虚拟页映射至父进程的物理页，同时设置了子进程相应的标志位 if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){ goto err; } // 这里是对物理页的引用计数进行+1，后文会说明 kref_inc((void*)pa); } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; } 引用计数 Ensure that each physical page is freed when the last PTE reference to it goes away \u0026ndash; but not before. A good way to do this is to keep, for each physical page, a \u0026ldquo;reference count\u0026rdquo; of the number of user page tables that refer to that page. Set a page\u0026rsquo;s reference count to one when kalloc() allocates it. Increment a page\u0026rsquo;s reference count when fork causes a child to share the page, and decrement a page\u0026rsquo;s count each time any process drops the page from its page table. kfree() should only place a page back on the free list if its reference count is zero. It\u0026rsquo;s OK to to keep these counts in a fixed-size array of integers.\n当我们采取了cow后，只有当所有虚拟页都没有引用某一物理页时这个物理页才能被释放，那么我们可以使用一个数组来对每一个物理页进行引用计数，当fork导致子进程共享物理页时，对应的物理页的引用计数+1，当有进程不再使用物理页时，对应的物理页的引用计数-1，回收物理页的kfree()函数只有当物理页的引用计数为0时才会将其放回空闲列表。\n1 2 3 4 5 6 // kalloc.c struct { struct spinlock lock; // 保证操作的原子性 int ref_count[(PGROUNDUP(PHYSTOP)) / PGSIZE]; // KERNBASE～PHYSTOP是物理内存的大小，因为xv6的内核地址采用了直接映射，为了方便，这里直接使用PHYSTOP。PHYSTOP / PGSIZE则表示有多少个物理页 } kref; 内核在使用kinit()进行初始化时，需要初始化kref的锁，并设置引用计数数组的值：\n1 2 3 4 5 6 7 8 9 10 11 void kinit() { initlock(\u0026amp;kmem.lock, \u0026#34;kmem\u0026#34;); initlock(\u0026amp;kmem.lock, \u0026#34;kref\u0026#34;); // 这里初始化时置为1是为了接下来的freerange在调用kfree时不会触发panic for (int i = 0; i \u0026lt; PGROUNDUP(PHYSTOP) / PGSIZE; i++) { kref.ref_count[i] = 1; } freerange(end, (void*)PHYSTOP); } 在kfree()中对物理页的引用计数来判断是否应该释放物理内存：\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 void kfree(void *pa) { struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa \u0026lt; end || (uint64)pa \u0026gt;= PHYSTOP) panic(\u0026#34;kfree\u0026#34;); // 如果在还未对ref count操作前其值已经小于等于0，说明已有问题 if (kref.ref_count[(uint64)pa / PGSIZE] \u0026lt;= 0) { panic(\u0026#34;kref\u0026#34;); } // 每次free一个page时，先将这个page的引用计数-1 kref_dec(pa); // ref count - 1，如果结果还大于0，说明这个物理页还被其他进程引用，暂时不需要释放 if (kref.ref_count[(uint64)pa / PGSIZE] \u0026gt; 0) { return; } // Fill with junk to catch dangling refs. memset(pa, 1, PGSIZE); r = (struct run*)pa; acquire(\u0026amp;kmem.lock); r-\u0026gt;next = kmem.freelist; kmem.freelist = r; release(\u0026amp;kmem.lock); } 每分配一个物理页时，该物理页的引用计数初始为1：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void * kalloc(void) { struct run *r; acquire(\u0026amp;kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r-\u0026gt;next; release(\u0026amp;kmem.lock); if (r) { memset((char*)r, 5, PGSIZE); // fill with junk // 这里赋值为1一定要在设置垃圾数值之后，否则会造成污染 acquire(\u0026amp;kref.lock); kref.ref_count[(uint64)r / PGSIZE] = 1; release(\u0026amp;kref.lock); } return (void*)r; } 将引用计数的操作封装一下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // kalloc.c // 为pa所在的page的引用+1 void kref_inc(void* pa) { acquire(\u0026amp;kref.lock); ++kref.ref_count[(uint64)pa / PGSIZE]; release(\u0026amp;kref.lock); } // 为pa所在的page的引用-1 void kref_dec(void* pa) { acquire(\u0026amp;kref.lock); --kref.ref_count[(uint64)pa / PGSIZE]; release(\u0026amp;kref.lock); } page fault时分配新内存 当需要分配新内存时，要检测虚拟页的cow标志位是否有效。在分配了内存后，拷贝原来的物理页中的数据到新的物理页，并将虚拟页的标志位中的cow去除、添加写标志，最后取消虚拟页与原来物理页的映射关系，同时在解除时对引用计数-1，然后将虚拟页映射至新的物理页：\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 // 当出现page fault时，进行cow操作 int cow_alloc(pagetable_t pagetable, uint64 va) { if (va \u0026gt;= MAXVA) { return -1; } if ((va % PGSIZE) != 0) { return -1; } pte_t *pte = walk(pagetable, va, 0); if (pte == 0) return -1; uint64 pa = PTE2PA(*pte); if (pa == 0) return -1; // 当page的cow标志位有效时才会重新分配内存 if ((*pte \u0026amp; PTE_COW) \u0026amp;\u0026amp; (*pte \u0026amp; PTE_V)) { char* mem = kalloc(); if (mem == 0) { return -1; } uint64 flags = PTE_FLAGS(*pte); flags = (flags \u0026amp; ~PTE_COW) | PTE_W; // 去除COW标记，加上写权限标记 memmove(mem, (char *)pa, PGSIZE); uvmunmap(pagetable, PGROUNDDOWN(va), 1, 1); // 解除之前的映射，并设置do_free为1，这样在kfree中可来将引用计数-1 if (mappages(pagetable, va, PGSIZE, (uint64)mem, flags) \u0026lt; 0) { panic(\u0026#34;cow_alloc\u0026#34;); } } return 0; } 实验指导书提醒我们在copyout()中也要执行cow操作，但这里为何要这么做呢？\n这是因为需要内核将数据通过copyout拷贝到用户态时，如果需要拷贝的目标位置是用户进程与其父进程共享的，那么这时应该会有page fault产生，但是copyout中是通过walk遍历页表来获取地址的，不会触发page fault，因此需要我们手动执行cow。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) { uint64 n, va0, pa0; pte_t *pte; while(len \u0026gt; 0){ va0 = PGROUNDDOWN(dstva); if(va0 \u0026gt;= MAXVA) return -1; if (cow_alloc(pagetable, va0) \u0026lt; 0) { return -1; } ... } } usertrap中触发page fault 在之前fork操作时我们去除了物理页的write操作，那么在之后需要对该物理页进行写操作时，risc-v就会触发page fault了。\n查看risc-v的手册可以发现：\n我们需要使用excepton code 13和15（读写），当发生page fault时，这个code会保存在scause寄存器中，那么我们只需要在usertrap中对scause进行相应的处理即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void usertrap(void) { ... } else if (r_scause() == 13 || r_scause() == 15) { // 这里的stval寄存器，我的理解是保存了触发page fault时的虚拟地址 uint64 fault_va = r_stval(); // 判断地址是否不合法 if (fault_va \u0026gt;= MAXVA || (fault_va \u0026lt; p-\u0026gt;trapframe-\u0026gt;sp \u0026amp;\u0026amp; fault_va \u0026gt;= (p-\u0026gt;trapframe-\u0026gt;sp - PGSIZE)) || fault_va \u0026lt;= 0) { p-\u0026gt;killed = 1; } // 尝试进行cow操作 if (cow_alloc(p-\u0026gt;pagetable, PGROUNDDOWN(fault_va)) \u0026lt; 0) { p-\u0026gt;killed = 1; } } else { printf(\u0026#34;usertrap(): unexpected scause %p pid=%d\\n\u0026#34;, r_scause(), p-\u0026gt;pid); printf(\u0026#34; sepc=%p stval=%p\\n\u0026#34;, r_sepc(), r_stval()); setkilled(p); } } 值得注意的是，在进行cow操作之前，一定要对地址的范围进行合法性判断，因为usertests中会有非常多的测试函数，其中有一部分会检测地址合法性，不合法的地址应该直接让进程死亡。\n","permalink":"https://kerolt.github.io/p/mit6.s081lab5-copy-on-write-fork/","summary":"\u003ch2 id=\"为什么我们需要copy-on-write\"\u003e为什么我们需要copy on write\u003c/h2\u003e\n\u003cp\u003e通过xv6的实验指导书，我们可以知道：\u003c/p\u003e\n\u003cp\u003exv6中的\u003ccode\u003efork()\u003c/code\u003e系统调用将父进程的所有用户空间内存复制到子进程中。如果父进程所使用的页数很大，复制可能需要很长时间，而这样的复制操作经常是没有用的：\u003ccode\u003efork()\u003c/code\u003e之后通常是子进程中的\u003ccode\u003eexec()\u003c/code\u003e，这会丢弃复制的内存而不是使用它们。\u003c/p\u003e\n\u003cp\u003e那么，我们是不是可以在子进程刚创建时将其页表的映射到父进程的物理页，而在其需要对内存进行写操作时再重新分配内存呢？没错，这就是通过copy on write（写时复制）技术来进行优化。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e该实验的代码实现见：\u003ca href=\"https://github.com/kerolt/xv6-labs-2023/commit/68fd833dc03a681c04a647ed6aae2108a1e43fbb\"\u003e仓库commit\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"【MIT6.S081】Lab5 copy-on-write fork"},{"content":"backtrace这个lab非常有意思，虽然实现的代码量不多，但是能让我们更好地理解栈、栈帧、指针、gdb的一些知识。\n该实验的代码实现见：仓库commit\n首先先解答一下【RISC-V assembly】中的一些问题：\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 Q: Which registers contain arguments to functions? For example, which register holds 13 in main\u0026#39;s call to printf? A: a0-a7, a2保存了13 Q: Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.) A: 函数f和g被内联优化了 Q: At what address is the function printf located? A: 0x000000000000064a Q: What value is in the register ra just after the jalr to printf in main? A: jalr指令的后一条指令的地址，也是当前pc寄存器中的地址 Q: Run the following code. unsigned int i = 0x00646c72; printf(\u0026#34;H%x Wo%s\u0026#34;, 57616, \u0026amp;i); What is the output? Here\u0026#39;s an ASCII table that maps bytes to characters. The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value? Here\u0026#39;s a description of little- and big-endian and a more whimsical description. --- A: output: He110 World 若risc-v为大端序，则i应该设置成0x726c6400；57616不需要变，因为无论是大端序还是小端序，其十六进制都为E110 Q: In the following code, what is going to be printed after \u0026#39;y=\u0026#39;? (note: the answer is not a specific value.) Why does this happen? printf(\u0026#34;x=%d y=%d\u0026#34;, 3); --- A: x=3 y=1403684968 y的值是一个随机值，因为本该传入printf的第三个参数并没有传入，而其对应的寄存器为a2，故y会使用a2中残存的值 这里的几个问题不是很难，涉及到了一些汇编、寄存器的知识，在接下来学习backtrace的时候将会有详细讨论。\nbacktrace需要我们做的事情可以概括为：在发生错误的点之上的堆栈上的函数调用列表，并在每个堆栈帧中打印保存的返回地址。\n什么意思呢？就是例如在gdb调试中使用bt查看函数调用栈时，需要我们打印途中红色框中的地址。\n该lab让我们在kernel/printf.c中实现一个backtrace函数。在sys_sleep中插入对此函数的调用，然后运行bttest这个测试程序将调用sleep（也就是会执行sys_sleep）。\n首先，让我们来看看xv6中栈的结构：\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 . . +-\u0026gt; . | +-----------------+ | | | return address | | | | previous fp ------+ | | saved registers | | | local variables | | | ... | \u0026lt;-+ | +-----------------+ | | | return address | | +------ previous fp | | | saved registers | | | local variables | | +-\u0026gt; | ... | | | +-----------------+ | | | return address | | | | previous fp ------+ | | saved registers | | | local variables | | | ... | \u0026lt;-+ | +-----------------+ | | | return address | | +------ previous fp | | | saved registers | | | local variables | | $fp --\u0026gt; | ... | | +-----------------+ | | return address | | | previous fp ------+ | saved registers | $sp --\u0026gt; | local variables | +-----------------+ 栈是由高地址向低地址增长的，risc-v中sp寄存器代表“stack pointer”，即栈顶指针，fp寄存器代表“frame pointer”，为当前栈帧的指针。\n假设有一个这样的程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; void g() { printf(\u0026#34;g()\\n\u0026#34;); } void f() { g(); printf(\u0026#34;f()\\n\u0026#34;); } int main() { f(); } 假设程序中的main函数里，函数f调用了函数g，那么在函数调用栈中从高地址到低地址三个函数的顺序为：main、f、g；当g函数执行完成后，其栈帧将会从栈中弹出，并且通过栈帧中的数据回到调用自身的下一条指令，即f中g的调用发生在第8行，当g执行完毕后，应该继续执行第9行的指令，这也就是“return address”。\n我们可以通过内联汇编获取当前栈帧的的指针：\n1 2 3 4 5 6 7 8 9 // kernel/riscv.h static inline uint64 r_fp() { uint64 x; asm volatile(\u0026#34;mv %0, s0\u0026#34; : \u0026#34;=r\u0026#34; (x) ); return x; } 然后不断遍历栈中的栈帧，打印其return address，直到遍历到最后一个栈帧。xv6在给栈分配内存时确保了每一个栈帧都在同一页中，这样的话可以通过PGROUNDDOWN(fp)宏来判断fp是否超出栈空间：\n1 #define PGROUNDDOWN(a) (((a)) \u0026amp; ~(PGSIZE - 1)) xv6中页的大小为4096B，故PGROUNDDOWN(a)可以获取a地址所在的页号，或者说这一页的最高地址，只要我们的fp不等于它，就说明我们还没有遍历到栈底。\nfp - 8获取到return address的地址，fp - 16获取到当前栈帧的前一个栈帧的地址。由于xv6中获取的地址是用uint64来表示的，那么可将其强转为uint64*来将一个值解释为内存地址，之后便可以解引用这个地址获取其中的值了。\n1 2 3 4 5 6 7 8 9 void backtrace() { uint64 fp = r_fp(); printf(\u0026#34;backtrace:\\n\u0026#34;); while (fp != PGROUNDDOWN(fp)) { uint64 *return_addr = (uint64 *)(fp - 8); fp = *(uint64 *)(fp - 16); printf(\u0026#34;%p\\n\u0026#34;, *return_addr); } } 当然，也不要忘记在kernel/def.h中声明backtrace，还有在sys_sleep中调用backtrace。\n","permalink":"https://kerolt.github.io/p/mit6.s081lab4-trap-backtrace/","summary":"\u003cp\u003ebacktrace这个lab非常有意思，虽然实现的代码量不多，但是能让我们更好地理解栈、栈帧、指针、gdb的一些知识。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e该实验的代码实现见：\u003ca href=\"https://github.com/kerolt/xv6-labs-2023/commit/d1dba8ae4a1a71604e8bb6df5238f8cac1771683\"\u003e仓库commit\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"【MIT6.S081】Lab4 trap backtrace"},{"content":"Alarm综合了该lab中前几个练习的知识点：系统调用、中断、寄存器等，我们需要对trap机制有比较好的认识才能理解。Alarm的任务是需要我们完成一个定时器的实现：sigalarm(interval, handler)，当调用sigalarm(n, fn)时，内核会每n个时间间隔（tick）执行fn函数。\n该实验的代码实现见：仓库commit\n如何理解Alarm “内核会每n个时间间隔执行fn函数”，如何理解这“每n个时间间隔”呢？在计算机中有一个“时钟周期”的概念，而我们这里所说的时间间隔就是xv6中设置的每次发生时钟中断所间隔的始终周期。在xv6内核初始化时会执行timerinit()函数，其中有：\n1 2 3 // ask the CLINT for a timer interrupt. int interval = 1000000; // cycles; about 1/10th second in qemu. *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval; 这里就是设置了xv6会每经过interval个时钟周期（在qemu中大概为0.1秒）进行一次时钟中断，这个中断是硬件自动执行的（在没看源码之前，一直没搞懂xv6是什么时候、怎么进行的时钟中断）。下文将以时钟中断的间隔tick作为基本单位。\n所以简单来说，Alarm需要我们在内核中进行计数，每当经过n个tick的时候，需要去执行fn函数。\ntest0~3所做的事 test0：用于测试我们的sigalarm是否起作用了； test1：用于测试内核是否多次调用处理函数，需要确保中断发生时跳转的地址为处理函数所在的地址，还有中断时需要保存好之前寄存器中的值； test2：用于测试内核不允许重入sysalarm系统调用，即若某个进程正在执行处理函数，那么内核就不应该再次调用它； test3：用于测试sys_sigreturn系统调用能否正确返回寄存器a0的值。 实现 注册系统调用 首先的注册系统调用的步骤这里就不展开了，具体可参考之前的lab。\nstruc proc结构体 为了完成定时执行某个函数，我们需要在struct proc结构体中加入一些成员：\n1 2 3 4 5 6 7 8 struct proc { ... int is_handling; // 用于判断当前进程是否正在执行处理函数 int tick_interval; // 定时器间隔，由系统sigalarm的第一个参数传入 int tick_counter; // 定时器计数器，每次tick进行+1 uint64 tick_handler; // 间隔到了后执行的处理函数 struct trapframe *saved_trapframe; // 保存寄存器 } 值的注意的是处理函数的类型我们设置为了uint64，为什么不是一个函数指针呢？其实都差不多，在之后设置跳转处理函数的时候，就是通过地址来跳转，而地址在xv6中就是用的uint64来表示。故这里的设置即是处理函数所在的起始地址。\n实现sys_sigalarm 在sysproc.c中实现sys_sigalarm()函数：\n1 2 3 4 5 6 uint64 sys_sigalarm(void) { struct proc *p = myproc(); argint(0, \u0026amp;(p-\u0026gt;tick_interval)); argaddr(1, \u0026amp;(p-\u0026gt;tick_handler)); return 0; } 该函数要做的事情很简单，只需要接收从用户态传来的两个参数，并将其赋值给当前进程的tick_interval和tick_handler。\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 static struct proc* allocproc(void) { ... p-\u0026gt;tick_interval = 0; p-\u0026gt;tick_counter = 0; p-\u0026gt;tick_handler = 0; p-\u0026gt;is_handling = 0; ... // Allocate a saved_trapframe page. if((p-\u0026gt;saved_trapframe = (struct trapframe *)kalloc()) == 0){ freeproc(p); release(\u0026amp;p-\u0026gt;lock); return 0; } ... } static void freeproc(struct proc *p) { ... if(p-\u0026gt;saved_trapframe) kfree((void*)p-\u0026gt;saved_trapframe); p-\u0026gt;saved_trapframe = 0; ... p-\u0026gt;tick_counter = 0; p-\u0026gt;tick_interval = 0; p-\u0026gt;tick_handler = 0; p-\u0026gt;is_handling = 0; } 补全usertrap 接下来就是需要实现在usertrap中处理时钟中断，在实验指导书中提示我们在if(which_dev == 2) ...中处理时钟中断。这里我的处理逻辑为，当时钟中断发生时：\n当前进程的tick计数器++ 判断进程设置的定时器间隔是否不为0、当前计数器是否已经经过了interval个间隔、且当前进程未执行处理函数，如果其中一项不满足，则不进行第三步 将当前进程的trapframe的内容（即寄存器的值）保存到saved_trapframe（用于恢复现场），将SEPC寄存器的值设置为处理函数的地址，这样中断结束返回时就会去执行处理函数了，最后设置当前进程“正在执行处理函数” 代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void usertrap(void) { ... // give up the CPU if this is a timer interrupt. if (which_dev == 2) { p-\u0026gt;tick_counter++; if (p-\u0026gt;tick_interval \u0026amp;\u0026amp; p-\u0026gt;tick_counter % p-\u0026gt;tick_interval == 0 \u0026amp;\u0026amp; p-\u0026gt;is_handling == 0) { memmove(p-\u0026gt;saved_trapframe, p-\u0026gt;trapframe, PGSIZE); p-\u0026gt;trapframe-\u0026gt;epc = p-\u0026gt;tick_handler; p-\u0026gt;is_handling = 1; } yield(); } ... 在第二步中不知道是xv6的bug还是我有地方没理解好，如果不先检查tick_interval != 0，则可能在执行p-\u0026gt;tick_counter % p-\u0026gt;tick_interval会有问题，因为取模运算符%在分母为0时是未定义的，但这么运行时xv6并没有任何错误。\n返回，恢复现场 在处理函数执行的最后，将会执行sigreturn系统调用进行返回并恢复现场，这时我们就可以将之前存放在saved_trapframe中的值拷贝回trapframe中，并设置当前进程“未执行处理函数”。实验指导书中还提示我们最终返回的结果为a0寄存器中的值。\n1 2 3 4 5 6 uint64 sys_sigreturn(void) { struct proc *p = myproc(); memmove(p-\u0026gt;trapframe, p-\u0026gt;saved_trapframe, PGSIZE); p-\u0026gt;is_handling = 0; return p-\u0026gt;trapframe-\u0026gt;a0; // return this for alarm test3 } ","permalink":"https://kerolt.github.io/p/mit6.s081lab4-trap-alarm/","summary":"\u003cp\u003eAlarm综合了该lab中前几个练习的知识点：系统调用、中断、寄存器等，我们需要对trap机制有比较好的认识才能理解。Alarm的任务是需要我们完成一个定时器的实现：\u003ccode\u003esigalarm(interval, handler)\u003c/code\u003e，当调用\u003ccode\u003esigalarm(n, fn)\u003c/code\u003e时，内核会每n个时间间隔（tick）执行fn函数。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e该实验的代码实现见：\u003ca href=\"https://github.com/kerolt/xv6-labs-2023/commit/0f70ef7d68ab1273262b8d43a2deaf7345a87698\"\u003e仓库commit\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"【MIT6.S081】Lab4 trap alarm"},{"content":"前言 页表是最常用的机制，操作系统通过它为每个进程提供自己的私有地址空间和内存。页表决定了内存地址的含义，以及可以访问物理内存的哪些部分。在本文中，记录了Lab: page tables的前两个实验：加速系统调用和打印页表。\nSpeed up system calls (easy) 通常我们在需要执行系统调用时，在操作系统中会发生从用户态到内核态的切换，这是因为这些核心的操作只能交给内核去完成。在这个实验中，xv6要求我们通过在用户空间和内核之间共享只读区域中的数据来加快某些系统调用。\n由于现在只是入门如何将映射添加至页表中，这个实验只需要为xv6中的getpid()系统调用进行优化。\n在xv6的实验指导书中：\n创建每个进程时，在USYSCALL（memlayout.h中定义的虚拟地址）映射一个只读页。在该页面的开头，存储一个usyscall结构体（也在memlayout.h中定义），并对其进行初始化以存储当前进程的PID。\n既然如此，进程结构体中也应当有一个usyscall结构体：\n1 2 3 4 5 6 7 8 9 10 // kernel/proc.h struct proc { ... pagetable_t pagetable; // User page table struct trapframe *trapframe; // data page for trampoline.S struct usyscall *usyscall; // data page for USYSCALL struct context context; // swtch() here to run process ... }; 这样就可以通过p-\u0026gt;usyscall来获取了。\n在memlayout.h中我们可以看到用户态空间内存布局：\n1 2 3 4 5 6 7 8 9 Address zero first: text original data and bss fixed-size stack expandable heap ... USYSCALL (shared with kernel) TRAPFRAME (p-\u0026gt;trapframe, used by the trampoline) TRAMPOLINE (the same page as in the kernel) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 MAXVA-\u0026gt; ------------------------------------- | TRAMPOLINE (与内核相同的页面) | ------------------------------------- | TRAPFRAME (p-\u0026gt;trapframe, 由跳板使用)| ------------------------------------- | USYSCALL (与内核共享) | ------------------------------------- | ... | ------------------------------------- | 可扩展堆 | ------------------------------------- | 固定大小的栈 | ------------------------------------- | 原始数据和BSS | ------------------------------------- | text | 0 -\u0026gt; ------------------------------------- 我们需要做的就是仿照TRAPFRAME将USYSCALL也做一层映射。\n在allocproc()为进程分配物理页时，使用kalloc()对usyscall的分配（kalloc每次从空闲页表中取出一个项，其大小为4KB）：\n1 2 3 4 5 6 // Allocate a usyscall page. if((p-\u0026gt;usyscall = (struct usyscall *)kalloc()) == 0){ freeproc(p); release(\u0026amp;p-\u0026gt;lock); return 0; } 在proc_pagetable()函数中，其为指定进程创建用户页表，不含用户内存，但有trampoline 和 trapframe页。以下代码是对trampoline 和 trapframe进行映射。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // map the trampoline code (for system call return) // at the highest user virtual address. // only the supervisor uses it, on the way // to/from user space, so not PTE_U. if(mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) \u0026lt; 0){ uvmfree(pagetable, 0); return 0; } // map the trapframe page just below the trampoline page, for // trampoline.S. if(mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p-\u0026gt;trapframe), PTE_R | PTE_W) \u0026lt; 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } 如此，我们照猫画虎，也可以写出usyscall的映射。需要注意的是，该页是read-only的，并且允许用户态访问，因此其权限应该为PTE_R和PTE_U。\n1 2 3 4 5 6 7 8 // map the usyscall page if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p-\u0026gt;usyscall), PTE_R | PTE_U) \u0026lt; 0){ uvmunmap(pagetable, USYSCALL, 1, 0); uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } 在结束进程时，即freeproc函数中，也需要对usyscall的空间进行释放：\n1 2 3 if(p-\u0026gt;usyscall) kfree((void*)p-\u0026gt;usyscall); p-\u0026gt;usyscall = 0; 同时还应当在proc_freepagetable函数中解除之前对usyscall的映射：\n1 uvmunmap(pagetable, USYSCALL, 1, 0); Print a page table (easy) 这个实验要求我们将页表打印出来。在实验开始前，让我们先看看xv6中的页表。\nxv6中的页表为三级页表，在VA转换为PA的过程中，处理单元会通过satp寄存器找到当前进程的页表基地址，然后取出VA中的L2部分找到一级页表的项，一级页表中的项（PTE）保存二级页表的地址，再通过L1可获取二级页表中的项，依次类推即可将VA转换为PA。\n这样看来，想要打印页表，有点类似于DFS算法，需要使用递归。按照实验指导书所说，我们可以从freewalk函数中获取灵感，查看其源码可以知道如何去遍历页表项。那么按照要求所实现打印页表就比较容易了：\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 static void print_pgtbl(pagetable_t pagetable, int depth) { if (depth \u0026gt; 2) { return; } for(int i = 0; i \u0026lt; 512; i++){ pte_t pte = pagetable[i]; if (pte \u0026amp; PTE_V) { if (depth == 0) { printf(\u0026#34;..\u0026#34;); } else if (depth == 1) { printf(\u0026#34;.. ..\u0026#34;); } else if (depth == 2) { printf(\u0026#34;.. .. ..\u0026#34;); } uint64 child = PTE2PA(pte); printf(\u0026#34;%d: pte %p pa %p\\n\u0026#34;, i, pte, PTE2PA(pte)); print_pgtbl((pagetable_t)child, depth + 1); } } } void vmprint(pagetable_t pagetable) { printf(\u0026#34;page table %p\\n\u0026#34;, pagetable); print_pgtbl(pagetable, 0); } 由于打印页表这个操作是进程号为1的init进程做的，所以不要忘记在kernel/exec.c的exec函数中添加：\n1 2 3 if (p-\u0026gt;pid == 1) { vmprint(p-\u0026gt;pagetable); } 并且在kernel/defs.h中添加vmprint的函数声明：\n1 void vmprint(pagetable_t); ","permalink":"https://kerolt.github.io/p/mit6.s081lab3-page-tables%E4%B8%8A/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e页表是最常用的机制，操作系统通过它为每个进程提供自己的私有地址空间和内存。页表决定了内存地址的含义，以及可以访问物理内存的哪些部分。在本文中，记录了\u003ccode\u003eLab: page tables\u003c/code\u003e的前两个实验：加速系统调用和打印页表。\u003c/p\u003e","title":"【MIT6.S081】Lab3 page tables（上）"},{"content":"本关的任务为“Detect which pages have been accessed”，需要实现一个新的系统调用pgaccess，它指出访问了哪些页面被访问了（读、写等）。系统调用需要三个参数：\n第一个用户页面的起始虚拟地址 需要检查页数 一个存储每一页是否被访问的掩码 该lab的所有代码：Github\n测试该系统调用的函数位于user/pgtbltest.c:pgaccess_test()中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void pgaccess_test() { char *buf; unsigned int abits; printf(\u0026#34;pgaccess_test starting\\n\u0026#34;); testname = \u0026#34;pgaccess_test\u0026#34;; buf = malloc(32 * PGSIZE); if (pgaccess(buf, 32, \u0026amp;abits) \u0026lt; 0) err(\u0026#34;pgaccess failed\u0026#34;); buf[PGSIZE * 1] += 1; buf[PGSIZE * 2] += 1; buf[PGSIZE * 30] += 1; if (pgaccess(buf, 32, \u0026amp;abits) \u0026lt; 0) err(\u0026#34;pgaccess failed\u0026#34;); if (abits != ((1 \u0026lt;\u0026lt; 1) | (1 \u0026lt;\u0026lt; 2) | (1 \u0026lt;\u0026lt; 30))) err(\u0026#34;incorrect access bits set\u0026#34;); free(buf); printf(\u0026#34;pgaccess_test: OK\\n\u0026#34;); } 分析下源码可以知道，测试程序分配了32个页，并且使用（or 访问）了这分配的32个页的第1、2、30页，之后程序调用pgaccess来检测abits的第1、2、30位是否为1，即判断该系统调用是否实现了“检测已经访问的页”这个功能。\n向内核添加系统调用的方法在lab2中已经了解过了，不过这里xv6已经帮我们添加好了，我们只需要实现系统调用kernel/sysproc.c:sys_pgaccess()即可。\n在xv6 book中可以知道一个PTE的每位构成如上图，其中0 - 9位是一些标志位，第6位为Accessed，也就是访问位，需要在内核中添加这个标志：\n1 2 3 // kernel/riscv.h #define PTE_A (1L \u0026lt;\u0026lt; 6) 而risc-v处理器会利用硬件将已访问的页的PTE_A正确设置。\n实现sys_pgaccess的步骤大致可分为：\n接受用户态传递的三个参数： 第一个用户页面的起始虚拟地址（指针） 需要检查页数（int） 一个存储每一页是否被访问的掩码（指针） 遍历“需要检查页数”，并每次检查遍历的页是否已访问。获取每个页对应的PTE将使用walk函数来获取，而检查将使用PTE_A来判断；如果当前页的PTE_A为1，则说明该页被访问过，利用位运算（是的，位运算在该lab里立大功）来存储信息，即第几页被访问了，并且不要忘记了实验指导中的提示，“Be sure to clear PTE_A after checking if it is set. Otherwise, it won\u0026rsquo;t be possible to determine if the page was accessed since the last time pgaccess() was called”，检测完后应该将PTE_A标记位清除。 将存储的信息利用copyout函数从内核态传递给用户态，届时用户态可通过第三个参数获取。 具体实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int sys_pgaccess(void) { // lab pgtbl: your code here. uint64 uvm_pgaddr; int page_counts; uint64 abits; argaddr(0, \u0026amp;uvm_pgaddr); argint(1, \u0026amp;page_counts); argaddr(2, \u0026amp;abits); int result = 0; pagetable_t page_table = myproc()-\u0026gt;pagetable; for (int i = 0; i \u0026lt; page_counts; i++) { pte_t *pte = walk(page_table, uvm_pgaddr, 0); if (((*pte) \u0026amp; (PTE_A)) != 0) { result |= (1 \u0026lt;\u0026lt; i); *pte \u0026amp;= (~PTE_A); } uvm_pgaddr += PGSIZE; } copyout(page_table, abits, (char*)\u0026amp;result, sizeof(result)); return 0; } ","permalink":"https://kerolt.github.io/p/mit6.s081lab3-page-tables%E4%B8%8B/","summary":"\u003cp\u003e本关的任务为“Detect which pages have been accessed”，需要实现一个新的系统调用\u003ccode\u003epgaccess\u003c/code\u003e，它指出访问了哪些页面被访问了（读、写等）。系统调用需要三个参数：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e第一个用户页面的起始虚拟地址\u003c/li\u003e\n\u003cli\u003e需要检查页数\u003c/li\u003e\n\u003cli\u003e一个存储每一页是否被访问的掩码\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e该lab的所有代码：\u003ca href=\"https://github.com/kerolt/xv6-labs-2023/commit/87373230877d27d7f60c39696688863210c41133\"\u003eGithub\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"【MIT6.S081】Lab3 page tables（下）"},{"content":"前言 这个lab开始我们就正式进入了xv6的世界了，这一次我们可以了解到内核中系统调用的注册和运行原理，这可以说是之后lab的一个基石。\ntrace 首先，我们要清楚这个实验的目的是什么：\nIn this assignment you will add a system call tracing feature that may help you when debugging later labs. You\u0026rsquo;ll create a new trace system call that will control tracing. It should take one argument, an integer \u0026ldquo;mask\u0026rdquo;, whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 \u0026laquo; SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call\u0026rsquo;s number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don\u0026rsquo;t need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.\n译：在Xv6的trace命令中，它应该有一个参数，一个整数“掩码”，其位指定要跟踪的系统调用。例如，要跟踪fork系统调用，程序调用trace(1\u0026lt;\u0026lt;SYS_fork)，其中SYS_fork是kernel/syscall.h中的系统调用编号。如果系统调用的编号在掩码中设置，则必须修改xv6内核，以便在每个系统调用即将返回时打印出一行。该行应包含进程id、系统调用的名称和返回值；您不需要打印系统调用参数。跟踪系统调用应启用对调用它的进程及其随后分叉的任何子进程的跟踪，但不应影响其他进程。\n注意，在该实验的初始阶段，xv6已经为我们提供了trace命令的用户态实现，但是其底层的系统调用需要我们自己实现。\nmask是什么？ 在使用trace命令时用到的掩码，是用来跟踪之后使用的命令用到了哪些系统调用。例如实验中给出的例子：\n1 2 3 4 5 $ trace 32 grep hello README 3: syscall read -\u0026gt; 1023 3: syscall read -\u0026gt; 966 3: syscall read -\u0026gt; 70 3: syscall read -\u0026gt; 0 这个32就是掩码，其跟踪到了grep命令中使用到了read系统调用（为什么是read？马上就说到了）。在xv6的kernel/syscall.h中有所有系统调用的编号：\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 // System call numbers #define SYS_fork 1 #define SYS_exit 2 #define SYS_wait 3 #define SYS_pipe 4 #define SYS_read 5 #define SYS_kill 6 #define SYS_exec 7 #define SYS_fstat 8 #define SYS_chdir 9 #define SYS_dup 10 #define SYS_getpid 11 #define SYS_sbrk 12 #define SYS_sleep 13 #define SYS_uptime 14 #define SYS_open 15 #define SYS_write 16 #define SYS_mknod 17 #define SYS_unlink 18 #define SYS_link 19 #define SYS_mkdir 20 #define SYS_close 21 // 添加 #define SYS_trace 22 将这个mask以二进制的形式来看待更加容易理解，如果传入的mask是32，那么其二进制为100000，这个1出现的位置是第5位（最低位按0计数），也就是去找编号为5的系统调用，也就是SYS_read。\nxv6内核提供给用户态的接口为trace，但是我们需要自己在xv6的用户头文件中添加函数的声明：\n1 2 3 4 5 6 7 // user/user.h // ... int trace(int); // ... 这个trace底层其实调用的应该是sys_trace（这个函数名不是固定的，但是源码中其他的系统调用的命名都为sys_*，故trace对应的系统调用写成sys_trace更加合理）。sys_trace需要做的是将用户传入的mask再传给当前进程及其子进程。\n我们这里将系统调用sys_trace的编号设置为22。\n进程及其子进程如何获取mask？ 在xv6 book的4.3节中，有这么一段话：\n1 2 3 syscall (kernel/syscall.c:132) retrieves the system call number from the saved a7 in the trapframe and uses it to index into syscalls. For the first system call, a7 contains SYS_exec (ker\u0002nel/syscall.h:8), resulting in a call to the system call implementation function sys_exec. When sys_exec returns, syscall records its return value in p-\u0026gt;trapframe-\u0026gt;a0. This will cause the original user-space call to exec() to return that value, since the C calling convention on RISC-V places return values in a0. System calls conventionally return negative numbers to indicate errors, and zero or positive numbers for success. If the system call number is invalid, syscall prints an error and returns −1. 即系统调用的编号会保存在进程的trapframe中，根据a7寄存器即可获得，系统调用的返回值可通过a0寄存器获得。欸！这两个值可不就是实验实现中需要的吗！那么理所当然，实验中需要打印的语句应该就在这个函数中添加。\n让我们来看看xv6中进程的数据结构：\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 // kernel/proc.h struct proc { struct spinlock lock; // p-\u0026gt;lock must be held when using these: enum procstate state; // Process state void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent\u0026#39;s wait int pid; // Process ID // wait_lock must be held when using this: struct proc *parent; // Parent process // these are private to the process, so p-\u0026gt;lock need not be held. uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table struct trapframe *trapframe; // data page for trampoline.S struct context context; // swtch() here to run process struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) // 添加 int trace_mask; } 可以看到诸如进程名、pid、上下文等信息都是保存在这个数据结构中，那么我们可以在其中加上一个成员变量 trace_mask 用于保存当前进程所对应trace命令中的掩码mask。\nxv6已经实现了用户态的trace命令，其位于 user/trace.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 #include \u0026#34;kernel/param.h\u0026#34; #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char *argv[]) { int i; char *nargv[MAXARG]; if(argc \u0026lt; 3 || (argv[1][0] \u0026lt; \u0026#39;0\u0026#39; || argv[1][0] \u0026gt; \u0026#39;9\u0026#39;)){ fprintf(2, \u0026#34;Usage: %s mask command\\n\u0026#34;, argv[0]); exit(1); } if (trace(atoi(argv[1])) \u0026lt; 0) { fprintf(2, \u0026#34;%s: trace failed\\n\u0026#34;, argv[0]); exit(1); } for(i = 2; i \u0026lt; argc \u0026amp;\u0026amp; i \u0026lt; MAXARG; i++){ nargv[i-2] = argv[i]; } exec(nargv[0], nargv); exit(0); } 对于这样的一条命令 trace 32 grep hello README ，假设开启的进程名为p，那么32将会传给p.trace_mask，之后的grep操作将使用exec创建子进程（假设进程名为son）执行，那么在创建子进程后应该有son.trace_mask = p.trace_mask，只有这样，grep操作所用到的系统调用才能被跟踪到。\n在使用trace命令时，其后的mask参数会存到a0寄存器中，为了从其中拿到mask，可以使用argint()函数，其源码为：\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 // kernel/syscall.h // Fetch the nth 32-bit system call argument. void argint(int n, int *ip) { *ip = argraw(n); } static uint64 argraw(int n) { struct proc *p = myproc(); switch (n) { case 0: return p-\u0026gt;trapframe-\u0026gt;a0; case 1: return p-\u0026gt;trapframe-\u0026gt;a1; case 2: return p-\u0026gt;trapframe-\u0026gt;a2; case 3: return p-\u0026gt;trapframe-\u0026gt;a3; case 4: return p-\u0026gt;trapframe-\u0026gt;a4; case 5: return p-\u0026gt;trapframe-\u0026gt;a5; } panic(\u0026#34;argraw\u0026#34;); return -1; } argint()内调用了argraw()，在查看以上源码后，由于只传入了一个参数，故应该将0传入argraw中。在kernel/sysproc.c中实现sys_trace()：\n1 2 3 4 5 6 7 8 9 10 11 12 // kernel/sysproc.c // ... uint64 sys_trace(void) { int mask; argint(0, \u0026amp;mask); if (mask \u0026lt; 0) return -1; struct proc *p = myproc(); p-\u0026gt;trace_mask = mask; return 0; } 这样当trace使用了底层的sys_trace时，就可以把mask参数传递给当前进程。但是只传递给当前进程还不够，还要传给当前进程的子进程。在Linux，我们创建子进程的函数为fork()，在xv6中也同样如此，fork内部先获取当前进程的proc结构体，然后新创建一个proc结构体代表子进程，并将父进程中的值拷贝过去，故传递给子进程的mask也在其中拷贝：\n1 2 3 4 5 6 7 8 9 10 11 // kernel/proc.c int fork(void) { // ... // copy mask from father process to son process // np为子进程，p为父进程 np-\u0026gt;trace_mask = p-\u0026gt;trace_mask; ... } 要注意一个小细节，当进程结构体被释放时（进程结束或者为进程分配proc结构体），其mask也该重置：\n1 2 3 4 5 6 // kernel/proc.c static void freeproc(struct proc *p) { // ... ... p-\u0026gt;trace_mask = 0; } 完成好以上内容后，就可以实现sys_trace了：\n1 2 3 4 5 6 7 8 9 10 // kernel/sysproc.c uint64 sys_trace(void) { int mask; argint(0, \u0026amp;mask); if (mask \u0026lt; 0) return -1; struct proc *p = myproc(); p-\u0026gt;trace_mask = mask; return 0; } 这样，当前进程就可以获取了到mask，当其创建子进程时，子进程也可获取到mask~\n如何跟踪系统调用？ 刚刚我们说了mask的作用，还有进程及其子进程如何获取mask，那么我们又应该如何跟踪系统调用呢？\nxv6中所用的系统调用都是在 kernel/syscall.c 中的 syscall 函数中调用的，为了能在syscall.c中调用sys_trace，需要在其中添加extern声明（其定义在刚刚已经实现，位于kernel/sysproc.c）：\n1 extern uint64 sys_trace(void); 同时需要在syscalls数组中添加sys_trace的编号：\n1 2 3 4 5 6 7 static uint64 (*syscalls[])(void) = { // ... ... [SYS_trace] sys_trace, }; // 这里实际上就是 [22] = sys_trace // 使用了gcc的一个拓展 并按照顺序添加各个系统调用的名字：\n1 2 3 4 5 6 char *syscall_names[] = { \u0026#34;fork\u0026#34;, \u0026#34;exit\u0026#34;, \u0026#34;wait\u0026#34;, \u0026#34;pipe\u0026#34;, \u0026#34;read\u0026#34;, \u0026#34;kill\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;fstat\u0026#34;, \u0026#34;chdir\u0026#34;, \u0026#34;dup\u0026#34;, \u0026#34;getpid\u0026#34;, \u0026#34;sbrk\u0026#34;, \u0026#34;sleep\u0026#34;, \u0026#34;uptime\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;write\u0026#34;, \u0026#34;mknod\u0026#34;, \u0026#34;unlink\u0026#34;, \u0026#34;link\u0026#34;, \u0026#34;mkdir\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;trace\u0026#34;, }; 在 kernel/syscall.c 中，xv6根据a7寄存器获取系统调用的编号，然后通过syscalls函数数组执行系统调用，那么我们的实现为：当使用的系统调用合法时，获取当前进程的mask，并通过判断(mask \u0026gt;\u0026gt; syscall_num) \u0026amp; 1是否为1来输出跟踪信息。\n例如sys_read系统调用的编号为5，mask为32，则(32 \u0026gt;\u0026gt; 5) \u0026amp; 1 = 1。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void syscall(void) { int num; struct proc *p = myproc(); num = p-\u0026gt;trapframe-\u0026gt;a7; if(num \u0026gt; 0 \u0026amp;\u0026amp; num \u0026lt; NELEM(syscalls) \u0026amp;\u0026amp; syscalls[num]) { // Use num to lookup the system call function for num, call it, // and store its return value in p-\u0026gt;trapframe-\u0026gt;a0 p-\u0026gt;trapframe-\u0026gt;a0 = syscalls[num](); // =============================== int mask = p-\u0026gt;trace_mask; if ((mask \u0026gt;\u0026gt; num) \u0026amp; 1) { printf(\u0026#34;%d: syscall %s -\u0026gt; %d\\n\u0026#34;, p-\u0026gt;pid, syscall_names[num - 1], p-\u0026gt;trapframe-\u0026gt;a0); } // =============================== } else { printf(\u0026#34;%d %s: unknown sys call %d\\n\u0026#34;, p-\u0026gt;pid, p-\u0026gt;name, num); p-\u0026gt;trapframe-\u0026gt;a0 = -1; } } 那么，内核是如何通过trace找到sys_trace的呢？根据实验指导上的提示，可以知道 user/usys.pl 起到了一个中间人的作用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # user/usys.pl ... sub entry { my $name = shift; print \u0026#34;.global $name\\n\u0026#34;; print \u0026#34;${name}:\\n\u0026#34;; print \u0026#34; li a7, SYS_${name}\\n\u0026#34;; print \u0026#34; ecall\\n\u0026#34;; print \u0026#34; ret\\n\u0026#34;; } ... entry(\u0026#34;uptime\u0026#34;); ++entry(\u0026#34;trace\u0026#34;); # 这是实验中需要由我们自己添加的 通过其中的entry函数，可以生成对应的调用（xv6中为ecall）系统调用的汇编语句，即大致的流程为xv6在构建内核时，会将用户态trace命令对应到：\n1 2 3 4 5 .global trace trace: li a7, SYS_trace ecall ret 这样，当前进程就可以通过a7寄存器拿到sys_trace的系统调用编号了，也就是说，syscall 函数可以调用 sys_trace 了。\nOK，那么一个大致的框架就出来了：\n完成上面的步骤后，最后只要在Makefile中的UPROGS加上$U/_trace即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 UPROGS=\\ $U/_cat\\ $U/_echo\\ $U/_forktest\\ $U/_grep\\ $U/_init\\ $U/_kill\\ $U/_ln\\ $U/_ls\\ $U/_mkdir\\ $U/_rm\\ $U/_sh\\ $U/_stressfs\\ $U/_usertests\\ $U/_grind\\ $U/_wc\\ $U/_zombie\\ ++\t$U/_trace 在这个lab中，我们需要添加一个系统调用sysinfo，用于收集有关正在运行的系统的信息。\n系统调用的声明为：\n1 int sysinfo(struct sysinfo*); 这个系统调用接受一个指向结构体sysinfo的指针，其定义为：\n1 2 3 4 5 6 // kernel/sysinfo.h struct sysinfo { uint64 freemem; // amount of free memory (bytes) uint64 nproc; // number of process }; 内核应填写此结构的字段：freemem字段应设置为可用内存的字节数，nproc字段应设为状态未使用的进程数。\nsysinfo 在这个part中，我们需要添加一个系统调用sysinfo，用于收集有关正在运行的系统的信息。\n计算可用内存字节数 我们可以通过内核中的kmem来获取可用的内存块的数量：\n1 2 3 4 struct { struct spinlock lock; struct run *freelist; } kmem; kmem.freelist是一个链表，保存了所有可用的内存块的地址，我们遍历这个链表即可获取可用内存块数量，又一个内存块的大小为4KB，那么系统可用的内存字节数 = 可用内存块数量 * 4KB：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // kernel/kalloc.c uint64 freemem() { struct run* r; uint64 free_page = 0; acquire(\u0026amp;kmem.lock); r = kmem.freelist; while (r) { free_page++; r = r-\u0026gt;next; } release(\u0026amp;kmem.lock); // 4K = 2^12，左移操作相当于对2的乘法 return (free_page \u0026lt;\u0026lt; 12); } 计算状态为未为使用进程数 内核中有一个全局数组，其中每一项为系统中的进程，xv6中设置最多进程数为64个：\n1 struct proc proc[NPROC]; 在表示进程的结构体中，有一个成员表示这个进程的状态：\n1 2 3 4 5 6 7 8 9 enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; // Per-process state struct proc { ... // p-\u0026gt;lock must be held when using these: enum procstate state; ... } 我们可以遍历proc数组，找到所有state != UNUSED的进程的数量（这里一定要看清楚，是状态为未使用的进程数，而不是未使用的进程数）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // kernel/proc.c uint64 nproc() { struct proc* p; uint64 not_unused = 0; for (p = proc; p \u0026lt; \u0026amp;proc[NPROC]; p++) { if (p-\u0026gt;state != UNUSED) { not_unused++; } } return not_unused; } 完成系统调用 如何获取用户态传递过来的参数和注册系统调用可以参考这篇博客，这里就不赘述了\n我们创建了struct sysinfo结构体变量info后，使用刚刚的freemem和nproc函数来为结构体变量赋值，之后通过copyout函数将内核态中的info拷贝给用户态的struct sysinfo结构体变量。\n这里的实现原理是：用户态下我们使用系统调用传递了一个struct sysinfo指针，其实就是传递了一个内存地址addr；内核态下我们将info中的数据原封不动地搬一份到addr处。这样当用户态访问addr处的内存时就可以获取到想要的数据了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 uint64 sys_sysinfo() { uint64 addr; argaddr(0, \u0026amp;addr); if (addr \u0026lt; 0) { return -1; } struct proc* p = myproc(); struct sysinfo info; info.freemem = freemem(); info.nproc = nproc(); if (copyout(p-\u0026gt;pagetable, addr, (char*)\u0026amp;info, sizeof(info))) { return -1; } return 0; } ","permalink":"https://kerolt.github.io/p/mit6.s081lab2-system-calls/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这个lab开始我们就正式进入了xv6的世界了，这一次我们可以了解到内核中系统调用的注册和运行原理，这可以说是之后lab的一个基石。\u003c/p\u003e","title":"【MIT6.S081】Lab2 system calls"},{"content":"前言 该Lab通过实现几个命令来熟悉 xv6 及其系统调用\nsleep pingpong primes find xargs 官方实验指导：https://pdos.csail.mit.edu/6.S081/2021/labs/util.html\n个人代码实现仓库：https://github.com/kerolt/xv6-labs-2023\n环境搭建 使用docker创建ubuntu20.04容器，后执行：\n1 apt install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 之后测试一下：\n首先下载xv6源码：git clone https://github.com/mit-pdos/xv6-riscv.git 运行：make qemu，如果结果如下，说明成功，按下ctrl + a和x退出qemu 1 2 3 # ... lots of output ... init: starting sh $ 测试 对于完成的程序，如果想要测试，则在Makefile中的UPROGS中添加：\n1 $U/_\u0026lt;xxx\u0026gt;\\ 其中的xxx即为程序的名称，如sleep,则为$U/_sleep\\。\n之后，可使用如下方法进行测试：\n1 2 3 ./grade-lab-util xxx # or make GRADEFLAGS=xxx grade sleep (easy) 练前开胃菜，使用sleep系统调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char* argv[]) { if (argc != 2) { printf(\u0026#34;Usage: sleep \u0026lt;seconds\u0026gt;\\n\u0026#34;); exit(1); } int time = atoi(argv[1]); sleep(time); return exit(0); } pingpong (easy) 编写一个程序，使用 UNIX 系统调用在两个进程之间通过一对管道 \u0026ldquo;乒乓 \u0026ldquo;传送一个字节，每个管道一个方向。\n该程序需要注意的就是对于两个管道的操作，何时关，关哪个？读写顺序又如何？\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 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; #define R 0 #define W 1 int main() { int p2c[2], c2p[2]; pipe(p2c); pipe(c2p); int pid = fork(); if (pid == 0) { // chile: read from the parent char buf[32] = {0}; close(c2p[R]); // 不用从子进程读 close(p2c[W]); // 不用从父进程写 read(p2c[R], buf, sizeof(buf)); close(p2c[R]); printf(\u0026#34;%d: received ping\\n\u0026#34;, getpid()); write(c2p[W], \u0026#34;pong\u0026#34;, 4); close(c2p[W]); exit(0); } else { // parent: read from the child char buf[32] = {0}; close(p2c[R]); // 不用从父进程读 close(c2p[W]); // 不用从子进程写 write(p2c[W], \u0026#34;ping\u0026#34;, 4); close(p2c[W]); read(c2p[R], buf, sizeof(buf)); printf(\u0026#34;%d: received pong\\n\u0026#34;, getpid()); close(c2p[R]); exit(0); } } primes （moderate/hard） 使用管道编写并发版质数筛。\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 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; void Filter(int pipe_fd[2]) { close(pipe_fd[1]); int prime; read(pipe_fd[0], \u0026amp;prime, 4); printf(\u0026#34;prime %d\\n\u0026#34;, prime); int num; if (read(pipe_fd[0], \u0026amp;num, 4) == 0) { exit(0); } int new_pipe_fd[2]; pipe(new_pipe_fd); int pid = fork(); if (pid == -1) { printf(\u0026#34;Fork error!\\n\u0026#34;); exit(1); } else if (pid == 0) { Filter(new_pipe_fd); } else { close(new_pipe_fd[0]); if (num % prime != 0) { write(new_pipe_fd[1], \u0026amp;num, 4); } while (read(pipe_fd[0], \u0026amp;num, 4) \u0026gt; 0) { if (num % prime != 0) { write(new_pipe_fd[1], \u0026amp;num, 4); } } close(new_pipe_fd[1]); close(pipe_fd[0]); wait(0); } } int main() { int pipe_fd[2]; pipe(pipe_fd); int pid = fork(); if (pid == -1) { printf(\u0026#34;Fork error!\\n\u0026#34;); exit(1); } else if (pid == 0) { Filter(pipe_fd); } else { close(pipe_fd[0]); for (int i = 2; i \u0026lt;= 35; i++) { write(pipe_fd[1], \u0026amp;i, 4); // 一个int为4 byte } close(pipe_fd[1]); wait(0); } exit(0); } find (moderate) 实现Unix下的find命令，利用递归处理，要注意关闭文件描述符的时机。\n最关键的是要理解目录（文件夹）也是一种文件，其目录项就是这个文件的内容，所以我们可以通过read系统调用来读取目录项，进而当读取的内容的类型是一个目录时，即可递归调用Find。\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 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; #include \u0026#34;kernel/fs.h\u0026#34; char* GetName(char* path) { char* p; for (p = path + strlen(path); p \u0026gt;= path \u0026amp;\u0026amp; *p != \u0026#39;/\u0026#39;; p--) {} p++; return p; } void Find(char* dir, char* name) { char buf[512]; char* p; int fd = open(dir, 0); struct dirent de; struct stat st; if (fd \u0026lt; 0) { printf(\u0026#34;Error: open\\n\u0026#34;); return; } if (fstat(fd, \u0026amp;st) \u0026lt; 0) { printf(\u0026#34;Error: stat\\n\u0026#34;); close(fd); return; } if (st.type != T_DIR) { printf(\u0026#34;the current the file is not dictionary\\n\u0026#34;, dir); close(fd); return; } strcpy(buf, dir); p = buf + strlen(buf); *p++ = \u0026#39;/\u0026#39;; while (read(fd, \u0026amp;de, sizeof(de)) == sizeof(de)) { if (de.inum == 0) continue; if (strcmp(de.name, \u0026#34;.\u0026#34;) == 0) continue; if (strcmp(de.name, \u0026#34;..\u0026#34;) == 0) continue; char* cur = p; memmove(cur, de.name, DIRSIZ); cur[DIRSIZ] = 0; if (stat(buf, \u0026amp;st) \u0026lt; 0) { printf(\u0026#34;Error: stat\\n\u0026#34;); continue; } switch (st.type) { case T_FILE: if (strcmp(GetName(buf), name) == 0) { printf(\u0026#34;%s\\n\u0026#34;, buf); } break; case T_DIR: if (strlen(dir) + 1 + DIRSIZ + 1 \u0026gt; sizeof(buf)) { printf(\u0026#34;Error: path too long\\n\u0026#34;); break; } Find(buf, name); break; } } close(fd); } int main(int argc, char* argv[]) { if (argc != 3) { printf(\u0026#34;Usage: find \u0026lt;dir\u0026gt; \u0026lt;file_name\u0026gt;\\n\u0026#34;); exit(1); } Find(argv[1], argv[2]); exit(0); } xargs (moderate) 简单实现Unix上的xargs命令。简单介绍xargs的用法，就是将标准输入作为xargs的参数。更多的介绍，可以看阮一峰老师的博客：https://ruanyifeng.com/blog/2019/08/xargs-tutorial.html\n该命令的可使用fork和exec来实现。\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 #include \u0026#34;kernel/types.h\u0026#34; #include \u0026#34;kernel/stat.h\u0026#34; #include \u0026#34;kernel/param.h\u0026#34; #include \u0026#34;user/user.h\u0026#34; int main(int argc, char* argv[]) { if (argc \u0026lt; 2) { printf(\u0026#34;Usage: xargs \u0026lt;params\u0026gt;\\n\u0026#34;); exit(1); } char* child_argv[MAXARG]; char buf[512] = {\u0026#39;\\0\u0026#39;}; int index = 0; for (int i = 1; i \u0026lt; argc; i++) { child_argv[index++] = argv[i]; } sleep(10); // 从标准输入中读取命令到buf中 // 若执行 echo 1 2 3，则标准输入为 1 2 3 int n; while ((n = read(0, buf, sizeof(buf))) \u0026gt; 0) { char* p = buf; for (int i = 0; i \u0026lt; n; i++) { if (buf[i] != \u0026#39;\\n\u0026#39;) continue; if (fork() == 0) { buf[i] = \u0026#39;\\0\u0026#39;; // 例如：echo 1 | xargs echo 2 // 在xargs中，标准输入为1，buf中内容为\u0026#34;1\\n\u0026#34;，child_argv为[\u0026#34;echo\u0026#34;, \u0026#34;2\u0026#34;]，index为2 // 程序执行到此处时，buf中内容变为了\u0026#34;1\\0\u0026#34;，child_argv变为了[\u0026#34;echo\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;1\u0026#34;] child_argv[index] = p; // exec的第一个参数是要执行的可执行文件的路径 // 第二个参数是作为新命令的参数数组，数组的第一项为新命令的名称 exec(child_argv[0], child_argv); exit(0); } else { // 在父进程中，跳过buf中的\\n后，是一条新命令的开始 p = \u0026amp;buf[i + 1]; wait(0); } } } exit(0); } ","permalink":"https://kerolt.github.io/p/mit6.s081lab1-utilities/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e该Lab通过实现几个命令来熟悉 xv6 及其系统调用\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003esleep\u003c/li\u003e\n\u003cli\u003epingpong\u003c/li\u003e\n\u003cli\u003eprimes\u003c/li\u003e\n\u003cli\u003efind\u003c/li\u003e\n\u003cli\u003exargs\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e官方实验指导：\u003ca href=\"https://pdos.csail.mit.edu/6.S081/2021/labs/util.html\"\u003ehttps://pdos.csail.mit.edu/6.S081/2021/labs/util.html\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e个人代码实现仓库：\u003ca href=\"https://github.com/kerolt/xv6-labs-2023/\"\u003ehttps://github.com/kerolt/xv6-labs-2023\u003c/a\u003e\u003c/p\u003e","title":"【MIT6.S081】Lab1 utilities"},{"content":" 该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\n这个Project需要我们实现一个缓存池，减少对于磁盘的频繁IO。开始慢慢上强度了，细节拉满！\nTask1 - LRU-K Replacement Policy 什么是LRU算法？LRU是Least Recently Used的缩写，即最近最少使用，是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段，用来记录一个页面自上次被访问以来所经历的时间 t，当须淘汰一个页面时，选择现有页面中其 t 值最大的，即最近最少使用的页面予以淘汰。\nLRU-K是一种增强型的LRU，其主要思想是利用历史访问模式来进行页面置换决策，具体过程如下：\n访问记录：每个页面维护一个访问时间戳列表，记录其最近的k次访问时间。这个列表用于计算向后k距离。 向后k距离：当一个页面被访问时，算法会更新其时间戳列表，并计算向后k距离。向后k距离是当前时间戳与列表中第k个时间戳之间的差值。如果列表中没有k个时间戳，该页面的向后k距离被赋值为正无穷。 页面替换： 在需要替换页面时，算法会遍历所有页面，找到具有最大向后k距离的页面进行替换。 如果存在多个页面的向后k距离为正无穷，算法将选择这些页面中最早的访问时间戳进行替换。 优点：LRU-K相较于传统的LRU算法更为智能，它能更好地适应不同的访问模式，减少不必要的页面置换。通过考虑历史访问，它能够识别出哪些页面可能会被频繁使用，从而提高缓存的命中率。 在BusTub的实现中，LRU-K有着每个frame id与LRUKNode的映射。\n1 2 3 4 5 6 7 class LRUKNode { private: [[maybe_unused]] std::list\u0026lt;size_t\u0026gt; history_; [[maybe_unused]] size_t k_; [[maybe_unused]] frame_id_t fid_; [[maybe_unused]] bool is_evictable_{false}; }; 一个node记录了一个frame的id，还有这个frame在最近使用k次的时间戳。\nEvict(frame_id_t* frame_id) : 与所有其他可删除的frame相比，删除具有最大向后k距离的frame。将frame id存储在输出参数中并返回True。如果没有可替换的frame，则返回False。具有少于k个访问记录的frame被给定“无穷”作为其向后k距离。如果多个帧具有inf向后k距离，则根据LRU驱逐具有最早时间戳的帧。成功删除帧应减小替换器的大小并删除帧的访问历史记录。 RecordAccess(frame_id_t frame_id) : 每次调用RecordAccess，内置的时间戳就要+1，并且根据frame_id是否在LRU缓存中来决定是更新对应的node还是添加一个新node。 Add：新建一个node，并且初始化，再把node插入缓存 Update：通过frame id找到对应的node，并修改node的历史访问时间戳，更新它在缓存中的访问位置 Remove(frame_id_t frame_id) : 从LRU缓存中删除frame id以及对应的node。要注意删除后对当前“可替换的frame数量”-1。 SetEvictable(frame_id_t frame_id, bool set_evictable) : 控制frame是否可逐出。它还控制着LRUKReplacer的size。 Size() : 返回LRUKReplacer中当前可替换的frame数量。 总的来说，把这个task当成力扣上的一道数据结构设计题来完成就好了。\nTask2 - Disk Scheduler 比较简单，实现两个函数的功能：\nSchedule(DiskRequest r)：调度DiskManager执行的请求。DiskRequest结构体指定请求是否为读/写，数据应写入/从何处写入，以及操作的页面ID。DiskRequest还包括一个std::promise，一旦请求被处理，其值应设置为true。 StartWorkerThread()：启动处理请求的后台工作线程。在DiskScheduler构造函数中创建工作线程并调用此方法。此方法负责获取排队的请求并将其分派给DiskManager。记住设置DiskRequest回调的值，以向请求发出者发出请求已完成的信号。在调用DiskScheduler的析构函数之前，这不应该返回。 Task3 - Buffer Pool Manager BufferPoolManager内部采用一个原始数组来存放Page的指针。初始时，每个page都在free list中。\n同一个块，在内存中称作“帧（frame）”，在硬盘中称作“页（page）”。缓冲池中会存储pool_size个Page，这些个Page包含硬盘上的数据和一些元信息。\nBufferPoolManager中的page_table_采用page_id作为键，frame_id作为值。如果有某个frame被使用了，那么page_table_中就会插入相应的键值对。BufferPoolManager中的pages_的下标即代表了frame id，我们通过page id可以找到对应的frame id，再通过frame id就可以找到在pages_中的Page了。\n下图来源：https://www.qtmuniao.com/2021/02/10/cmu15445-project1-buffer-pool/\n我们可以从freelist或者replacer中找到frame（优先从freelist中找）。如果free list中有空闲的frame，则使用它；否则从replacer中选出需要换出的frame（replacer的size与buffer pool的size相同）。\n要实现的每个函数在代码头文件中都有较为详细的实现过程，需要注意很多细节。\n需要注意:\n每次执行FetchPage时相当于要使用到某一个页面，既然如此，也要在LRU-K缓存中更新历史访问序列（通过调用RecordAccess）； 需要新的frame时，优先从free list中找，其次通过replacer的Evict来获取； 对于页面的pin_count_，只有当某个函数返回的是Page*时，说明这个页是需要使用的，此时其pin_count_ + 1；而只有在Unpin中，才会对pin_count_ - 1； 如果需要替换或新建或删除某个页面时，其脏位位true，则要及时写回磁盘； 错误记录 错误点1：没有保证LRU-K中的原子性 对于当前可替换的frame数量的操作应该是原子性的，可以用atomic_size_t来取代size_t，或者使用锁。\n错误点2：最大向前K距离的计算错误 The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. A frame with fewer than k historical accesses is given +inf as its backward k-distance. When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).\n主要是对这段话的理解不到位，之前是这样：\n1 2 3 4 5 6 7 8 9 // 计算最大向后K距离 int k_dist = node.history_.size() \u0026lt; k_ ? std::numeric_limits\u0026lt;int\u0026gt;::max() : (current_timestamp_ - node.history_[k_ - 1]); if (k_dist \u0026gt; max_k_dist) { evict_frame_id = fid; max_k_dist = k_dist; } else if (k_dist == max_k_dist \u0026amp;\u0026amp; node.history_.back() \u0026lt; node_store_[evict_frame_id].history_.back()) { evict_frame_id = fid; } 但实际应该为：\n1 2 3 4 5 6 7 8 9 // 计算最大向后K距离 int k_dist = node.history_.size() \u0026lt; k_ ? std::numeric_limits\u0026lt;int\u0026gt;::max() : (current_timestamp_ - node.history_[k_ - 1]); if (k_dist \u0026gt; max_k_dist) { evict_frame_id = fid; max_k_dist = k_dist; } else if (k_dist == max_k_dist \u0026amp;\u0026amp; node.history_.back() \u0026lt; node_store_[evict_frame_id].history_.back()) { evict_frame_id = fid; } 当有多个距离为无穷大的frame时，应该选择其历史记录中具有最小时间戳的那个frame！\n错误3：FetchPage找到直接返回时没有pin一下 这里卡了几个测试是因为FetchPage在page_id在缓冲池中时会直接返回，但是我没有在这种情况中对这个page的pin_count_进行+1操作。\n错误4：Unpin中的is_dirty参数的设置 在Unpin中，不能直接将page.is_dirty_设置成参数is_dirty，而是应该用或操作：page.is_dirty_ |= is_dirty;。如果不这样做，那么当原先page.is_dirty_为true时，如果我们通过Unpin设置了false，其is_dirty_就变为了false，但是这个页面仍然是脏页面。\n错误5：FetchPage没有RecordAccess 在FetchPage中，如果缓冲池中有 page id 直接返回时，replacer_也应该执行RecordAccess。因为这时相当于使用了Page，当然要记录更新。\n最终提交 然后Leaderboard的排名有点低了，因为之前都是使用的大锁，希望之后能优化一下。\n小结 这个Project差不多搞了四五天，主体代码用了两天左右，然后就是漫长的修Bug。由于从这个Project开始，就不会在本地给出完整的测试集了，所以在评测平台上也是提交了很多次来检测。通过Discord中的频道，也是找到了一些解决Bug的办法，很多时候是一些细节处自己没有考虑到（错误记录）。好在最后也基本上是自己独立完成的，有点小小的成就感！\n","permalink":"https://kerolt.github.io/p/cmu15-445-fall2023project1-buffer-pool-%E5%B0%8F%E7%BB%93/","summary":"\u003cblockquote\u003e\n\u003cp\u003e该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这个Project需要我们实现一个缓存池，减少对于磁盘的频繁IO。开始慢慢上强度了，细节拉满！\u003c/p\u003e","title":"【CMU15-445 Fall2023】Project1 Buffer Pool 小结"},{"content":" 该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\nproject0只在task4中浅浅涉及了一点BusTub的内容，其他都是检测我们对于C++的一个掌握，主要涉及智能指针和C++的常用特性（dynamic_cast、std::move、并发与锁等）。\nTask1 - Copy-On-Write Trie 如果做过力扣上的208. 实现 Trie (前缀树)，实现Get和Put这两个操作会更容易。虽然这个task要求我们要使用COW，但是总体的思路是差不多的。\nGet 这个比较简单，顺序遍历key去找节点的子节点就行。值得注意的是有值节点的类型为TrieNodeWithValue，其继承自TrieNode。当遍历玩key并找到节点后，使用dynamic_cast将其转换为TrieNodeWithValue*，如果这个节点不是一个有值的节点，则dynamic_cast会转换失败返回nullptr，否则会返回转换后的指针，通过这个指针获取最终的值即可。\nPut Put的基本思路就是遍历key的同时创建相应的节点，难点在于如何实现“copy on write”。\n看官网的描述，我们要做的就是在需要修改Trie树（例如进行Put操作或者Remove操作）时，需要返回一颗新的Trie树，也就是我们当前的操作不能影响之前的Trie树的结构，这就需要使用到TrieNode::Clone()函数来拷贝一份需要修改的节点，这样才不会影响之前Trie树中的节点。同时，在这个过程中，我们需要尽可能地使用已有节点，举个🌰：\n只需记住：\n本次的插入或删除操作不会影响上次的Trie树的结构，例如上图如果我使用root来访问还是原来的结构，而用new root来访问就可以访问key值为“ad”的节点的值； 尽可能利用已有的节点，上图我们为了不影响之前的Trie树，我们必须拷贝根节点下“a”路径的子节点，插入一个新的节点也需要创建，但是值为“233”和“C++”的节点我们可以利用。 Remove 与Put不同，我使用递归来进行删除，这是因为Put可以顺序遍历key值并不断向下添加节点，而Remove需要从要删除的节点不断向上进行删除。\n删除不合法的情况有：\n碰到空节点 无法再继续往下查找key（key不存在） 这时按照任务要求应该返回原来的Trie树。\nTask2 - Concurrent Key-Value Store Get 加锁获取root_，然后调用之前再Task1中实现的Get操作获取key对应的value，如果value不为nullptr，则将root和value封装为ValueGuard。\nPut、Remove 这两个操作的逻辑相同。由于之前我们实现Trie的三个操作时用到了COW，因此每次Put、Remove时返回的都是一个新的Trie，那么我们在Task2中的操作中要用全程获取写锁（ensure there is only one writer at a time），然后使用Put or Remove获取新的Trie，接着获取root_lock_，最后用获取的新的Trie更新root_。\nTask3 - Debugging 这个没什么太多需要说明的。选择Clion或VSCode配置好环境打断点Debug就行，当然使用print大法也是可以的haha。注意这个task虽然有给你单元测试文件，但是运行肯定是不通过，因为问题答案并不在源码中，我们只能在gradescope才能检测自己做的是否正确。\n（我所做的project所属课程是fall2023版本，如果是spring2023版本，task3在本地测试和gradescope上的测试可能会有区别，是随机数的问题，相关老师有在Discord上说明）\nTask4 - SQL String Functions 这个task需要我们为BusTub实现两个简单的函数：lower和upper。实现并不难，找到需要修改的位置，添加相关处理逻辑和异常操作。\n完成这个task我认为需要对string_expression.h这个文件的内容有一定理解，要明白如何判断获取的操作是lower还是upper，还有在plan_func_call.cpp中处理非法操作。总体来说不难。\n提交 本地跑了测试代码还不够，课程有为我们这些非CMU的学生准备检测平台gradescope，如何加入课程可以看这里。\n在提交之前，需要使用进行clang-tidy检测，并通过python3 gradescope_sign.py生成签名，这样才能通过平台的预检测。\n这里贴一个通关的截图hh：\n总结 这个project0我也是做了两三天（还是太菜了），在其中我更好地巩固了C++中的一些知识，例如智能指针、移动赋值、dynamic_cast。与之前做Xv6的Lab不同，这次的CMU15-445在网上基本没有关于实现的源代码，这就让我无法直接通过代码来学习了。当然这是课程的要求，希望我们能一起构建一个良好的学习氛围，鼓励我们独立思考完成，与他人交流而不是直接要代码，我觉得这是一个很锻炼自己的过程，网上大多是一些思路的介绍（我写的这篇博客也是自己实现的简单思路，希望没有违反课程的要求），我们在没思路时参考一下这些博客，然后再通过自己来完成代码，这比直接看别人写好的代码来说更能够提升自己！希望我能把接下来的几个project都完成，尽量不烂尾！\n","permalink":"https://kerolt.github.io/p/cmu15-445-fall2023project0-c-primer-%E5%B0%8F%E7%BB%93/","summary":"\u003cblockquote\u003e\n\u003cp\u003e该系列博客只是为了记录自己在写Lab时的思路，按照课程要求不会在Github和博客中公开源代码。欢迎与我一起讨论交流！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eproject0只在task4中浅浅涉及了一点BusTub的内容，其他都是检测我们对于C++的一个掌握，主要涉及智能指针和C++的常用特性（dynamic_cast、std::move、并发与锁等）。\u003c/p\u003e","title":"【CMU15-445 Fall2023】Project0 C++ Primer 小结"},{"content":" 【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 从零开始重写sylar C++高性能分布式服务器框架 和代码随想录中的 文档。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\nhook 函数的具体定义实现可以在这里查看：Github: src/hook.cpp\n该协程库框架的目标并不是做成类似 goroutine 那样，而是希望能够通过协程来提高 IO 处理的效率。因此，对于每个文件描述符 fd，我们都希望它有一个读写 IO 的超时时间。\nhook 的目的是在不重新编写代码的情况下，把老代码中的 socket IO 相关的 API 都转成异步，以提高性能。\n需要 Hook 的几类函数 在 sylar 的设计中，只针对 socket fd 进行 hook（因为我们更关心的是网络 IO），也就是如果我们操作的不是 socket fd，那么就会使用原来的 API。\nsylar 对如下三类函数进行了 hook：\nsleep 延时系列接口：包括 sleep/usleep/nanosleep。对于这些接口的 hook，只需要给 IO 协程调度器注册一个定时事件，在定时事件触发后再继续执行当前协程即可。当前协程在注册完定时事件后即可 yield 让出执行权 socket IO 系列接口：包括 read/write/recv/send\u0026hellip;等，connect 及 accept 也可以归到这类接口中。这类接口的 hook 首先需要判断操作的 fd 是否是 socket fd，以及用户是否显式地对该 fd 设置过非阻塞模式，如果不是 socket fd 或是用户显式设置过非阻塞模式，那么就不需要 hook 了，直接调用操作系统的 IO 接口即可。如果需要 hook，那么首先在 IO 协程调度器上注册对应的读写事件，等事件发生后再继续执行当前协程。当前协程在注册完 IO 事件即可 yield 让出执行权。 socket/fcntl/ioctl/close 等接口：这类接口主要处理的是边缘情况，比如分配 fd 上下文，处理超时及用户显式设置非阻塞问题。 Hook 的实现 我们 hook 的所有函数，都要与原来的 API 的行为保持一致（使用这些 hook api 的时候就好像使用的原来的 api）。例如原来的 API 的返回值通常用 0 表示成功，-1 表示失败\n在 Sylar 中，Hook 的实现通常涉及以下几个关键方面：\n一、函数指针替换\n保存原始函数指针：首先，需要保存被 Hook 函数的原始实现的函数指针。这可以通过在程序启动时或者在首次需要 Hook 的时候，获取原始函数的地址并存储起来。例如，可以定义一个与被 Hook 函数具有相同签名的函数指针变量，并将其初始化为指向原始函数的地址。 替换函数指针：然后，将被 Hook 函数的入口地址替换为自定义的 Hook 函数的地址。这样，当程序调用被 Hook 函数时，实际上会执行 Hook 函数。 二、参数传递和返回值处理\n参数传递：在 Hook 函数中，需要接收与被 Hook 函数相同的参数。这可以通过将参数直接传递给 Hook 函数，或者使用一些技术（如函数调用栈的分析）来获取参数的值。如果被 Hook 函数是 int func(int a, char* b)，那么 Hook 函数也应该具有相同的参数列表 int hook_func(int a, char* b)。 返回值处理：Hook 函数需要根据需要处理被 Hook 函数的返回值。可以选择直接返回被 Hook 函数的原始返回值，或者根据特定的逻辑修改返回值后再返回。 三、条件判断和控制\nHook 启用 / 禁用：通常会提供一种机制来启用或禁用 Hook 功能。这可以通过一个全局变量、配置文件或者运行时参数来控制。我们可以在代码中定义一个布尔变量，如 bool hook_enable，当它为真时启用 Hook 功能，为假时直接调用原始函数而不执行 Hook 函数。 特定条件下的 Hook：可以根据特定的条件来决定是否执行 Hook 函数。例如，可以检查参数的值、函数的调用者、当前的运行环境等条件，只有在满足特定条件时才执行 Hook 函数。 FdManager 我们会通过 FdContext 类（注意与 IOManager 中的 FdContext 进行区分）来保存 fd 的一些状态，例如 fd 是否关闭了，是否设置为非阻塞，其读写事件超时时间是多少等。\nsleep API 对 sleep，usleep，nanosleep 三个函数进行 hook 操作，其逻辑一致：sleep 类函数会阻塞当前线程，那么我们的改造方法就是用一个定时器来代替 sleep 的休眠阻塞，获取当前运行的协程，然后通过 IOManager 添加一个定时器，规定时间后再将这个协程加入调度，之后 yield 这个协程。\nsocket API socket：当使用 socket 创建套接字 fd 时，我们需要将它加入到 FdManager 中。 connect：对于原始的 connect，它是一个阻塞调用，直到连接成功或发生错误，如果网络延迟较高或目标主机不可达，可能会导致程序长时间挂起。我们需要将其改造为与异步或非阻塞操作结合。对应的实现方法就是通过设置一个超时时间，到时间后取消文件描述符的写事件。为 socket fd 添加写事件后，如果添加成功，则 yield 当前协程，并取消定时器 setsockopt：对于 optname 为 SO_RCVTIMEO 和 SO_RCVTIMEO 的情况，我们需要设置 sockfd 对应的超时时间。 socket IO API accept、read、readv、recv、recvfrom、recvmsg、write、writev、send、sendto、sendmsg 这些函数所要作的 hook 操作都很类似，不同的地方无非就是读写事件的不同，其处理逻辑和 connect 相似，所以利用了模板来减少冗余代码。\nother API 还有类似 close、ioctl、fcntl 的函数，由于我们在之前 hook api 时处理了文件描述符，因此在这些函数中我们需要对文件描述符进行清理或其他操作。\n","permalink":"https://kerolt.github.io/p/%E5%8A%A8%E6%89%8B%E5%86%99%E5%8D%8F%E7%A8%8B%E5%BA%93-5%E5%B8%B8%E7%94%A8io%E5%87%BD%E6%95%B0%E7%9A%84hook%E5%8A%9F%E8%83%BD/","summary":"\u003cblockquote\u003e\n\u003cp\u003e【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 \u003ca href=\"https://www.midlane.top/wiki/pages/viewpage.action?pageId=10060952\"\u003e从零开始重写sylar C++高性能分布式服务器框架\u003c/a\u003e 和代码随想录中的 \u003ca href=\"https://www.programmercarl.com/other/project_coroutine.html#%E4%B8%8B%E8%BD%BD%E6%96%B9%E5%BC%8F\"\u003e文档\u003c/a\u003e。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\u003c/p\u003e\n\u003cp\u003ehook 函数的具体定义实现可以在这里查看：\u003ca href=\"https://github.com/kerolt/coroutine-lib/blob/master/src/hook.cpp\"\u003eGithub: src/hook.cpp\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e该协程库框架的目标并不是做成类似 goroutine 那样，而是希望能够通过协程来提高 IO 处理的效率。因此，对于每个文件描述符 fd，我们都希望它有一个读写 IO 的超时时间。\u003c/p\u003e\n\u003cp\u003ehook 的目的是在不重新编写代码的情况下，把老代码中的 socket IO 相关的 API 都\u003cstrong\u003e转成异步\u003c/strong\u003e，以提高性能。\u003c/p\u003e","title":"【动手写协程库 5】常用IO函数的HOOK功能"},{"content":" 【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 从零开始重写sylar C++高性能分布式服务器框架 和代码随想录中的 文档。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\nIOManager 类中具体定义实现可以在这里查看：Github: src/iomanager.cpp\n之前实现的协程调度器的功能其实非常简单，当添加任务后调度器只是单纯的从任务队列中取出任务交给协程去执行。sylar 的协程库的关注对象是网络 IO，如果采用这么简单的调度就根本没有用到协程的精髓。\nsylar 的 IO 协程调度解决了之前调度器在 idle 状态下忙等待导致 CPU 占用率高的问题。IO 协程调度器使用一对管道 fd 来 tickle 调度协程，当调度器空闲时，idle 协程通过 epoll_wait 阻塞在管道的读描述符上，等管道的可读事件。添加新任务时，tickle 方法写管道，idle 协程检测到管道可读后退出，调度器执行调度。\nIOManager API IOManager 的 API 如下：\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 #ifndef IOMANAGER_H_ #define IOMANAGER_H_ #include \u0026lt;cstddef\u0026gt; #include \u0026lt;sys/epoll.h\u0026gt; #include \u0026lt;atomic\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;shared_mutex\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026#34;coroutine.h\u0026#34; #include \u0026#34;scheduler.h\u0026#34; #include \u0026#34;timer.h\u0026#34; // 事件：无、读、写 enum Event { NONE = 0x0, READ = EPOLLIN, WRITE = EPOLLOUT, }; // IO协程调度 class IOManager : public Scheduler, public TimerManager { public: IOManager(size_t threads = 1, bool use_caller = true, const std::string\u0026amp; name = \u0026#34;IOManager\u0026#34;); ~IOManager(); bool AddEvent(int fd, Event event, std::function\u0026lt;void()\u0026gt; cb = nullptr); bool DelEvent(int fd, Event event); bool CancelEvent(int fd, Event event); bool CancelAllEvent(int fd); static IOManager* GetIOManager(); protected: void Idle() override; void Tickle() override; void OnTimerInsertAtFront() override; bool IsStop() override; void ResizeContexts(size_t size); private: // socket fd 上下文 struct FdContext { struct EventContext { Scheduler* scheduler = nullptr; Coroutine::Ptr coroutine; std::function\u0026lt;void()\u0026gt; callback; }; // 根据类型获取对应的上下文 EventContext\u0026amp; GetEventContext(Event\u0026amp; e); void ResetEventContext(EventContext\u0026amp; ectx); void TriggerEvent(Event e); EventContext read_ctx, write_ctx; int fd; Event events = Event::NONE; std::mutex mutex; }; private: int epfd_; int tickle_fd_[2]; std::atomic_size_t pending_evt_cnt_; std::mutex mutex_; std::shared_mutex rw_mutex_; // 利用fd作为下标来获取对应的FdContext*，也可以使用哈希表代替 std::vector\u0026lt;FdContext*\u0026gt; fd_contexts_; }; #endif /* IOMANAGER_H_ */ Scheduler::Run() 我们可以先回顾一下 Scheduler::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 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 void Scheduler::Run() { LOG \u0026lt;\u0026lt; \u0026#34;Scheduler running...\\n\u0026#34;; SetHookFlag(true); SetThisAsScheduler(); // 如果当前线程不是调度器所在线程，设置调度的协程为当前线程运行的协程 if (std::this_thread::get_id() != sched_id_) { sched_coroutine = Coroutine::GetNowCoroutine().get(); } Coroutine::Ptr idle_co = std::make_shared\u0026lt;Coroutine\u0026gt;([this] { this-\u0026gt;Idle(); }); Coroutine::Ptr callback_co; SchedulerTask task; while (true) { task.Reset(); bool tickle = false; { std::lock_guard lock(mutex_); auto iter = tasks_.begin(); while (iter != tasks_.end()) { // 当前遍历的task已经分配了线程去执行且这个线程不是当前线程，则不用管 if (iter-\u0026gt;thread_id_ \u0026amp;\u0026amp; *iter-\u0026gt;thread_id_ != std::this_thread::get_id()) { ++iter; tickle = true; continue; } if (iter-\u0026gt;coroutine_ \u0026amp;\u0026amp; iter-\u0026gt;coroutine_-\u0026gt;GetState() != Coroutine::READY) { LOG \u0026lt;\u0026lt; \u0026#34;Coroutine task\u0026#39;s state should be READY!\\n\u0026#34;; assert(false); } task = *iter; tasks_.erase(iter++); active_threads_++; break; } // 有任务可以去执行，需要tickle一下 tickle |= (iter != tasks_.end()); } if (tickle) { Tickle(); } // 子协程执行完毕后yield会回到Run()中 // 注意，每次运行了一个task后需要Reset一下 if (task.coroutine_) { // 任务类型为协程 task.coroutine_-\u0026gt;Resume(); active_threads_--; task.Reset(); } else if (task.callback_) { // 任务类型为回调函数，将其包装为协程 if (callback_co) { callback_co-\u0026gt;Reset(task.callback_); } else { callback_co = std::make_shared\u0026lt;Coroutine\u0026gt;(task.callback_); } callback_co-\u0026gt;Resume(); active_threads_--; callback_co.reset(); task.Reset(); } else { // 无任务，任务队列为空 if (idle_co-\u0026gt;GetState() == Coroutine::FINISH) { LOG \u0026lt;\u0026lt; \u0026#34;Idle coroutine finish\\n\u0026#34;; break; } idle_threads_++; idle_co-\u0026gt;Resume(); // Idle最后Yeild时回到这里 idle_threads_--; } } LOG \u0026lt;\u0026lt; \u0026#34;Scheduler Run() exit\\n\u0026#34;; } 这个函数就是每个线程会启动的协程调度函数，负责管理和执行任务队列中的任务，包括协程和回调函数两种类型的任务，如果任务队列为空，则执行 Idle 协程。\n在 Scheduler::Idle() 函数中，仅仅只是做了一个简单的处理：调度器没有停止就让出当前正在执行的协程，我们要做的增强后的 IOManager 需要重写 Idle 函数，让它不断等待事件、处理事件、然后再次等待事件的循环过程，它在没有其他协程运行时保持系统的活跃度，并在有事件发生时进行相应的处理。\n重写 Idle 函数 在 IOManager 中，我们就需要重写 Idle 函数，我们需要它是一个不断等待事件、处理事件、然后再次等待事件的循环过程，它在没有其他协程运行时保持系统的活跃度，并在有事件发生时进行相应的处理：\n我们会先找到最近一个定时器的超时时间，并将其与自定义最长超时时间（源码中是 5s）进行比较取最小者作为 epoll_wait 的超时时间 将超时的定时器的回调函数加入调度器 处理 epoll_wait 送来的事件 将当前线程运行的协程暂停（也就是暂停 Idle 协程），并将执行权交给调度协程（Scheduler::Run()） 从 1.又开始重复执行 具体操作可看代码：\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 void IOManager::Idle() { LOG \u0026lt;\u0026lt; \u0026#34;idle coroutine start up\\n\u0026#34;; const int MAX_EVENTS = 256; const int MAX_TIMEOUT = 5000; epoll_event events[MAX_EVENTS]{}; while (true) { // LOG \u0026lt;\u0026lt; \u0026#34;in idle now\\n\u0026#34;; if (IsStop()) { LOG \u0026lt;\u0026lt; GetName() \u0026lt;\u0026lt; \u0026#34;idle stop now\\n\u0026#34;; break; } uint64_t next_timeout = GetNextTimerInterval(); int triggered_events; do { // 如果时间堆中有超时的定时器，则比较这个超时定时器的下一次触发的时间与MAX_TIMEOUT（5s），选取最小值作为超时时间 next_timeout = next_timeout != ~0ull ? std::min(static_cast\u0026lt;int\u0026gt;(next_timeout), MAX_TIMEOUT) : MAX_TIMEOUT; // 没有事件到来时会阻塞在epoll_wait上，除非到了超时时间 triggered_events = epoll_wait(epfd_, events, MAX_EVENTS, static_cast\u0026lt;int\u0026gt;(next_timeout)); if (triggered_events \u0026lt; 0 \u0026amp;\u0026amp; errno == EINTR) { continue; } else { break; } } while (true); // 用while(true)的目的是确保在出现特定错误情况时能够重新尝试执行 epoll_wait // 将超时的定时器的回调函数加入调度器 // 这些回调函数的作用可能是关闭连接等操作 std::vector\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; cbs = GetExpiredCbList(); for (auto\u0026amp; cb : cbs) { Sched(cb); } // 处理事件 for (int i = 0; i \u0026lt; triggered_events; i++) { epoll_event\u0026amp; event = events[i]; // 是一个用于通知协程调度的事件 // epoll中监听了用于通知的管道读端fd，当有数据到时即会触发 if (event.data.fd == tickle_fd_[0]) { char buf[256]{}; // 将管道内的数据读完 while (read(tickle_fd_[0], buf, sizeof(buf)) \u0026gt; 0) ; continue; } // FdContext* fd_ctx = (FdContext*) event.data.ptr; FdContext* fd_ctx = static_cast\u0026lt;FdContext*\u0026gt;(event.data.ptr); std::lock_guard lock(fd_ctx-\u0026gt;mutex); // 发生错误时，如果原来的文件描述符上下文（fd_ctx）中有可读或可写事件标志被设置，那么现在将重新触发这些事件 if (event.events \u0026amp; (EPOLLERR | EPOLLHUP)) { event.events |= (EPOLLIN | EPOLLOUT) \u0026amp; fd_ctx-\u0026gt;events; } // 获取fd_ctx对应的事件 int real_event = Event::NONE; if (event.events \u0026amp; EPOLLIN) { real_event |= Event::READ; } if (event.events \u0026amp; EPOLLOUT) { real_event |= Event::WRITE; } if ((fd_ctx-\u0026gt;events \u0026amp; real_event) == Event::NONE) { continue; } // 如果还有剩余事件，则修改；否则将其从epoll中删除 // 注意获取rest_events时不是使用的event.events \u0026amp; ~real_event，因为是要去除fd_ctx-\u0026gt;fd中本次触发的事件 int rest_events = fd_ctx-\u0026gt;events \u0026amp; ~real_event; int op = rest_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL; event.events = EPOLLET | rest_events; if (epoll_ctl(epfd_, op, fd_ctx-\u0026gt;fd, \u0026amp;event) \u0026lt; 0) { LOG_ERROR \u0026lt;\u0026lt; strerror(errno) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; continue; } if (real_event \u0026amp; Event::READ) { fd_ctx-\u0026gt;TriggerEvent(Event::READ); --pending_evt_cnt_; } if (real_event \u0026amp; Event::WRITE) { fd_ctx-\u0026gt;TriggerEvent(Event::WRITE); --pending_evt_cnt_; } } // 将当前线程运行的协程暂停（也就是暂停Idle协程），并将执行权交给调度协程 Coroutine::Ptr co_ptr = Coroutine::GetNowCoroutine(); auto co = co_ptr.get(); co_ptr.reset(); co-\u0026gt;Yield(); } } 添加事件 IOManager 除了重写 Idle 函数这个重要点外，还有个重要点就是为指定文件描述符添加事件。\nIOManager 内部有一个 [FdContext](#IOManger API) 结构体用来封装 socket fd 的上下文（需要绑定的回调函数，对应的事件、协程），并使用一个 vector 保存这些 FdContext。\n我们在添加 fd 的事件时，需要将其加入 vector 中，并且需要通过 epoll_ctl 注册 fd 的对应事件。\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 bool IOManager::AddEvent(int fd, Event event, std::function\u0026lt;void()\u0026gt; cb) { FdContext* fd_ctx = nullptr; { std::shared_lock rw_lock(rw_mutex_); if (fd_contexts_.size() \u0026gt; fd) { fd_ctx = fd_contexts_[fd]; rw_lock.unlock(); } else { rw_lock.unlock(); std::unique_lock rw_lock2(rw_mutex_); ResizeContexts(fd * 1.5); fd_ctx = fd_contexts_[fd]; } } std::lock_guard lock(mutex_); if (fd_ctx-\u0026gt;events \u0026amp; event) { LOG_ERROR \u0026lt;\u0026lt; \u0026#34;A fd can\u0026#39;t add same event\\n\u0026#34;; return false; } int op = fd_ctx-\u0026gt;events ? EPOLL_CTL_MOD : EPOLL_CTL_ADD; epoll_event ep_evt{}; ep_evt.events = static_cast\u0026lt;int\u0026gt;(fd_ctx-\u0026gt;events) | EPOLLET | event; ep_evt.data.ptr = fd_ctx; // 在Idle()中将使用fd对应的这个ep_evt int ret = epoll_ctl(epfd_, op, fd, \u0026amp;ep_evt); if (ret) { LOG_ERROR \u0026lt;\u0026lt; \u0026#34;epoll_ctl \u0026#34; \u0026lt;\u0026lt; strerror(errno); return false; } ++pending_evt_cnt_; // 设置fd对应事件的EventContext fd_ctx-\u0026gt;events = static_cast\u0026lt;Event\u0026gt;(fd_ctx-\u0026gt;events | event); // 使用event_ctx相当于使用fd_ctx-\u0026gt;read_ctx or fd_ctx-\u0026gt;write_ctx（注意是auto\u0026amp;而不是auto） auto\u0026amp; event_ctx = fd_ctx-\u0026gt;GetEventContext(event); assert(!event_ctx.scheduler \u0026amp;\u0026amp; !event_ctx.callback \u0026amp;\u0026amp; !event_ctx.coroutine); event_ctx.scheduler = Scheduler::GetScheduler(); if (cb) { event_ctx.callback = cb; } else { // 设置fd相关事件触发时使用的协程为当前 event_ctx.coroutine = Coroutine::GetNowCoroutine(); assert(event_ctx.coroutine-\u0026gt;GetState() == Coroutine::RUNNING); } return true; } ","permalink":"https://kerolt.github.io/p/%E5%8A%A8%E6%89%8B%E5%86%99%E5%8D%8F%E7%A8%8B%E5%BA%93-4io%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%99%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 \u003ca href=\"https://www.midlane.top/wiki/pages/viewpage.action?pageId=10060952\"\u003e从零开始重写sylar C++高性能分布式服务器框架\u003c/a\u003e 和代码随想录中的 \u003ca href=\"https://www.programmercarl.com/other/project_coroutine.html#%E4%B8%8B%E8%BD%BD%E6%96%B9%E5%BC%8F\"\u003e文档\u003c/a\u003e。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eIOManager\u003c/code\u003e 类中具体定义实现可以在这里查看：\u003ca href=\"https://github.com/kerolt/coroutine-lib/blob/master/src/iomanager.cpp\"\u003eGithub: src/iomanager.cpp\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e之前实现的协程调度器的功能其实非常简单，当添加任务后调度器只是单纯的从任务队列中取出任务交给协程去执行。sylar 的协程库的关注对象是网络 IO，如果采用这么简单的调度就根本没有用到协程的精髓。\u003c/p\u003e\n\u003cp\u003esylar 的 IO 协程调度解决了之前调度器在 idle 状态下忙等待导致 CPU 占用率高的问题。IO 协程调度器使用一对管道 fd 来 tickle 调度协程，当调度器空闲时，idle 协程通过 epoll_wait 阻塞在管道的读描述符上，等管道的可读事件。添加新任务时，tickle 方法写管道，idle 协程检测到管道可读后退出，调度器执行调度。\u003c/p\u003e","title":"【动手写协程库 4】IO协程调度器"},{"content":" 【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 从零开始重写sylar C++高性能分布式服务器框架 和代码随想录中的 文档。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\nTimerManager 类中具体定义实现可以在这里查看：Github: src/timer.cpp\n通过定时器，我们可以实现给服务器注册定时事件。sylar 的定时器采用最小堆设计，所有定时器根据绝对的超时时间点（也就是超时到期的具体时间戳）进行排序，每次取出离当前时间最近的一个超时时间点，计算出超时需要等待的时间，然后等待超时。超时时间到后，获取当前的绝对时间点，然后把最小堆里超时时间点小于这个时间点的定时器都收集起来，执行它们的回调函数。\n定时器相关 API 如下：\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 class Timer : public std::enable_shared_from_this\u0026lt;Timer\u0026gt; { friend class TimerManager; public: using Ptr = std::shared_ptr\u0026lt;Timer\u0026gt;; bool Cancel(); bool Refresh(); bool Reset(uint64_t ms, bool from_now); private: Timer(uint64_t ms, std::function\u0026lt;void()\u0026gt; cb, bool recur, TimerManager* manager); Timer(uint64_t next); private: bool is_recur_; // 是否循环定时器 uint64_t exec_cycle_; // 执行周期 uint64_t next_; // 下一次的到期时间 std::function\u0026lt;void()\u0026gt; callback_; TimerManager* manager_; struct Comp { bool operator()(const Timer::Ptr\u0026amp; lt, const Timer::Ptr\u0026amp; rt) const { if (!lt || !rt) { return !lt \u0026amp;\u0026amp; rt; } return lt-\u0026gt;next_ \u0026lt; rt-\u0026gt;next_; } }; }; class TimerManager { friend class Timer; public: TimerManager(); virtual ~TimerManager(); // public 添加定时器 Timer::Ptr AddTimer(uint64_t ms, std::function\u0026lt;void()\u0026gt; cb, bool is_recur = false); // 添加条件定时器，如果条件成立则定时器才有效 Timer::Ptr AddConditionTimer(uint64_t ms, std::function\u0026lt;void()\u0026gt; cb, std::weak_ptr\u0026lt;void\u0026gt; cond, bool is_recur = false); // 获取下一个定时器到现在的执行间隔时间 // 如果没有定时器了，就返回uint64_t的最大值 uint64_t GetNextTimerInterval(); // 获取需要执行的定时器的回调函数列表 std::vector\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; GetExpiredCbList(); // 是否还有定时器 bool HasTimer(); protected: virtual void OnTimerInsertAtFront() = 0; void AddTimer(Timer::Ptr timer, std::shared_lock\u0026lt;std::shared_mutex\u0026gt;\u0026amp; lock); private: // 系统时钟是否出现了回绕（rollover）现象，即当前时间比之前记录的时间要小很多 // 用于检测服务器时间是否被调后了 bool DetectClockRollover(uint64_t now_ms); private: std::shared_mutex rw_mutex_; std::set\u0026lt;Timer::Ptr, Timer::Comp\u0026gt; timer_heap_; bool is_tickled_; uint64_t pre_exec_time_; }; 个人感觉最重要的 API 是 AddTimer、GetNextTimerInterval 和 GetExpiredCbList。\nAddTimer 向时间堆中添加超时超时时间到了后的回调函数（利用 Timer 类来封装）。 GetNextTimerInterval 用于获取下一个定时器到现在的执行间隔时间，这会用于 IOManager::Idle() 中用于与规定的最大超时时间进行比较，用较小者作为 epoll_wait 的超时时间参数。 GetExpiredCbList 获取的是所有超时定时器的回调函数。在 IOManager::Idle() 会调用这个函数将所有超时定时器的回调函数作为调度任务加入任务队列进行处理。 在定时器中，使用 GetElapsedMS() 来获取系统自启动来经过的时间，其内部使用 clock_gettime 来获取时间，这相比于一些传统时间获取函数（如 time 或 gettimeofday）有更高的精度：\n1 2 3 4 5 6 // 获取系统自启动来经过的时间 static uint64_t GetElapsedMS() { struct timespec ts = {0}; clock_gettime(CLOCK_MONOTONIC_RAW, \u0026amp;ts); return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } ","permalink":"https://kerolt.github.io/p/%E5%8A%A8%E6%89%8B%E5%86%99%E5%8D%8F%E7%A8%8B%E5%BA%93-3%E5%AE%9A%E6%97%B6%E5%99%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 \u003ca href=\"https://www.midlane.top/wiki/pages/viewpage.action?pageId=10060952\"\u003e从零开始重写sylar C++高性能分布式服务器框架\u003c/a\u003e 和代码随想录中的 \u003ca href=\"https://www.programmercarl.com/other/project_coroutine.html#%E4%B8%8B%E8%BD%BD%E6%96%B9%E5%BC%8F\"\u003e文档\u003c/a\u003e。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eTimerManager\u003c/code\u003e 类中具体定义实现可以在这里查看：\u003ca href=\"https://github.com/kerolt/coroutine-lib/blob/master/src/timer.cpp\"\u003eGithub: src/timer.cpp\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e通过定时器，我们可以实现给服务器注册定时事件。sylar 的定时器采用最小堆设计，所有定时器根据绝对的超时时间点（也就是超时到期的具体时间戳）进行排序，每次取出离当前时间\u003cstrong\u003e最近的一个超时时间点\u003c/strong\u003e，计算出超时需要等待的时间，然后等待超时。超时时间到后，获取当前的绝对时间点，然后\u003cstrong\u003e把最小堆里超时时间点小于这个时间点的定时器都收集起来\u003c/strong\u003e，执行它们的回调函数。\u003c/p\u003e","title":"【动手写协程库 3】定时器"},{"content":"最近的练手项目web-terminal中（也就是一个网页终端，可执行一些命令），在按下键盘后会显示可能匹配的命令列表（假设对应的函数是setHintList），这不仅是按下字母按键会触发，按下删除键、tab键都会触发。那就不得不考虑一个问题，如果我们手速太快，那么setHintList就会频繁触发，但我们只需要响应用户最后一次输入的命令即可，虽然在这个小项目中没啥问题，但是由此可以引出一些对于以后大项目的考虑：如何减小这种多次频繁执行函数带来的性能开销问题？那就是函数防抖~\n函数防抖是一种优化技术，用来限制某个函数在一定时间内被调用的频率。当事件被触发后，它会等待一段时间，如果在这段时间内再次被触发，那么它会重新开始等待。\n下面是一种实现方式：\n1 2 3 4 5 6 7 8 9 10 11 12 export function buildDebounce(fn: (...arg: any[]) =\u0026gt; any, duration: number = 300) { let timer = -1; return function (this: unknown, ...args: any[]) { if (timer \u0026gt; -1) { clearTimeout(timer); } timer = window.setTimeout(() =\u0026gt; { fn.bind(this)(...args); timer = -1; }, duration); }; } buildDebounce接受一个函数fn和一个可选的时间间隔duration（默认为 300 毫秒）作为参数，并返回一个新的函数。\n内部使用了一个变量timer来跟踪定时器的引用。初始值为 -1，表示没有定时器在运行。 返回的函数在被调用时，首先检查timer是否大于 -1。如果是，说明之前已经有一个定时器在运行，此时会调用clearTimeout清除这个定时器，以取消之前可能正在等待执行的函数调用。 然后，使用window.setTimeout创建一个新的定时器，在duration毫秒后执行传入的函数fn，并通过bind方法确保函数在正确的上下文中执行。 当定时器执行完函数后，将timer重置为 -1，表示没有定时器在运行。 在(this: unknown, ...args: any[])中，this的值是在调用由buildDebounce返回的函数时，根据调用的上下文确定的。也就是当这个返回的函数被调用时，通过保留传入的 this 值，可以确保在最终执行被包裹的函数 fn 时，fn 能够在正确的上下文中执行。\nbuildDebounce的使用如下：\n1 2 3 4 5 6 7 8 9 function printMessage(message) { console.log(message); } const debouncedPrint = buildDebounce(printMessage); debouncedPrint(\u0026#39;Hello\u0026#39;); debouncedPrint(\u0026#39;World\u0026#39;); // 如果在 300 毫秒内连续调用 debouncedPrint，只有最后一次调用会在 300 毫秒后执行打印操作。 ","permalink":"https://kerolt.github.io/p/%E5%87%BD%E6%95%B0%E9%98%B2%E6%8A%96/","summary":"\u003cp\u003e最近的练手项目web-terminal中（也就是一个网页终端，可执行一些命令），在按下键盘后会显示可能匹配的命令列表（假设对应的函数是\u003ccode\u003esetHintList\u003c/code\u003e），这不仅是按下字母按键会触发，按下删除键、tab键都会触发。那就不得不考虑一个问题，如果我们手速太快，那么\u003ccode\u003esetHintList\u003c/code\u003e就会频繁触发，但我们只需要响应用户最后一次输入的命令即可，虽然在这个小项目中没啥问题，但是由此可以引出一些对于以后大项目的考虑：如何减小这种多次频繁执行函数带来的性能开销问题？那就是函数防抖~\u003c/p\u003e","title":"函数防抖"},{"content":"C++ 中，std::enable_shared_from_this类模板和shared_from_this成员函数主要用于在一个类的成员函数中安全地获取指向自身的std::shared_ptr。它们的作用更多是为了确保资源正确管理。\n当一个对象被多个std::shared_ptr管理时，如果在对象内部的成员函数中直接创建新的std::shared_ptr指向自身（也就是this），可能会导致多个独立的引用计数，从而无法正确管理对象的生命周期。而使用shared_from_this可以确保所有指向该对象的std::shared_ptr共享同一个引用计数，从而正确管理对象的生命周期。\n例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;iostream\u0026gt; #include \u0026lt;memory\u0026gt; class MyClass : public std::enable_shared_from_this\u0026lt;MyClass\u0026gt; { public: void print() { std::shared_ptr\u0026lt;MyClass\u0026gt; ptr = shared_from_this(); std::cout \u0026lt;\u0026lt; \u0026#34;Use count: \u0026#34; \u0026lt;\u0026lt; ptr.use_count() \u0026lt;\u0026lt; std::endl; } }; int main() { std::shared_ptr\u0026lt;MyClass\u0026gt; obj = std::make_shared\u0026lt;MyClass\u0026gt;(); obj-\u0026gt;print(); return 0; } print函数中使用shared_from_this获取指向自身的std::shared_ptr，确保了与外部创建的std::shared_ptr共享同一个引用计数。\n如果在对象内部的成员函数中直接返回一个指向自身的普通指针，当外部的std::shared_ptr被销毁后，这个普通指针就会变成悬空指针。而使用shared_from_this可以避免这种情况，因为它返回的std::shared_ptr会在引用计数为零时自动释放对象所占用的内存。例如这里不使用enable_shared_from_this和shared_from_this，则对象会多次释放，导致程序出错。\n代码及其执行结果可参见：这里\n","permalink":"https://kerolt.github.io/p/enable_shared_from_this%E7%9A%84%E4%BD%9C%E7%94%A8/","summary":"\u003cp\u003eC++ 中，\u003ccode\u003estd::enable_shared_from_this\u003c/code\u003e类模板和\u003ccode\u003eshared_from_this\u003c/code\u003e成员函数主要用于在一个类的成员函数中安全地获取指向自身的\u003ccode\u003estd::shared_ptr\u003c/code\u003e。它们的作用更多是为了确保资源正确管理。\u003c/p\u003e","title":"enable_shared_from_this的作用"},{"content":" 【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 从零开始重写sylar C++高性能分布式服务器框架 和代码随想录中的 文档。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\nScheduler 类中其他函数的定义可以在这里查看：Github: src/scheduler.cpp\nSylar 的协程调度器是一个 N-M 模型，意味着 N 个线程可以运行 M 个协程，协程能够在线程之间进行切换，也可以被绑定到特定的线程上执行。\n调度器可以由应用程序中的任何线程创建，但创建它的线程（称为 caller 线程）可以选择是否参与协程的调度。如果 caller 线程参与调度，那么调度器的线程数会相应减少一个，因为 caller 线程本身也会作为一个调度线程。\nScheduler 相关 API 如下：\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 #ifndef SCHEDULER_H_ #define SCHEDULER_H_ #include \u0026lt;atomic\u0026gt; #include \u0026lt;cstddef\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;list\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;utility\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026#34;coroutine.h\u0026#34; #include \u0026#34;util.h\u0026#34; // 协程调度器 class Scheduler { private: // 调度任务，任务类型可以是协程/函数二选一，并且可指定调度线程 using ThreadIdPtr = std::shared_ptr\u0026lt;std::thread::id\u0026gt;; struct SchedulerTask { ThreadIdPtr thread_id_; Coroutine::Ptr coroutine_; std::function\u0026lt;void()\u0026gt; callback_; SchedulerTask() {} SchedulerTask(Coroutine::Ptr co, ThreadIdPtr id) : coroutine_(co) , thread_id_(std::move(id)) {} SchedulerTask(std::function\u0026lt;void()\u0026gt; callback, ThreadIdPtr id) : callback_(callback) , thread_id_(std::move(id)) {} void Reset() { thread_id_.reset(); callback_ = nullptr; coroutine_ = nullptr; } }; public: Scheduler(size_t threads = 1, bool use_caller = true, const std::string\u0026amp; name = \u0026#34;scheduler\u0026#34;); virtual ~Scheduler(); std::string GetName() const { return name_; } void Start(); void Stop(); template \u0026lt;typename TaskType\u0026gt; void Sched(TaskType t, ThreadIdPtr id = nullptr) requires(std::invocable\u0026lt;TaskType\u0026gt; || std::same_as\u0026lt;TaskType, Coroutine::Ptr\u0026gt;) { bool is_need_tick = false; { std::lock_guard lock(mutex_); is_need_tick = tasks_.empty(); SchedulerTask task(t, id); if (task.callback_ || task.coroutine_) { tasks_.push_back(task); } } if (is_need_tick) { Tickle(); } } public: static Scheduler* GetScheduler(); static Coroutine* GetSchedCoroutine(); protected: virtual void Tickle(); void Run(); void SetThisAsScheduler(); virtual void Idle(); virtual bool IsStop(); bool HasIdleThreads() { return idle_threads_ \u0026gt; 0; } private: std::string name_; std::mutex mutex_; std::vector\u0026lt;std::thread\u0026gt; thread_pool_; std::vector\u0026lt;std::thread::id\u0026gt; thread_ids_; std::list\u0026lt;SchedulerTask\u0026gt; tasks_; size_t threads_size_; std::atomic_size_t active_threads_{0}; std::atomic_size_t idle_threads_{0}; std::thread::id sched_id_; // use_caller为true时，调度器所在的线程id Coroutine::Ptr sched_co_; // use_caller为true时调度器所在线程的调度协程 bool is_stop_; bool is_use_caller_; }; #endif /* SCHEDULER_H_ */ 调度器的工作流大致为：\n协程调度器在初始化时可传入线程数和一个布尔型的 use_caller 参数，表示是否使用 caller 线程。在使用 caller 线程的情况下，线程数自动减一，并且调度器内部会初始化一个属于 caller 线程的调度协程并保存起来（比如，在 main 函数中创建的调度器，如果 use_caller 为 true，那调度器会初始化一个属于 main 函数线程的调度协程）。 调度器创建好后 ，即可调用调度器的 Sched 函数向调度器添加调度任务，但此时调度器并不会立刻执行这些任务，而是将它们保存到内部的一个任务队列中。 调用 Scheduler::Start() 函数启动调度 。调用 Start 会创建调度线程池，线程数量由初始化时的线程数和 use_caller 确定。调度线程一旦创建，就会立刻从任务队列里取任务执行。比较特殊的一点是，如果初始化时指定线程数为 1 且 use_caller 为 true，那么 Start 方法什么也不做，因为不需要创建新线程用于调度。并且，由于没有创建新的调度线程，那只能由 caller 线程的调度协程来负责调度协程（这里有点绕），而 caller 线程的调度协程的执行时机与 Start 函数并不在同一个地方。caller 线程的调度协程的执行时机在 Stop 函数中。 接下来是调度协程，对应 Scheduler::Run() 。调度协程负责从调度器的任务队列中取任务执行。取出的任务即子协程，每个子协程执行完后都必须返回调度协程，由调度协程重新从任务队列中取新的协程并执行。如果任务队列空了，那么调度协程会切换到一个 idle 协程，等有新任务进来时，idle 协程才会退出并回到调度协程，重新开始下一轮调度。（在 Scheduler 中，idle 函数的定义十分简单粗暴，因为实际使用协程库时并不是直接使用 Scheduler 类，而是使用它的派生类，在派生类中将会实现更为完善的调度） 接下来是添加调度任务，对应 Scheduler::Sched() ，这个方法支持传入协程或函数，并且支持一个线程 id 参数，表示是否将这个协程或函数绑定到一个具体的线程上执行。如果任务队列为空，那么在添加任务之后，要调用一次 tickle 方法以通知各调度线程的调度协程有新任务来了。在执行调度任务时，还可以通过调度器的 GetScheduler() 获取到当前调度器，再通过 Sched 函数继续添加新的任务，这就变相实现了在子协程中创建并运行新的子协程的功能。 接下来是调度器的停止。调度器的停止行为要分两种情况讨论，首先是 use_caller 为 false 的情况，这种情况下，由于没有使用 caller 线程进行调度，那么只需要简单地等各个调度线程的调度协程退出就行了。如果 use_caller 为 true，表示 caller 线程也要参于调度，这时，调度器初始化时记录的属于 caller 线程的调度协程就要起作用了，在调度器停止前，应该让这个 caller 线程的调度协程也运行一次，让 caller 线程完成调度工作后再退出。如果调度器只使用了 caller 线程进行调度，那么所有的调度任务要在调度器停止时才会被调度。 调度器中最重要的一个函数我认为就是 Run() 函数了，这个函数用于协程的调度，或者，你可以将他理解为是一个调度协程（名词）。\n创建 Scheduler 时会为每一个内部线程池中的每一个线程都绑定一个调度协程，线程数量默认为 1，此时也默认会使用 caller 线程，也就是使用的主线程。调度协程 Scheduler::Run() 会从任务队列 Task Queue 中不断去取任务去执行。如果有任务可执行，那就切换至任务协程执行，任务协程执行完毕后又切换回调度协程；无任务执行时，调度协程切换至 Idle 协程进行等待。\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 // 用于协程的调度 void Scheduler::Run() { LOG \u0026lt;\u0026lt; \u0026#34;Scheduler running...\\n\u0026#34;; SetThisAsScheduler(); // 如果当前线程不是调度器所在线程，设置调度的协程为当前线程运行的协程 if (std::this_thread::get_id() != sched_id_) { sched_coroutine = Coroutine::GetNowCoroutine().get(); } Coroutine::Ptr idle_co = std::make_shared\u0026lt;Coroutine\u0026gt;([this] { this-\u0026gt;Idle(); }); Coroutine::Ptr callback_co; SchedulerTask task; while (true) { task.Reset(); bool tickle = false; { std::lock_guard lock(mutex_); auto iter = tasks_.begin(); while (iter != tasks_.end()) { // 当前遍历的task已经分配了线程去执行且这个线程不是当前线程，则不用管 if (iter-\u0026gt;thread_id_ \u0026amp;\u0026amp; *iter-\u0026gt;thread_id_ != std::this_thread::get_id()) { ++iter; tickle = true; continue; } if (iter-\u0026gt;coroutine_ \u0026amp;\u0026amp; iter-\u0026gt;coroutine_-\u0026gt;GetState() != Coroutine::READY) { LOG \u0026lt;\u0026lt; \u0026#34;Coroutine task\u0026#39;s state should be READY!\\n\u0026#34;; assert(false); } task = *iter; tasks_.erase(iter++); active_threads_++; break; } // 有任务可以去执行，需要tickle一下 tickle |= (iter != tasks_.end()); } if (tickle) { Tickle(); } // 子协程执行完毕后yield会回到Run()中 if (task.coroutine_) { // 任务类型为协程 task.coroutine_-\u0026gt;Resume(); active_threads_--; } else if (task.callback_) { // 任务类型为回调函数 if (callback_co) { callback_co-\u0026gt;Reset(task.callback_); } else { callback_co = std::make_shared\u0026lt;Coroutine\u0026gt;(task.callback_); } callback_co-\u0026gt;Resume(); active_threads_--; } else { // 无任务，任务队列为空 if (idle_co-\u0026gt;GetState() == Coroutine::FINISH) { LOG \u0026lt;\u0026lt; \u0026#34;Idle coroutine finish\\n\u0026#34;; break; } idle_threads_++; idle_co-\u0026gt;Resume(); // Idle最后Yeild时回到这里 idle_threads_--; } } LOG \u0026lt;\u0026lt; \u0026#34;Scheduler Run() exit\\n\u0026#34;; } 这个 Scheduler 是一个很简单的调度器，要对任务做更好的调度，少不了 Idle 协程的帮助。Idle 协程的具体实现要在之后的 IOManager 中，其继承自 Scheduler，重写了 Tickle()、Idle() 等函数，并且使用 epoll 来实现在不同的 I/O 事件发生时，触发相应的处理逻辑。这使得程序可以以非阻塞的方式处理多个 I/O 操作，而不必等待每个操作完成后再进行下一个操作。\n","permalink":"https://kerolt.github.io/p/%E5%8A%A8%E6%89%8B%E5%86%99%E5%8D%8F%E7%A8%8B%E5%BA%93-2%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%99%A8/","summary":"\u003cblockquote\u003e\n\u003cp\u003e【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 \u003ca href=\"https://www.midlane.top/wiki/pages/viewpage.action?pageId=10060952\"\u003e从零开始重写sylar C++高性能分布式服务器框架\u003c/a\u003e 和代码随想录中的 \u003ca href=\"https://www.programmercarl.com/other/project_coroutine.html#%E4%B8%8B%E8%BD%BD%E6%96%B9%E5%BC%8F\"\u003e文档\u003c/a\u003e。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eScheduler\u003c/code\u003e 类中其他函数的定义可以在这里查看：\u003ca href=\"https://github.com/kerolt/coroutine-lib/blob/master/src/scheduler.cpp\"\u003eGithub: src/scheduler.cpp\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eSylar 的协程调度器是一个 N-M 模型，意味着 N 个线程可以运行 M 个协程，协程能够在线程之间进行切换，也可以被绑定到特定的线程上执行。\u003c/p\u003e\n\u003cp\u003e调度器可以由应用程序中的任何线程创建，但创建它的线程（称为 caller 线程）可以选择是否参与协程的调度。如果 caller 线程参与调度，那么调度器的线程数会相应减少一个，因为 caller 线程本身也会作为一个调度线程。\u003c/p\u003e","title":"【动手写协程库 2】协程调度器"},{"content":" 【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 从零开始重写sylar C++高性能分布式服务器框架 和代码随想录中的 文档。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\nCoroutine 类中其他函数的定义可以在这里查看：Github: src/coroutine.cpp\n对于什么是协程，为什么要使用协程，可以看看之前的笔记：【协程】C++20协程初体验。\n对于我们自己来实现协程，其实在之前 Xv6 的 Lab 中就有做过：【MIT6.S081】Lab6 multithreading，当初做这个 lab 的时候没有意识到这就是协程。协程的切换最重要的就是要保存和恢复上下文，在这个 lab 中，我们通过保存每个协程在切换之前的寄存器的值，以此可用来恢复原来的执行流。\n在 sylar 的协程库实现中，使用的是 Linux 原生提供的 ucontext 来保存协程的上下文和切换。对于协程切换，最重要的两个 API 就是 yield 和 resume，分别对应协程让出执行权和恢复协程。利用 ucontext 提供的四个函数 getcontext()、setcontext()、makecontext()、swapcontext() 可以在一个进程中实现协程切换。（这里就不介绍这几个函数的用法来，详细文档可使用 man 命令查看）\nCoroutine 的相关 API 如下：\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 #ifndef COROUTINE_H_ #define COROUTINE_H_ #include \u0026lt;sys/ucontext.h\u0026gt; #include \u0026lt;ucontext.h\u0026gt; #include \u0026lt;cstdint\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;memory\u0026gt; class Coroutine : public std::enable_shared_from_this\u0026lt;Coroutine\u0026gt; { public: using Ptr = std::shared_ptr\u0026lt;Coroutine\u0026gt;; enum State { READY, RUNNING, FINISH }; Coroutine(std::function\u0026lt;void()\u0026gt; callback, size_t stack_size = 0, bool run_in_scheduler = true); ~Coroutine(); void Yield(); void Resume(); void Reset(std::function\u0026lt;void()\u0026gt; callback); uint64_t GetId() const { return id_; } State GetState() const { return state_; } public: static void SetNowCoroutine(Coroutine* co); static Coroutine::Ptr GetNowCoroutine(); static uint64_t TotalCoNums(); static void Task(); static uint64_t GetCurrentId(); private: Coroutine(); private: uint64_t id_ = 0; uint32_t stack_size_ = 0; State state_ = READY; bool is_run_in_sched_; ucontext_t ctx_; // 协程上下文 void* pstack_ = nullptr; // 协程的栈地址 std::function\u0026lt;void()\u0026gt; callback_; }; #endif /* COROUTINE_H_ */ 一个线程可以运行多个协程，但是在某一个时刻只能运行一个协程。我们需要为每个线程设置当前运行的协程的指针和表示线程的主协程的指针。这里用到了 C++ 中的 thread_local 关键字：\n1 2 static thread_local Coroutine* cur_coroutine = nullptr; static thread_local Coroutine::Ptr main_coroutine = nullptr; 每当我们需要创建一个协程来执行任务时，我们必须要传入的参数为要执行的函数，而协程的栈大小有默认值，默认使用调度器进行调度：\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 Coroutine::Coroutine(std::function\u0026lt;void()\u0026gt; callback, size_t stack_size, bool run_in_scheduler) : callback_(callback) , is_run_in_sched_(run_in_scheduler) { co_count++; stack_size_ = stack_size \u0026gt; 0 ? stack_size : CO_STACK_SIZE; pstack_ = malloc(stack_size_); // getcontext()用于保存当前上下文，以便将来可以从这个点恢复执行 if (getcontext(\u0026amp;ctx_) != 0) { std::cout \u0026lt;\u0026lt; \u0026#34;err: Coroutine::getcontext\\n\u0026#34;; exit(1); } // 初始化协程上下文 ctx_.uc_link = nullptr; ctx_.uc_stack.ss_sp = pstack_; ctx_.uc_stack.ss_size = stack_size_; // 为已经初始化的上下文设置一个将在该上下文被激活时执行的函数，并为该函数传递参数。 // 为什么这里的第二个参数不直接设置成callback呢？是因为我们自己写协程的话不仅仅只要将任务函数执行完成就行了，执行完成后还要设置协程的状态 makecontext(\u0026amp;ctx_, \u0026amp;Coroutine::Task, 0); } // 每个协程会运行它所绑定的callback，并且在执行完成后将重置该协程的状态，并让出执行权 void Coroutine::Task() { auto cur = GetNowCoroutine(); assert(cur); cur-\u0026gt;callback_(); cur-\u0026gt;callback_ = nullptr; cur-\u0026gt;state_ = FINISH; auto raw_ptr = cur.get(); cur.reset(); raw_ptr-\u0026gt;Yield(); } Yield 和 Resume 函数利用 swapcontext 来进行协程的切换。由于我们在之后会需要将协程添加到调度器中而不是手动调度，所以要注意协程有使用调度器标志时要与调度器协程进行切换：\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 // 让出该协程的执行权，转交到主协程 void Coroutine::Yield() { assert(state_ == FINISH || state_ == RUNNING); SetNowCoroutine(main_coroutine.get()); if (state_ != FINISH) { state_ = READY; } if (is_run_in_sched_) { if (swapcontext(\u0026amp;ctx_, \u0026amp;(Scheduler::GetSchedCoroutine()-\u0026gt;ctx_)) != 0) { std::cout \u0026lt;\u0026lt; \u0026#34;err: Yield::swapcontext\\n\u0026#34;; assert(false); } } else { if (swapcontext(\u0026amp;ctx_, \u0026amp;(main_coroutine-\u0026gt;ctx_)) \u0026lt; 0) { std::cout \u0026lt;\u0026lt; \u0026#34;err: Yield::swapcontext\\n\u0026#34;; exit(1); } } } // 从当前运行的协程恢复到该协程 void Coroutine::Resume() { assert(state_ != FINISH \u0026amp;\u0026amp; state_ != RUNNING); // 每次恢复时需要将当前运行的协程设置为自身 SetNowCoroutine(this); state_ = RUNNING; if (is_run_in_sched_) { if (swapcontext(\u0026amp;(Scheduler::GetSchedCoroutine()-\u0026gt;ctx_), \u0026amp;ctx_) != 0) { std::cout \u0026lt;\u0026lt; \u0026#34;err: Resume::swapcontext\\n\u0026#34;; assert(false); } } else { if (swapcontext(\u0026amp;(main_coroutine-\u0026gt;ctx_), \u0026amp;ctx_) != 0) { std::cout \u0026lt;\u0026lt; \u0026#34;err: Resume::swapcontext\\n\u0026#34;; exit(1); } } } ","permalink":"https://kerolt.github.io/p/%E5%8A%A8%E6%89%8B%E5%86%99%E5%8D%8F%E7%A8%8B%E5%BA%93-1%E5%8D%8F%E7%A8%8B%E5%AE%9A%E4%B9%89/","summary":"\u003cblockquote\u003e\n\u003cp\u003e【动手写协程库】系列笔记是学习 sylar 的协程库时的记录，参考了 \u003ca href=\"https://www.midlane.top/wiki/pages/viewpage.action?pageId=10060952\"\u003e从零开始重写sylar C++高性能分布式服务器框架\u003c/a\u003e 和代码随想录中的 \u003ca href=\"https://www.programmercarl.com/other/project_coroutine.html#%E4%B8%8B%E8%BD%BD%E6%96%B9%E5%BC%8F\"\u003e文档\u003c/a\u003e。文章并不是对所有代码的详细解释，而是为了自己理解一些片段所做的笔记。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eCoroutine\u003c/code\u003e 类中其他函数的定义可以在这里查看：\u003ca href=\"https://github.com/kerolt/coroutine-lib/blob/master/src/coroutine.cpp\"\u003eGithub: src/coroutine.cpp\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e对于什么是协程，为什么要使用协程，可以看看之前的笔记：\u003ca href=\"https://kerolt.github.io/posts/f663291a/\"\u003e【协程】C++20协程初体验\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e对于我们自己来实现协程，其实在之前 Xv6 的 Lab 中就有做过：\u003ca href=\"https://kerolt.github.io/posts/cb4e63bc/#Uthread-switching-between-threads\"\u003e【MIT6.S081】Lab6 multithreading\u003c/a\u003e，当初做这个 lab 的时候没有意识到这就是协程。协程的切换最重要的就是要保存和恢复上下文，在这个 lab 中，我们通过保存每个协程在切换之前的寄存器的值，以此可用来恢复原来的执行流。\u003c/p\u003e","title":"【动手写协程库 1】协程定义"},{"content":" Debian 系 Linux 安装 N 卡驱动参考这篇 博客\n最近 Debian 终于是装好 nvidia 的驱动了，但是只能用在 X11 上，并且相比于不用 n 卡驱动时的 Wayland，X11 下的窗口拖动和视频播放会有比较明显的撕裂感，以下是解决的一个方法。\n修改 /etc/X11/xorg.conf 文件，在屏幕相关的区域内加上：\n1 2 Option \u0026#34;TripleBuffer\u0026#34; \u0026#34;true\u0026#34; Option \u0026#34;ForceFullCompositionPipeline\u0026#34; \u0026#34;on\u0026#34; Option \u0026quot;TripleBuffer\u0026quot; \u0026quot;true\u0026quot; 启用三重缓冲（Triple Buffering）来减少屏幕撕裂，它通过增加一个额外的缓冲区来同步视频输出和显示器的刷新率。 Option \u0026quot;ForceFullCompositionPipeline\u0026quot; \u0026quot;on\u0026quot; 用于强制使用完整的合成管线（Full Composition Pipeline）。启用这个选项可以提高性能，在某些游戏和应用程序中，它可以帮助减少延迟和提高帧率。 ","permalink":"https://kerolt.github.io/p/%E8%A7%A3%E5%86%B3x11%E4%B8%8B%E7%AA%97%E5%8F%A3%E6%8B%96%E5%8A%A8%E6%92%95%E8%A3%82%E7%9A%84%E4%B8%80%E4%B8%AA%E6%96%B9%E6%B3%95/","summary":"\u003cblockquote\u003e\n\u003cp\u003eDebian 系 Linux 安装 N 卡驱动参考这篇 \u003ca href=\"https://www.if-not-true-then-false.com/2021/debian-ubuntu-linux-mint-nvidia-guide/\"\u003e博客\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e最近 Debian 终于是装好 nvidia 的驱动了，但是只能用在 X11 上，并且相比于不用 n 卡驱动时的 Wayland，X11 下的窗口拖动和视频播放会有比较明显的撕裂感，以下是解决的一个方法。\u003c/p\u003e","title":"解决X11下窗口拖动撕裂的一个方法"},{"content":"我们知道在C++20的协程中，自己实现的Coroutine中必须包含一个Promise，并且这个Promise必须要实现：\nget_return_object() initial_suspend() final_suspend() unhandled_exception() 少了其中任何一个，编译器都会报错。那这是怎么实现的呢？如果是像java那样是一个接口而没有对应的实现从而报错还能理解，但是我们的代码中的Promise完完全全是我们自己写的，也没有使用继承，编译器你怎么知道我在实现协程时少了什么东西呢？\n答案就是：SFINAE（Substitution Failure Is Not An Error，替换失败不是错误）。\nSFINAE 主要应用于函数模板的重载解析过程中。当编译器尝试选择一个合适的函数模板重载版本时，如果某个模板参数的替换导致编译错误，编译器不会立即报错，而是继续尝试其他可能的重载版本。\n在 C++20 协程中，编译器会根据协程函数的定义和 promise_type 的实现来生成协程的执行代码。promise_type 是一个用户定义的类型，它必须提供一些特定的函数，如 get_return_object、initial_suspend、final_suspend、unhandled_exception 和 return_value 等。这些函数定义了协程的行为和状态，编译器会在编译期检查这些函数的存在性和正确性，以确保协程能够正确地执行。\n编译器使用 SFINAE 机制来检查 promise_type 中是否包含特定的函数。具体来说，就是编译器会在协程的实现代码中使用表达式 SFINAE，尝试调用这些函数，并根据调用的结果来确定函数的存在性。\n我们来写个简单的例子，只检查get_return_object函数：\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 #include \u0026lt;type_traits\u0026gt; /* * check(int) 是一个重载的模板函数，用于检查U类型是否有get_return_object()函数。 * decltype(std::declval\u0026lt;U\u0026gt;().get_return_object(), std::true_type{}) 会在U具有get_return_object()时返回std::true_type。 * 如果U没有get_return_object()，则会匹配check(...)重载，返回std::false_type。 * value成员是一个常量布尔值，指示T是否具有get_return_object()函数。 */ template \u0026lt;typename T\u0026gt; struct has_get_return_object { template \u0026lt;typename U\u0026gt; static auto check(int) -\u0026gt; decltype(std::declval\u0026lt;U\u0026gt;().get_return_object(), std::true_type{}); template \u0026lt;typename\u0026gt; static std::false_type check(...); static constexpr bool value = decltype(check\u0026lt;T\u0026gt;(0))::value; }; template \u0026lt;typename T\u0026gt; void check_promise_type() { static_assert(has_get_return_object\u0026lt;T\u0026gt;::value, \u0026#34;Promise type must have a get_return_object() method.\u0026#34;); } struct MyCoroutine { struct promise_type { MyCoroutine get_return_object() { return {}; } }; }; int main() { check_promise_type\u0026lt;MyCoroutine::promise_type\u0026gt;(); } 当我们去掉Mycoroutine::promise_type中的get_return_object函数时，check_promise_type函数中就会出发断言失败，从而编译不通过。\n这也就是在将一个函数变为协程时编译器是如何检查的一个大概思路了。不得不感叹，C++真是博大精深。\n","permalink":"https://kerolt.github.io/p/%E5%8D%8F%E7%A8%8Bc-%E6%98%AF%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87sfinae%E6%9D%A5%E6%A3%80%E6%9F%A5promise_type%E7%9A%84/","summary":"\u003cp\u003e我们知道在C++20的协程中，自己实现的Coroutine中必须包含一个\u003ccode\u003ePromise\u003c/code\u003e，并且这个\u003ccode\u003ePromise\u003c/code\u003e必须要实现：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eget_return_object()\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003einitial_suspend()\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efinal_suspend()\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eunhandled_exception()\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e少了其中任何一个，编译器都会报错。那这是怎么实现的呢？如果是像java那样是一个接口而没有对应的实现从而报错还能理解，但是我们的代码中的\u003ccode\u003ePromise\u003c/code\u003e完完全全是我们自己写的，也没有使用继承，编译器你怎么知道我在实现协程时少了什么东西呢？\u003c/p\u003e\n\u003cp\u003e答案就是：\u003cstrong\u003eSFINAE\u003c/strong\u003e（Substitution Failure Is Not An Error，替换失败不是错误）。\u003c/p\u003e","title":"【协程】C++是如何通过SFINAE来检查promise_type的"},{"content":"前言 暑假里跟着鱼皮的yuindex项目写了个web终端的小玩具，完成的终端命令不多，但是大体上成型了。由于我打算做一个纯前端的项目，并没有写后端api，这样也方便我最终直接部署到Github Pages上（这样就不用花钱了hh）。\n项目使用vite构建，并使用了ts。在构建部署时遇到了一些问题，由于是第一次将前端项目发布到github pages上，这里记录下。\n生成了额外的源映射文件 例如在执行了pnpm run build后，虽然生成了dist文件夹和其中的内容，但是源文件中的.ts文件会额外生成.js和.js.map文件，.vue文件会额外生成.vue.js和.vue.js.map文件：\n很明显这些额外生成的文件我们在发布部署项目时用不上，而且也无需提交到git上。摸索一番后，知道了这是TypeScript编译器生成的，我们需要修改项目的tsconfig.json文件。\n对于.map文件：\n1 2 3 4 5 { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;sourceMap\u0026#34;: false } } 对于ts生成的对应的js文件：\n1 2 3 4 5 6 { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;allowJs\u0026#34;: false, // 禁止编译JS文件 \u0026#34;noEmit\u0026#34;: true, // 仅进行类型检查，而不输出任何文件 } } 如何部署到Github Pages 在解决完前面的问题后，我们就可以将dist目录提交到github上了（记住要在.gitignore文件中排除dist目录），这里我们可以使用：\n1 git subtree push --prefix dist origin gh-pages 这会将项目的子目录dist推送到远程仓库的gh-pages分支，如果远程仓库没有gh-pages分支，Git 会自动创建它并推送内容。\n接着在Github仓库的Settings中设置部署的位置：\nGithub Pages项目路径错误 由于kerolt.github.io用作了我的博客，这个项目只能作为子项来部署，也就是通过kerolt.github.io/web-terminal来访问。完成前面的步骤后，我虽然能访问，但是其中的内容无法显示，其原因为无法正确获取对应的文件。\n这是因为相当于是在嵌套的公共路径下部署项目，在vite中需指定 base 配置项：\n1 2 3 4 5 6 // vite.config.ts export default defineConfig({ base: \u0026#34;/web-terminal/\u0026#34;, // ... }); 至此，项目已经可以通过https://kerolt.github.io/web-terminal/来访问。\n","permalink":"https://kerolt.github.io/p/%E8%AE%B0%E4%B8%80%E6%AC%A1%E9%A1%B9%E7%9B%AE%E6%9E%84%E5%BB%BA%E9%83%A8%E7%BD%B2%E7%9A%84%E6%80%BB%E7%BB%93/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e暑假里跟着鱼皮的\u003ccode\u003eyuindex\u003c/code\u003e项目写了个web终端的小玩具，完成的终端命令不多，但是大体上成型了。由于我打算做一个纯前端的项目，并没有写后端api，这样也方便我最终直接部署到\u003cstrong\u003eGithub Pages\u003c/strong\u003e上（这样就不用花钱了hh）。\u003c/p\u003e\n\u003cp\u003e项目使用vite构建，并使用了ts。在构建部署时遇到了一些问题，由于是第一次将前端项目发布到github pages上，这里记录下。\u003c/p\u003e","title":"记一次项目构建部署的总结"},{"content":"在使用手机浏览器搜索时为了免于CSDN垃圾信息的影响，我们可以使用Bing（其他搜索引擎也有类似的功能，但是国内不借助其他方法的情况下体验感最好的也只有Bing了）的“-site:*.csdn.net”来屏蔽CSDN，但是每次输入搜索内容后还要加这么一个字符串真是太麻烦了，因此可以写一个浏览器脚本来完成这一操作。\n在PC上可以直接使用油猴中其他大佬写的脚本，而我在手机上使用Via浏览器时不一定能用（可能可以，不过我还没试～）这些脚本，同时我需要的功能比较简单，故写一个Via浏览器的脚本。\n其实主要就是一个立即执行函数，其中对于输入框注册了keydown的事件，当按下回车键时可以在输入框的文本后添加-site:*.csdn.net。需要注意的是需要使用preventDefault()方法来防止默认的表单提交动作：\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 // ==UserScript== // @name Bing CSDN Filter // @namespace https://viayoo.com/ // @version 0.0 // @description 自动在Bing搜索框输入内容后添加-csdn，屏蔽CSDN内容 // @author kerolt // @match https://*.bing.com/* // @grant none // ==/UserScript== (function() { \u0026#39;use strict\u0026#39;; // 获取搜索框中的textarea元素 let searchInput = document.querySelector(\u0026#39;textarea[name=\u0026#34;q\u0026#34;]\u0026#39;); if (searchInput) { // 监听搜索框中的按键事件 searchInput.addEventListener(\u0026#39;keydown\u0026#39;, function(event) { // 检查是否按下回车键 if (event.key === \u0026#39;Enter\u0026#39;) { // 如果输入中没有-site:*.csdn.net，自动添加 if (!searchInput.value.includes(\u0026#39;-site:*.csdn.net\u0026#39;)) { searchInput.value += \u0026#39; -site:*.csdn.net\u0026#39;; } // 提交表单，进行搜索 event.preventDefault(); // 防止默认的表单提交动作 let searchForm = searchInput.closest(\u0026#39;form\u0026#39;); if (searchForm) { searchForm.submit(); // 手动提交表单 } } }); } })(); ","permalink":"https://kerolt.github.io/p/%E5%86%99%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E6%B5%8F%E8%A7%88%E5%99%A8%E8%84%9A%E6%9C%AC%E6%9D%A5%E5%B1%8F%E8%94%BDcsdn/","summary":"\u003cp\u003e在使用手机浏览器搜索时为了免于CSDN垃圾信息的影响，我们可以使用Bing（其他搜索引擎也有类似的功能，但是国内不借助其他方法的情况下体验感最好的也只有Bing了）的“\u003ccode\u003e-site:*.csdn.net\u003c/code\u003e”来屏蔽CSDN，但是每次输入搜索内容后还要加这么一个字符串真是太麻烦了，因此可以写一个浏览器脚本来完成这一操作。\u003c/p\u003e","title":"写一个简单的浏览器脚本来屏蔽CSDN"},{"content":"在用C++刷leetcode时，我希望把一个递归函数像js、python那样写在运行函数内部，那么可以使用function和lambda表达式来实现。但如果这个递归函数的参数比较多，那么function的模板参数同样需要写很多，能不能用auto来实现得简单一点呢？\n在C++中，使用lambda表达式实现递归时，由于lambda本身没有显式的类型名，需要通过一些技巧来实现递归调用。使用auto\u0026amp;\u0026amp;作为参数类型是其中一种常见的做法：\nLambda表达式的类型推断：C++中的lambda表达式没有类型名，意味着无法直接在lambda内部调用自身。如果直接将lambda表达式定义为递归函数，会遇到无法识别的编译错误。\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;iostream\u0026gt; int main() { auto factorial = [](auto\u0026amp;\u0026amp; self, int n) -\u0026gt; int { if (n \u0026lt;= 1) return 1; return n * self(self, n - 1); // 递归调用lambda }; std::cout \u0026lt;\u0026lt; factorial(factorial, 5) \u0026lt;\u0026lt; std::endl; // 输出120 return 0; } 这里不像js、python直接定义就好了，还需要多写一个通用引用（什么是通用引用，可参考这篇博客），具体为什么我也暂时不清楚，只知道和Y组合子的知识点有关。\n","permalink":"https://kerolt.github.io/p/c-%E4%B8%AD%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8lambda%E5%AE%9E%E7%8E%B0%E9%80%92%E5%BD%92/","summary":"\u003cp\u003e在用C++刷leetcode时，我希望把一个递归函数像js、python那样写在运行函数内部，那么可以使用\u003ccode\u003efunction\u003c/code\u003e和\u003ccode\u003elambda表达式\u003c/code\u003e来实现。但如果这个递归函数的参数比较多，那么function的模板参数同样需要写很多，能不能用\u003ccode\u003eauto\u003c/code\u003e来实现得简单一点呢？\u003c/p\u003e","title":"C++中如何使用lambda实现递归"},{"content":"为什么我们需要协程？ 为什么我们有了线程还需要协程呢？（其实这个问题不应该这么问，协程的出现在线程之前）在一个进程中虽然我们可以创建多个线程，但是在一个进程中能创建的线程数量是有限制的，并且线程的调度仍然受操作系统控制，也就是说线程何时抢占、何时被抢占对于开发者来说都是透明的，并且在调度的过程中还可能涉及到用户态和内核态的切换开销。\n当我们需要去处理一个非常耗时的IO操作时（假设使用的阻塞IO），为了不阻塞当前线程，我们可能会想到新建一个线程去执行这个操作，但是线程的创建和调度也是需要消耗资源的，我们希望有更加轻便的方法，例如：在当前线程中，一个任务遇到阻塞IO时，不要傻傻的停在这里，而是暂停这个任务，转而去执行其他任务，直到IO完成再恢复之前的任务来运行。\n这里看起来是不是像两个函数之间的调用和被调用关系？确实有点像，但区别可大了，在任务1中我们并没有去显式地调用任务2！\n协程的最本质的解释是“可以挂起和恢复的函数”。例如上图中我们遇到耗时的IO操作了，我们就可以主动将当前运行的函数挂起（suspend），让其等待（await）IO操作，让线程去运行其他的函数，直到IO操作完成后再恢复（resume）。上下文切换的时机是靠调用方（写代码的开发人员）自身去控制的，这样协程的调度掌握在我们自己手中，相比与线程减小了系统切换上下文和其他资源的开销。因此协程在需要处理大量I/O操作或者并发任务的情况下提高程序的性能和可维护性。\nC++20带来的协程 C++20带来的协程并不像python或lua中的那么易用，相反，C++给我们提供的是更为底层的操作（不过C++23已经有std::generator这种更高级的抽象，之后也会有更多丰富的用法）。\nC++协程中有很重要的三个概念：\nPromise Awaitable Coroutine Handle Promise promise_type 是每个协程函数的幕后执行对象。它主要负责以下几个任务：\n创建和初始化协程：当协程开始执行时，编译器会通过 promise_type 创建一个对象，并调用其 get_return_object() 方法来获取协程的返回对象。 处理协程的暂停和恢复：协程在暂停时会调用 yield_value() 或 await_suspend() 等方法来处理协程的状态，并决定何时恢复。 处理协程的结束：当协程执行结束时，return_void() 或 return_value() 会被调用，来处理协程的返回结果。 以下是 promise_type 中一些常见的方法：\nget_return_object()：用于创建和返回协程的返回对象，一般是协程返回类型的实例。 initial_suspend()：返回一个 std::suspend_always 或 std::suspend_never，决定协程在启动时是否立即暂停。 final_suspend()：返回一个 std::suspend_always 或 std::suspend_never，决定协程在结束时是否暂停，以允许调用方执行清理操作。 return_void() 或 return_value(T value)：用于在协程完成时返回结果。return_void() 用于没有返回值的协程，而 return_value(T) 则用于有返回值的协程。 yield_value(T value)：用于生成值并让协程暂停，等待下一次恢复时继续执行。 Awaitable Awaitable 是一个可以与 co_await 表达式一起使用的对象或类型。Awaitable 对象必须提供一组特定的方法，使协程可以暂停执行，并在某个条件满足时继续执行。\nco_await 是 C++ 协程中的一种操作符，用于暂停协程并等待某个条件的满足。当协程遇到 co_await 时，它会暂停，并返回控制权给调用者。协程可以通过调用 co_await some_awaitable 来等待 some_awaitable 完成。\n一个 Awaitable 对象需要提供以下三个方法中的一个或多个：\noperator co_await：返回一个 Awaitable 对象。Awaitable 对象是实际实现等待逻辑的对象。 await_ready()：这是 Awaitable 对象上的方法。它返回一个 bool，用于指示是否需要等待。如果返回 true，协程将不会暂停。 await_suspend(std::coroutine_handle\u0026lt;\u0026gt;)：这是 Awaitable 对象上的方法。它接受一个 std::coroutine_handle\u0026lt;\u0026gt; 参数，并在协程暂停时调用。这个方法决定协程何时恢复执行。 await_resume()：这是 Awaitable 对象上的方法。它在协程恢复时调用，并返回 co_await 表达式的结果。 C++中提供了两个简单的Awaitable：std::suspend_never和std::suspend_always。\nCoroutine Handle std::coroutine_handle 是 C++20 协程库中一个核心的工具类，用于表示和操作协程。它是一个模板类，通常用来指向协程的状态信息。它可以通过协程的 promise_type 访问和控制协程的状态。每个协程在创建时，都会生成一个 std::coroutine_handle，用于管理协程的生命周期。\nstd::coroutine_handle 提供了一系列方法来控制协程的执行，包括以下几个主要功能：\n创建和获取句柄： std::coroutine_handle\u0026lt;\u0026gt;::from_address(void* ptr)：通过指针获取一个句柄。 std::coroutine_handle\u0026lt;promise_type\u0026gt;::from_promise(promise_type\u0026amp; promise)：通过 promise_type 对象创建一个句柄。 控制协程的执行： void resume()：恢复协程的执行。 void destroy()：销毁协程并释放其占用的资源。 void operator()()：等效于 resume()，恢复协程的执行。 void* address()：返回协程句柄的地址，用于低级操作。 检查协程的状态： bool done()：检查协程是否已经完成执行。 访问 promise_type： promise_type\u0026amp; promise()：获取与当前协程关联的 promise_type 对象，允许访问协程内部状态。 简单示例 这里有一个使用C++20 coroutine来实现挂起和恢复函数的例子。\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 #include \u0026lt;coroutine\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; using namespace std::chrono_literals; struct Result { struct Promise { Result get_return_object() { return std::coroutine_handle\u0026lt;Promise\u0026gt;::from_promise(*this); } std::suspend_never initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {} }; using promise_type = Promise; Result(std::coroutine_handle\u0026lt;Promise\u0026gt; h) : handle(h) {} std::coroutine_handle\u0026lt;Promise\u0026gt; handle; }; Result hello() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello \u0026#34; \u0026lt;\u0026lt; std::endl; co_await std::suspend_always{}; // 挂起hello std::cout \u0026lt;\u0026lt; \u0026#34;world!\u0026#34; \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;hello one\\n\u0026#34;; } Result hello2() { std::cout \u0026lt;\u0026lt; \u0026#34;你好 \u0026#34; \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; \u0026#34;hello two two\\n\u0026#34;; co_await std::suspend_always{}; // 挂起hello2 std::cout \u0026lt;\u0026lt; \u0026#34;世界!\u0026#34; \u0026lt;\u0026lt; std::endl; } int main() { Result coro = hello(); Result coro2 = hello2(); coro.handle.resume(); // 恢复hello coro2.handle.resume(); // 恢复hello2 } 在main函数中，一开始我们启动了协程hello，输出了“Hello”后被挂起；之后启动了协程hello2，其输出“你好\\nhello two two\\n”后也被挂起。紧接着我们通过coroutine_handle来依次恢复这两个协程，最终输出结果为：\n1 2 3 4 5 6 Hello 你好 hello two two world! hello one 世界! References https://zplutor.github.io/2022/03/25/cpp-coroutine-beginner/ https://www.bluepuni.com/archives/stackless-coroutine-and-asio-coroutine https://zhuanlan.zhihu.com/p/355100152?utm_psn=1808059511308697600 https://itnext.io/c-20-coroutines-complete-guide-7c3fc08db89d https://jasonkayzk.github.io/2022/06/03/%E6%B5%85%E8%B0%88%E5%8D%8F%E7%A8%8B/ https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await https://juejin.cn/post/6844903715099377672 ","permalink":"https://kerolt.github.io/p/%E5%8D%8F%E7%A8%8Bc-20%E5%8D%8F%E7%A8%8B%E5%88%9D%E4%BD%93%E9%AA%8C/","summary":"\u003ch2 id=\"为什么我们需要协程\"\u003e为什么我们需要协程？\u003c/h2\u003e\n\u003cp\u003e为什么我们有了线程还需要协程呢？（其实这个问题不应该这么问，协程的出现在线程之前）在一个进程中虽然我们可以创建多个线程，但是在一个进程中能创建的线程数量是有限制的，并且线程的调度仍然受操作系统控制，也就是说线程何时抢占、何时被抢占对于开发者来说都是透明的，并且在调度的过程中还可能涉及到用户态和内核态的切换开销。\u003c/p\u003e","title":"【协程】C++20协程初体验"},{"content":"记录一下如何配置eslint（其实是怕下一次又被折磨）。\nuni-cli创建项目 由于我是在linux上使用uniapp，无法使用HBuilderX，故采用uni-cli来创建项目并使用vscode进行开发。\nuni-cli创建项目很简单，一行命令即可，这里采用vue3/vite版：\n1 npx degit dcloudio/uni-preset-vue#vite my-vue3-project 这里只用来创建项目，发布运行什么的具体可以看官网。\n使用eslint eslint的配置文件和配置方式我感觉有好多种，所以在配置的时候折腾了挺久，最好还是按照官网最新文档来：\neslint：https://eslint.org/docs/latest/use/getting-started eslint-plugin-vue：https://eslint.vuejs.org/ 安装eslint 1 npm init @eslint/config@latest 配置 安装完后，一般项目的根目录下就会出现eslint.config.mjs文件（eslint 9.4.0版本，采用vue框架），主体如下：\n1 2 3 4 5 6 7 8 9 10 import pluginVue from \u0026#34;eslint-plugin-vue\u0026#34;; export default [ ...... { rules: { } } ]; 现在我们就要来按照自己的要求来配置，最好是按照官方的文档来定制：eslint-plugin-vue，一般使用的规则会写在rules对象中。\n举个🌰 比如我希望在代码中使用双引号，并且语句使用分号结尾，那么eslint.config.mjs文件就可以这样写：\n1 2 3 4 5 6 7 8 9 10 11 import pluginVue from \u0026#34;eslint-plugin-vue\u0026#34;; export default [ ...pluginVue.configs[\u0026#34;flat/recommended\u0026#34;], { rules: { quotes: [\u0026#34;error\u0026#34;, \u0026#34;double\u0026#34;], semi: [\u0026#34;error\u0026#34;] } } ]; 如果在代码中碰到一些eslint的报错，例如\n那么可以在报错信息中给出的链接网页中查找\n在eslint.config.mjs中的rules中加入\u0026quot;vue/multi-word-component-names\u0026quot;: 0后，报错就消失了。\n","permalink":"https://kerolt.github.io/p/%E7%94%A8uni-cli%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E5%90%8E%E4%BD%BF%E7%94%A8eslint/","summary":"\u003cp\u003e记录一下如何配置eslint（其实是怕下一次又被折磨）。\u003c/p\u003e","title":"用uni-cli创建项目后使用eslint"},{"content":"起因是我想在 Linux 上想用 Uniapp，用 wine 的体验不是很好，故打算用虚拟机跑 Windows。安装好 VMware17.5.2 后，配置好 windows 镜像后，点击启动，却报了这样的错：\nCould not open /dev/vmmon: ?????????. Please make sure that the kernel module `vmmon\u0026rsquo; is loaded.\n搜了一下发现是原因为VMware 无法访问其必要的内核模块 vmmon：\n我首先是尝试了手动启用 VMware 模块，然后执行命令安装缺失的模块\n1 2 3 sudo /etc/init.d/vmware start sudo vmware-modconfig --console --install-all 但是还是无效，在查看别人的 博客 后，我手动去编译安装缺失的 vmmon 和 vmnet 模块：\n1 2 3 4 5 6 git clone https://github.com/mkubecek/vmware-host-modules cd vmware-host-modules git checkout workstation-17.5.1 sudo make sudo make install 执行完成后，这两个模块将会安装到 /lib/modules/6.1.0-18-amd64/misc 下\n1 2 3 4 5 kerolt  /usr/lib/modules/6.1.0-18-amd64/misc  $ ls -l 总计 7164 -rw-r--r-- 1 root root 3996784 5月27日 18:48 vmmon.ko -rw-r--r-- 1 root root 3337384 5月27日 18:48 vmnet.ko 本以为到现在已经结束，结果再次启动 VMware，还是不行，于是我想着可能是内核模块没有加载，采用如下命令查看：\n1 lsmod | grep vmmon 不出意外，没有输出，手动加载模块：\n1 2 sudo modprobe vmmon # modprobe: ERROR: could not insert \u0026#39;vmmon\u0026#39;: Key was rejected by service 根据该错误询问 ChatGPT，其给出的答复为：启用了安全启动（Secure Boot），导致系统拒绝加载未签名或未正确签名的内核模块。\n重启计算机，进入 BIOS/UEFI 设置，将 Secure Boot 设置为 Disabled，之后再次启动 VMware 时就没有问题了～\n","permalink":"https://kerolt.github.io/p/vmware17.5.2%E5%90%AF%E5%8A%A8%E8%B8%A9%E5%9D%91/","summary":"\u003cp\u003e起因是我想在 Linux 上想用 Uniapp，用 wine 的体验不是很好，故打算用虚拟机跑 Windows。安装好 VMware17.5.2 后，配置好 windows 镜像后，点击启动，却报了这样的错：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCould not open /dev/vmmon: ?????????. Please make sure that the kernel module `vmmon\u0026rsquo; is loaded.\u003c/p\u003e\n\u003c/blockquote\u003e","title":"VMware17.5.2启动踩坑"},{"content":"前言 std::cout重载了\u0026lt;\u0026lt;运算符，这使得写一些很短的代码时很方便。但是如果在多线程的条件下，cout并不是线程安全的。\n举例 举个例子，我们创建5个线程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; void Test() { std::cout \u0026lt;\u0026lt; \u0026#34;msg1\u0026#34; \u0026lt;\u0026lt; \u0026#34; msg2\u0026#34; \u0026lt;\u0026lt; \u0026#34; msg3\u0026#34; \u0026lt;\u0026lt; \u0026#34; thread_id = \u0026#34; \u0026lt;\u0026lt; std::this_thread::get_id() \u0026lt;\u0026lt; std::endl; } int main() { std::thread threads[5]; for (int i = 0; i \u0026lt; 5; i++) { threads[i] = std::thread(Test); } for (int i = 0; i \u0026lt; 5; i++) { threads[i].join(); } } 实际上，看样子好像控制台应该输出5行内容，但是运行结果可能是这样的\n1 2 3 4 5 msg1 msg2 msg3 thread_id = msg1 msg2 msg3 thread_id = 139926598575808139926606968512 msg1 msg2 msg3 thread_id = 139926590183104 msg1 msg2 msg3 thread_id = 139926455965376 msg1 msg2 msg3 thread_id = 139926581790400 这是因为cout在使用时可能会存在线程之间的打印信息乱串的问题，看一下编译器眼中我们这段程序中的cout是什么样的：\n1 std::operator\u0026lt;\u0026lt;(std::operator\u0026lt;\u0026lt;(std::operator\u0026lt;\u0026lt;(std::operator\u0026lt;\u0026lt;(std::operator\u0026lt;\u0026lt;(std::cout, \u0026#34;msg1\u0026#34;), \u0026#34; msg2\u0026#34;), \u0026#34; msg3\u0026#34;), \u0026#34; thread_id = \u0026#34;), std::this_thread::get_id()).operator\u0026lt;\u0026lt;(std::endl); 可以看到，这不是通过单个 std::operator\u0026lt;\u0026lt; 调用完成的，也就是说这个操作并不是原子的\n解决方法 使用std::format（C++20） 使用第三方库，如folly，fmtlib等 使用stringstream \u0026hellip;\u0026hellip; 这里使用stringstream做个演示。将Test函数修改如下：\n1 2 3 4 5 6 std::stringstream ss; ss \u0026lt;\u0026lt; \u0026#34;msg1\u0026#34; \u0026lt;\u0026lt; \u0026#34; msg2\u0026#34; \u0026lt;\u0026lt; \u0026#34; msg3\u0026#34; \u0026lt;\u0026lt; \u0026#34; thread_id = \u0026#34; \u0026lt;\u0026lt; std::this_thread::get_id() \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; ss.str(); 这样，控制台的输出就不会乱串了。\n","permalink":"https://kerolt.github.io/p/%E9%81%BF%E5%85%8Dcout%E7%BA%BF%E7%A8%8B%E4%B8%8D%E5%AE%89%E5%85%A8%E7%9A%84%E4%B8%80%E4%B8%AA%E5%81%9A%E6%B3%95/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003estd::cout\u003c/code\u003e重载了\u003ccode\u003e\u0026lt;\u0026lt;\u003c/code\u003e运算符，这使得写一些很短的代码时很方便。但是如果在多线程的条件下，cout并不是线程安全的。\u003c/p\u003e","title":"避免cout线程不安全的一个做法"},{"content":"之前对于移动语义的理解就是使用std::move将一个对象所占有的资源的所有权转移给另一个对象，但是只要使用std::move就足够了吗？这显然是错误的。\n看一下std::move的源码（g++12.2）\n1 2 3 4 5 6 7 8 9 10 /** * @brief Convert a value to an rvalue. * @param __t A thing of arbitrary type. * @return The parameter cast to an rvalue-reference to allow moving it. */ template\u0026lt;typename _Tp\u0026gt; _GLIBCXX_NODISCARD constexpr typename std::remove_reference\u0026lt;_Tp\u0026gt;::type\u0026amp;\u0026amp; move(_Tp\u0026amp;\u0026amp; __t) noexcept { return static_cast\u0026lt;typename std::remove_reference\u0026lt;_Tp\u0026gt;::type\u0026amp;\u0026amp;\u0026gt;(__t); } 其实move的实现并没有很复杂，粗略一点的理解就是将一个左值强制转换为右值。\nstd::move 并不会真正地移动对象，真正的移动操作是在移动构造函数、移动赋值函数等完成的，std::move 只是将参数转换为右值引用而已。\n写一个简单的例子如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;fmt/core.h\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;utility\u0026gt; struct A { A(std::string str) : data(str) {} A(const A\u0026amp;) { puts(\u0026#34;copy\u0026#34;); } A(A\u0026amp;\u0026amp;) { puts(\u0026#34;move\u0026#34;); } std::string data = \u0026#34;default\u0026#34;; }; int main() { A a(\u0026#34;hello\u0026#34;); A a2(std::move(a)); fmt::print(\u0026#34;a: {}, a2: {}\\n\u0026#34;, a.data, a2.data); } 看样子我们使用std::move后a2的data数据应该为“hello”，但是运行结果为\nmove a: hello, a2: default 虽然使用move匹配到了A的移动构造函数，但是在上文提到过，std::move仅仅只是一个强制转换，并没有实现真正的移动！但要是我们不写A中的移动构造函数或是将其设置成default：\n1 2 3 4 5 6 7 8 9 10 struct A { A(std::string str) : data(str) {} A(const A\u0026amp;) { puts(\u0026#34;copy\u0026#34;); } A(A\u0026amp;\u0026amp;) = default; std::string data = \u0026#34;default\u0026#34;; }; 这样，运行结果为\na: , a2: hello 这是因为当我们不显式指定移动构造函数（或是拷贝构造函数、移动or拷贝运算符）编译器会自动生成，貌似也一并实现了数据的移动（？这我也还不清楚）\n我们通常使用std::move能够实现标准库中一些资源的转移，是因为标准库中已经实现了这些资源类的移动构造函数or移动赋值运算符。\n","permalink":"https://kerolt.github.io/p/%E7%BA%A0%E6%AD%A3%E4%B8%80%E4%B8%8B%E5%AF%B9cpp%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89%E7%9A%84%E9%94%99%E8%AF%AF%E7%90%86%E8%A7%A3/","summary":"\u003cp\u003e之前对于移动语义的理解就是使用std::move将一个对象所占有的资源的所有权转移给另一个对象，但是只要使用std::move就足够了吗？这显然是错误的。\u003c/p\u003e","title":"纠正一下对cpp移动语义的错误理解"},{"content":"想写这篇博客的原因是在刷力扣的 347. 前 K 个高频元素 一题时，需要使用到优先队列priority_queue，其定义如下：\n1 2 3 4 5 template\u0026lt; class T, class Container = std::vector\u0026lt;T\u0026gt;, class Compare = std::less\u0026lt;typename Container::value_type\u0026gt; \u0026gt; class priority_queue; 第三个参数是一个可以自定义的比较类型，其必须满足二元谓词，通常可以使用如下两种方法：\n使用自定义的函数对象 lambda表达式 使用std::greater或std::less（这里就不介绍这种方法了） 以题 347. 前 K 个高频元素 为例，我们要建立一个小根堆，那么代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 // 方法一 using PII = pair\u0026lt;int, int\u0026gt;; // 比较类，重载了括号运算符 struct Comp { bool operator()(PII\u0026amp; p1, PII\u0026amp; p2) { return p1.second \u0026gt; p2.second; } }; priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, Comp\u0026gt; pq; 当然，另一种方法就是使用lambda表达式，如下：\n1 2 3 4 5 6 7 8 // 方法二 using PII = pair\u0026lt;int, int\u0026gt;; auto comp = [](PII\u0026amp; p1, PII\u0026amp; p2) { return p1.second \u0026gt; p2.second; }; // 注意这里需要使用decltype priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, decltype(comp)\u0026gt; pq; 但值得注意的是，方法二需要在C++20下才可使用。这是因为priority_queue的第三个模板形参需要的二元谓词要求可复制构造。\nlambda表达式即构造闭包（能够捕获作用域中的变量的无名函数对象）。而在C++20之前，闭包类型非可默认构造，闭包类型没有默认构造函数。C++20及之后，如果没有指定捕获，那么闭包类型拥有预置的默认构造函数。\n而在目前，力扣中C++编译器使用的是clang17，支持C++20，故使用lambda表达式是没有问题的。\n","permalink":"https://kerolt.github.io/p/c-%E4%B8%ADlambda%E4%B8%8Epriority_queue%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8/","summary":"\u003cp\u003e想写这篇博客的原因是在刷力扣的 \u003ca href=\"https://leetcode.cn/problems/top-k-frequent-elements/\"\u003e347. 前 K 个高频元素\u003c/a\u003e 一题时，需要使用到优先队列\u003ccode\u003epriority_queue\u003c/code\u003e，其定义如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eContainer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003evector\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCompare\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eless\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etypename\u003c/span\u003e \u003cspan class=\"n\"\u003eContainer\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003evalue_type\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003epriority_queue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e第三个参数是一个可以自定义的比较类型，其必须满足\u003ca href=\"https://zh.cppreference.com/w/cpp/named_req/BinaryPredicate\"\u003e二元谓词\u003c/a\u003e，通常可以使用如下两种方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e使用自定义的函数对象\u003c/li\u003e\n\u003cli\u003elambda表达式\u003c/li\u003e\n\u003cli\u003e使用\u003ccode\u003estd::greater\u003c/code\u003e或\u003ccode\u003estd::less\u003c/code\u003e（这里就不介绍这种方法了）\u003c/li\u003e\n\u003c/ol\u003e","title":"C++中lambda与priority_queue一起使用"},{"content":"下载源码 链接：https://github.com/chenshuo/muduo/releases/tag/v2.0.2\n编译安装 解压后，在项目根目录中更改 CMakeLists.txt 文件\n如图，将 option 属性注释掉（这是muduo的例子的编译选项，如果开启将增加编译时间）\n之后，执行\n1 2 ./build.sh ./build.sh install 如果没有出错的话，在与muduo-v2.0.2同目录下将生成一个 build 目录，其中有\nrelease-install-cpp11 ├── include │ └── muduo │ ├── base │ └── net │ ├── http │ └── inspect └── lib 将include目录和lib目录下的内容复制到系统路径下：\n1 2 mv include/muduo /usr/local/include mv lib/* /usr/local/lib ok，现在就可以使用muduo库了\n测试使用 测试代码可以参考博客 https://www.cnblogs.com/conefirst/articles/15224039.html\n","permalink":"https://kerolt.github.io/p/muduo%E7%BD%91%E7%BB%9C%E5%BA%93%E7%9A%84%E5%AE%89%E8%A3%85/","summary":"\u003ch2 id=\"下载源码\"\u003e下载源码\u003c/h2\u003e\n\u003cp\u003e链接：\u003ca href=\"https://github.com/chenshuo/muduo/releases/tag/v2.0.2\"\u003ehttps://github.com/chenshuo/muduo/releases/tag/v2.0.2\u003c/a\u003e\u003c/p\u003e","title":"muduo网络库的安装"},{"content":"前言 在Vue3项目中，如果我们想上传图片一般可以利用element-ui中的el-upload，为了避免代码的重复，我们可以自己封装一个图片上传组件。\n其中，主要实现思想为前端利用el-upload组件选择上传的图片，并利用其http-request属性来自定义函数来实现文件上传请求：该请求函数使用七牛云的对象存储，在通过后端得到的上传凭证token后来实现文件上传。\n后端代码 使用express框架，获取七牛云上传凭证并响应给前端\n项目结构 - routes |- token.js |- index.js - app.js - config.js - package.json 安装七牛云的SDK： npm i qiniu 获取上传凭证 编写获取上传凭证的相关代码：\n/* config.js */ ​ const qiniu = require(\u0026#39;qiniu\u0026#39;) ​ // 创建上传凭证 const accessKey = \u0026#39;*****\u0026#39; // 这里填写七牛云的accessKey const secretKey = \u0026#39;*****\u0026#39;// 这里填写七牛云的secretKey const mac = new qiniu.auth.digest.Mac(accessKey, secretKey) const options = { scope: \u0026#39;*****\u0026#39;, // 这里填写七牛云空间名称 expires: 60 * 60 * 24 * 7 // 这里是凭证的有效时间，默认是一小时 } const putPolicy = new qiniu.rs.PutPolicy(options) const uploadToken = putPolicy.uploadToken(mac) ​ module.exports = { uploadToken } 配置路由 token.js\nconst tokenRouter = require(\u0026#39;express\u0026#39;).Router() const qnconfig = require(\u0026#39;../config\u0026#39;) // 引入七牛云配置 ​ tokenRouter.get(\u0026#39;/qiniu\u0026#39;, (req, res, next) =\u0026gt; { res.status(200).send(qnconfig.uploadToken) }) ​ module.exports = tokenRouter index.js\nconst token = require(\u0026#39;./token\u0026#39;) ​ module.exports = routes = (app) =\u0026gt; { app.use(\u0026#39;/token\u0026#39;, token) // 可以通过/token/qiniu的方式获取上传凭证 } 项目启动 const express = require(\u0026#39;express\u0026#39;) const bodyparse = require(\u0026#39;body-parser\u0026#39;) const routers = require(\u0026#39;./route\u0026#39;) ​ // 创建服务 const app = express() // 解析数据 app.use(bodyparse.json()) ​ // 路由 routes(app) ​ // 监听3000端口 app.listen(3000, () =\u0026gt; { console.log(\u0026#39;this server are running on localhost:3000!\u0026#39;) }) 使用命令node app.js启动项目，这时访问http://localhost:3000/token/qiniu即可获取上传凭证了。\n前端代码 配置跨域 由于前后端项目运行在不同的端口，因此需要解决跨域问题，这里在vite.config.js中解决如下：\nserver: { proxy: { \u0026#39;/api\u0026#39;: { target: \u0026#39;http://localhost:3000\u0026#39;, changeOrigin: true, rewrite: (path) =\u0026gt; path.replace(/^/api/, \u0026#39;\u0026#39;) } } } 父组件使用 我们希望子组件上传图片得到一串url后父组件能接受到，并且在展示上传图片时其尺寸应能指定或者有默认值。\n\u0026lt;template\u0026gt; \u0026lt;Upload :url=\u0026#34;imageUrl\u0026#34; @upload=\u0026#34;changeUrl\u0026#34; /\u0026gt; \u0026lt;/template\u0026gt; ​ \u0026lt;script setup\u0026gt; import Upload from \u0026#39;@/components/Upload.vue\u0026#39; import { ref } from \u0026#39;vue\u0026#39; const imageUrl = ref(\u0026#39;\u0026#39;) ​ const changeUrl = (url) =\u0026gt; { imageUrl.value = url } \u0026lt;/script\u0026gt; 封装组件Upload.vue 这里只是简单使用axios，没有对其进行封装。\n\u0026lt;template\u0026gt; \u0026lt;!- action=\u0026#34;https://upload-z2.qiniup.com\u0026#34;：每个地区访问域名不同，具体可通过 https://developer.qiniu.com/kodo/1671/region-endpoint-fq 查看 -\u0026gt;\t\u0026lt;el-upload class=\u0026#34;avatar-uploader\u0026#34; action=\u0026#34;https://upload-z2.qiniup.com\u0026#34; :show-file-list=\u0026#34;false\u0026#34; :http-request=\u0026#34;up2qiniu\u0026#34; :before-upload=\u0026#34;beforeUpload\u0026#34; \u0026gt; \u0026lt;img v-if=\u0026#34;props.url\u0026#34; :src=\u0026#34;props.url\u0026#34; class=\u0026#34;avatar\u0026#34; :style=\u0026#34;\u0026#39;width: \u0026#39; + props.width + \u0026#39;px;\u0026#39; + \u0026#39;height: \u0026#39; + props.height + \u0026#39;px;\u0026#39;\u0026#34; /\u0026gt; \u0026lt;el-icon v-else class=\u0026#34;avatar-uploader-icon\u0026#34; :style=\u0026#34;\u0026#39;width: \u0026#39; + props.width + \u0026#39;px;\u0026#39; + \u0026#39;height: \u0026#39; + props.height + \u0026#39;px;\u0026#39;\u0026#34; \u0026gt;\u0026lt;Plus /\u0026gt;\u0026lt;/el-icon\u0026gt; \u0026lt;/el-upload\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script setup\u0026gt; import { ref } from \u0026#39;vue\u0026#39; import { getQiniuToken } from \u0026#39;../api/token\u0026#39; import axios from \u0026#39;axios\u0026#39; import { ElMessage } from \u0026#39;element-plus\u0026#39; const qiniuaddr = \u0026#39;rlr92qkze.hn-bkt.clouddn.com\u0026#39; // 这里是七牛云存储对象中的CDN域名 const imageUrl = ref(\u0026#39;\u0026#39;) // 父组件传值时，须有图片的url；其次可选择图片的宽高（默认都为180） const props = defineProps({ url: String, width: { type: Number, default: 180 }, height: { type: Number, default: 180 } }) const emit = defineEmits([\u0026#39;upload\u0026#39;]) const beforeUpload = (rawFile) =\u0026gt; { if (rawFile.type !== \u0026#39;image/jpg\u0026#39; \u0026amp;\u0026amp; rawFile.type !== \u0026#39;image/png\u0026#39;) { ElMessage.error(\u0026#39;图片格式应该是png或jpg\u0026#39;) return false } else if (rawFile.size / 1024 / 1024 \u0026gt; 2) { ElMessage.error(\u0026#39;图片大小应该小于2MB\u0026#39;) return false } return true } /** * 上传图片至七牛云 * @param {*} req */ const up2qiniu = (req) =\u0026gt; { const config = { headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;multipart/form-data\u0026#39; } } const fileType = req.file.type === \u0026#39;image/png\u0026#39; ? \u0026#39;png\u0026#39; : \u0026#39;jpg\u0026#39; // 重命名要上传的文件 const keyname = \u0026#39;blog\u0026#39; + new Date().getTime() + \u0026#39;.\u0026#39; + fileType axios.get(\u0026#39;/api/token/qiniu\u0026#39;).then(res =\u0026gt; { const formdata = new FormData() formdata.append(\u0026#39;file\u0026#39;, req.file) formdata.append(\u0026#39;token\u0026#39;, res.data) formdata.append(\u0026#39;key\u0026#39;, keyname) // 获取到凭证之后再将文件上传到七牛云空间 axios.post(\u0026#39;https://upload-z2.qiniup.com\u0026#39;, formdata, config).then((res) =\u0026gt; { imageUrl.value = \u0026#39;http://\u0026#39; + qiniuaddr + \u0026#39;/\u0026#39; + res.data.key emit(\u0026#39;upload\u0026#39;, imageUrl.value) // 向父组件传递图片的url }) }) } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;scss\u0026#34; scoped\u0026gt; .avatar-uploader .avatar { width: 360px; height: 180px; display: block; } .avatar-uploader :deep(.el-upload) { border: 1px dashed var(--el-border-color); border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: var(--el-transition-duration-fast); } .avatar-uploader :deep(.el-upload:hover) { border-color: var(--el-color-primary); } .el-icon.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 360px; height: 180px; text-align: center; } \u0026lt;/style\u0026gt; ","permalink":"https://kerolt.github.io/p/vue3%E5%B0%81%E8%A3%85el-upload/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e在Vue3项目中，如果我们想上传图片一般可以利用element-ui中的\u003ccode\u003eel-upload\u003c/code\u003e，为了避免代码的重复，我们可以自己封装一个图片上传组件。\u003c/p\u003e\n\u003cp\u003e其中，主要实现思想为前端利用el-upload组件选择上传的图片，并利用其\u003ccode\u003ehttp-request\u003c/code\u003e属性来自定义函数来实现文件上传请求：该请求函数使用七牛云的对象存储，在通过后端得到的上传凭证token后来实现文件上传。\u003c/p\u003e","title":"Vue3封装el-upload"}]